1. 28 Apr, 2026 2 commits
    • DaydreamCoding's avatar
      feat(openai): OpenAI Fast/Flex Policy 完整实现(HTTP + WebSocket + Admin) · 30f55a1f
      DaydreamCoding authored
      
      
      对称参照 Claude BetaPolicy 的 fast-mode 过滤实现,新增针对 OpenAI 上游
      service_tier 字段(priority / flex,含客户端 "fast" → "priority" 归一化)的
      pass / filter / block 三态策略,覆盖全部 OpenAI 入口 + admin 配置入口。
      
      后端核心
      - 新增 SettingKeyOpenAIFastPolicySettings、OpenAIFastPolicyRule、
        OpenAIFastPolicySettings 配置模型,含规则的 service_tier × action × scope
        × 模型白名单 × fallback action 维度。
      - SettingService.Get/SetOpenAIFastPolicySettings;缺失时返回内置默认策略
        (所有模型的 priority 走 filter,whitelist 为空,fallback=pass)。设计
        依据:service_tier=fast 是用户级开关,与 model 字段正交,默认锁定特定
        model slug 会留下"用 gpt-4 + fast 透传 priority 上游"的绕过路径。JSON
        解析失败不再静默 fallback,slog.Warn 记录脏数据,便于运维定位。
      - service_tier 归一化(trim + ToLower + fast→priority + 白名单 priority/flex)
        与策略评估(evaluateOpenAIFastPolicy)作为唯一真实来源,HTTP / WS 共用。
        抽出纯函数 evaluateOpenAIFastPolicyWithSettings,配合 ctx-bound settings
        快照(withOpenAIFastPolicyContext / openAIFastPolicySettingsFromContext),
        WS 长会话入口预取一次后所有帧复用,避免每帧打到 settingService。
      
      HTTP 入口(4 个)
      - Chat Completions、Anthropic 兼容(Messages,含 BetaFastMode→priority 二次
        命中)、原生 Responses、Passthrough Responses 全部接入
        applyOpenAIFastPolicyToBody,filter 走 sjson 顶层删除 service_tier,block
        返回 403 forbidden_error JSON。
      - 4 入口统一使用 upstream 视角的 model(GetMappedModel +
        normalizeOpenAIModelForUpstream + Codex OAuth normalize 后的 slug),
        避免 chat/messages/native /responses/passthrough 因为 model 维度不同
        造成 whitelist 命中差异。
      - 在 pass 路径也把客户端 "fast" 别名归一化为 "priority" 写回 body,
        否则 native /responses 与 passthrough 入口会把 "fast" 原样透传给上游
        导致 400/拒绝(chat-completions 入口的 normalizeResponsesBodyServiceTier
        此前已具备同等行为)。
      
      WebSocket 入口
      - 新增 applyOpenAIFastPolicyToWSResponseCreate:严格匹配
        type="response.create",仅处理顶层 service_tier;filter 用 sjson 删字段,
        block 返回 typed *OpenAIFastBlockedError。
      - ingress 路径在 parseClientPayload 内调用,block 命中先 Write Realtime
        风格 error event 再返回 OpenAIWSClientCloseError(StatusPolicyViolation
        =1008),依赖底层 WebSocket Conn.Write 的同步 flush 保证 error 先于
        close。
      - passthrough 路径在 RunEntry 前对 firstClientMessage 应用策略,并通过
        openAIWSPolicyEnforcingFrameConn 包装 ReadFrame 对每个 client→upstream
        帧执行策略;后续帧无 model 字段时回退到 capturedSessionModel。
        filter 闭包内同时侦测 session.update / session.created 帧的 session.model
        字段刷新 capturedSessionModel,封堵"首帧 model=gpt-4o(pass)→
        session.update 改为 gpt-5.5 → 不带 model 的 response.create fallback
        到 gpt-4o"的 mid-session 绕过路径。
      - passthrough billing:requestServiceTier 在策略 filter 之后再从
        firstClientMessage 提取,filter 命中时 OpenAIForwardResult.ServiceTier
        上报 nil(default tier),与 HTTP 入口(reqBody 来自 post-filter map)
        / WS ingress(payload 来自 post-filter bytes)的语义一致。
      - 错误事件 schema:{event_id: "evt_<32hex>", type: "error",
        error: {type: "forbidden_error", code: "policy_violation", message}},
        与 OpenAI codex 客户端 error event 解析兼容。
      
      Admin / Frontend
      - dto.SystemSettings / UpdateSettingsRequest 新增
        openai_fast_policy_settings 字段(omitempty),bulk GET/PUT 接入。
      - Settings 页 Gateway 页签新增 Fast/Flex Policy 表单卡片:
        service_tier × action × scope × 模型白名单 × fallback action 全字段配置。
      - 前端守门:openaiFastPolicyLoaded 标志仅在 GET 真带回字段时才允许回写,
        避免 rollout/错误把默认规则覆盖成空;saveSettings 回写循环 skip 该字段,
        由专用刷新逻辑处理;仅 action=block 时发送 error_message,匹配后端
        omitempty 行为。
      
      测试
      - HTTP 路径:openai_fast_policy_test.go 覆盖默认配置(whitelist=[],所有
        模型 priority filter)/ block 自定义错误 / scope 区分 / filter 删字段 /
        block 不改 body / block 短路上游 / Anthropic BetaFastMode 触发 OpenAI
        fast policy 等场景。
      - WebSocket 路径:openai_fast_policy_ws_test.go 覆盖
          helper 单元(filter / fast→priority 归一化 / flex 透传 / block typed
          error / 无 service_tier 字节不变 / 非 response.create 帧不动 / 空 type
          帧不动 / event_id+code 字段断言 / 非字符串 service_tier 容错)+
          pass 路径 fast 别名归一化回归 +
          ingress 端到端(filter 后上游不含 service_tier / block 后客户端先收
          error event 再收 close 1008 且上游 0 写)+
          passthrough capturedSessionModel fallback 用例(whitelist 策略下首帧
          建立、缺 model 命中 fallback、缺少 fallback 时的 leak 文档化)+
          passthrough session.update / session.created 旋转 capturedSessionModel
          的 mid-session 绕过回归 +
          passthrough billing post-filter ServiceTier 与 idempotent filter 回归。
      Co-Authored-By: default avatarClaude Opus 4.7 (1M context) <noreply@anthropic.com>
      30f55a1f
    • Zven's avatar
      3d4ca5e8
  2. 26 Apr, 2026 1 commit
  3. 25 Apr, 2026 5 commits
  4. 24 Apr, 2026 2 commits
  5. 23 Apr, 2026 7 commits
  6. 22 Apr, 2026 1 commit
  7. 15 Apr, 2026 2 commits
  8. 14 Apr, 2026 6 commits
    • erio's avatar
      fix: merge general improvements from release branch · 63f539b3
      erio authored
      Backend:
      - gateway_handler: pass subject.UserID instead of int64(0) for user-level routing
      - setting_handler: add missing BalanceLowNotifyRechargeURL to UpdateSettings response
      - openai_gateway_service: use applyAccountStatsCost for account stats pricing integration
      - embed_on: add local file override (data/public/) for embedded frontend assets
      
      Frontend:
      - useTableSelection: add batchUpdate method for batch operations
      - AccountsView: virtual scrolling params, Set-based isSelected, swipe virtualization
      - ProxiesView: add batchUpdate to selection and swipe-select
      - BulkEditAccountModal: fix submit handler to prevent event object as argument
      - SettingsView: move payload construction outside try block
      - i18n: add general translation keys (saved, deleted, view, validation, allowUserRefund)
      - api/client: reorder error fields for consistency
      - stores/payment: clarify pollOrderStatus JSDoc
      63f539b3
    • erio's avatar
      fix: correct account stats pricing priority order · 98c9d517
      erio authored
      Priority was wrong:
      - Before: custom rules → LiteLLM (when ApplyPricingToAccountStats) → nil
      - After:  custom rules → totalCost (when ApplyPricingToAccountStats) → LiteLLM → nil
      
      When ApplyPricingToAccountStats is enabled, use the request's actual
      client billing cost (before multiplier) as account_stats_cost, instead
      of recalculating from LiteLLM per-token prices which produced incorrect
      values for per-request billing mode.
      
      LiteLLM model pricing is now the final fallback (priority 3), used only
      when neither custom rules nor ApplyPricingToAccountStats apply.
      98c9d517
    • erio's avatar
      feat: WebSearch tri-state, account stats pricing fix, quota cache fix, usage tooltip · 1262654d
      erio authored
      WebSearch tri-state switch:
      - Account-level web_search_emulation changed from bool to tri-state
        string: "default" (follow channel) / "enabled" / "disabled"
      - shouldEmulateWebSearch checks channel config when account is "default"
      - SQL migration converts old bool values
      - Frontend select replaces toggle in Edit/CreateAccountModal
      
      Account stats pricing:
      - resolveAccountStatsCost uses upstream model (post-mapping) for matching
      - Priority: custom rules → model pricing file (when toggle on) → default
      - Custom rules always configurable, independent of toggle
      - Account ID field changed to searchable selector filtered by platform
      - Description updated to reflect new behavior
      
      Quota notification cache fix:
      - CheckAccountQuotaAfterIncrement fetches real-time account from DB
      - Reconstructs pre-increment usage for accurate threshold crossing detection
      - New AccountQuotaReader interface (minimal: GetByID only)
      
      Usage tooltip:
      - Per-request/image billing shows per-request price instead of $0 token price
      - Token billing continues to show input/output price per million tokens
      1262654d
    • erio's avatar
      fix(channel): use upstream model for account stats pricing and remove channel pricing fallback · 11c46068
      erio authored
      - resolveAccountStatsCost now uses the final upstream model (after
        account-level mapping) to match custom pricing rules, fixing the
        issue where requested model (e.g. claude-sonnet-4-5) didn't match
        rules configured for upstream model (e.g. claude-opus-4-6)
      - Remove tryChannelPricing fallback — only custom rules are applied,
        unmatched requests use default formula (total_cost × rate)
      - Remove unused billingService and serviceTier parameters
      - Update description: "启用后将支持自定义账号统计的模型价格"
      11c46068
    • erio's avatar
      feat(notify): add balance low & account quota notification system · b32d1a2c
      erio authored
      - User balance low notification: email alert when balance drops below
        configurable threshold (user email + verified extra emails)
      - Account quota notification: broadcast email to admin-configured
        recipients when daily/weekly/total quota usage exceeds alert threshold
      - Admin settings: global enable/disable, default threshold, quota
        notification email list (Email Settings tab)
      - User profile: enable/disable, custom threshold, add/remove extra
        notification emails with verification code flow
      - Account quota: per-dimension alert toggle and threshold in quota
        control card
      - Trigger logic: first-crossing only (old >= threshold && new < threshold
        for balance; old < threshold && new >= threshold for quota), naturally
        prevents duplicate notifications without Redis dedup
      b32d1a2c
    • erio's avatar
      feat(channels): add custom account stats pricing rules · 7535e312
      erio authored
      Allow channels to configure independent model pricing for account
      statistics cost calculation, decoupled from user billing.
      
      Backend:
      - Migration 101: channels.apply_pricing_to_account_stats toggle,
        channel_account_stats_pricing_rules/model_pricing tables,
        usage_logs.account_stats_cost column
      - resolveAccountStatsCost: match rules by group/account, then channel
        pricing, fallback to original formula when unconfigured
      - Integrate into both GatewayService.recordUsageCore and
        OpenAIGatewayService.RecordUsage
      - Update 8 account stats SQL queries to use
        COALESCE(account_stats_cost, total_cost) * account_rate_multiplier
      - 23 unit tests for matching, pricing lookup, and cost calculation
      
      Frontend:
      - Channel edit dialog: toggle + custom rules UI with group/account
        multi-select and pricing entry cards
      - API types and i18n (zh/en)
      7535e312
  9. 08 Apr, 2026 1 commit
  10. 07 Apr, 2026 3 commits
    • Elysia's avatar
      fix: 非流式响应路径扩展SSE检测至所有账号类型 (#1493) · 9e515ea7
      Elysia authored
      
      
      当上游返回SSE格式响应(如sub2api链路)时,API Key账号的非流式路径
      未检测SSE,导致终态事件中空output直接透传给客户端。
      
      - 将Content-Type SSE检测从仅OAuth扩展至所有账号类型
      - 重命名handleOAuthSSEToJSON为handleSSEToJSON(无OAuth专属逻辑)
      - 为透传路径新增handlePassthroughSSEToJSON,支持SSE转JSON及空output重建
      Co-Authored-By: default avatarClaude Opus 4.6 (1M context) <noreply@anthropic.com>
      9e515ea7
    • shaw's avatar
      fix: 非流式路径在上游终态事件output为空时从delta事件重建响应内容 · b2e379cf
      shaw authored
      上游API近期更新后,response.completed终态SSE事件的output字段可能为空,
      实际内容仅通过response.output_text.delta等增量事件下发。流式路径不受影响,
      但chat_completions非流式路径和responses OAuth非流式路径只依赖终态事件的
      output,导致返回空响应。
      
      新增BufferedResponseAccumulator累积器,在SSE扫描过程中收集delta事件内容
      (文本、function_call、reasoning),当终态output为空时补充重建。
      
      同时修复handleChatBufferedStreamingResponse遗漏response.done事件类型的问题。
      b2e379cf
    • Alex's avatar
      7eecc49c
  11. 05 Apr, 2026 1 commit
    • shaw's avatar
      fix(billing): prevent channel_mapped override from reverting BillingModel when channel did not map · f585a15e
      shaw authored
      When a channel has no model mapping for the requested model, ChannelMappedModel
      equals OriginalModel (the user's arbitrary input). Combined with the default
      BillingModelSource="channel_mapped", this incorrectly overrides the BillingModel
      set by the OpenAI format conversion layer (e.g., gpt-5.4 from DefaultMappedModel)
      back to the unmapped original model (e.g., glm) which has no pricing — resulting
      in zero-cost billing.
      
      Add guard condition so the channel_mapped override only fires when the channel
      actually changed the model (ChannelMappedModel != OriginalModel).
      f585a15e
  12. 04 Apr, 2026 9 commits
    • erio's avatar
      refactor: remove resolveOpenAIUpstreamModel, use normalizeCodexModel directly · e27b0adb
      erio authored
      Eliminates unnecessary indirection layer. The wrapper function only
      called normalizeCodexModel with a special case for "gpt 5.3 codex spark"
      (space-separated variant) that is no longer needed.
      
      All call sites now use normalizeCodexModel directly.
      e27b0adb
    • erio's avatar
      feat(channel): improve cache strategy and add restriction logging · 58f758c8
      erio authored
      - Change channel cache TTL from 60s to 10min (reduce unnecessary DB queries)
      - Actively rebuild cache after CRUD instead of lazy invalidation
      - Add slog.Warn logging for channel pricing restriction blocks (4 places)
      58f758c8
    • erio's avatar
      fix: resolve 5 audit findings in channel/credits/scheduling · 71f61bbc
      erio authored
      P0-1: Credits degraded response retry + fail-open
      - Add isAntigravityDegradedResponse() to detect transient API failures
      - Retry up to 3 times with exponential backoff (500ms/1s/2s)
      - Invalidate singleflight cache between retries
      - Fail-open after exhausting retries instead of 5h circuit break
      
      P1-1: Fix channel restriction pre-check timing conflict
      - Swap checkClaudeCodeRestriction before checkChannelPricingRestriction
      - Ensures channel restriction is checked against final fallback groupID
      
      P1-2: Add interval pricing validation (frontend + backend)
      - Backend: ValidateIntervals() with boundary, price, overlap checks
      - Frontend: validateIntervals() with Chinese error messages
      - Rules: MinTokens>=0, MaxTokens>MinTokens, prices>=0, no overlap
      
      P2: Fix cross-platform same-model pricing/mapping override
      - Store cache keys using original platform instead of group platform
      - Lookup across matching platforms (antigravity→anthropic→gemini)
      - Prevents anthropic/gemini same-name models from overwriting each other
      71f61bbc
    • erio's avatar
      fix: address review findings for channel restriction refactoring · 1fca2bfa
      erio authored
      - Fix 7 stale comments still mentioning "限制检查" in handlers/services
      - Make billingModelForRestriction explicitly list channel_mapped case
      - Add slog.Warn for error swallowing in ResolveChannelMapping and
        needsUpstreamChannelRestrictionCheck
      - Document sticky session upstream check exemption
      1fca2bfa
    • erio's avatar
      refactor: replace magic strings with named constants · 0d241d52
      erio authored
      - PricingSourceChannel/LiteLLM/Fallback for resolver source
      - MediaTypeImage/Video/Prompt for result.MediaType
      - Reuse BillingModeToken/BillingModeImage for billing mode
      - Reuse BillingModelSourceChannelMapped/PlatformAnthropic in handler
      0d241d52
    • erio's avatar
      fix: address audit findings - cache sync, validation, consistency · 9b213115
      erio authored
      - clearCreditsExhausted: sync Redis scheduler cache after DB update
      - Image billing mode UI: write to per_request_price instead of image_output_price
      - OpenAI RecordUsage: use BillingModelSourceRequested constant, add s.cfg nil guard
      - Fix i18n key path: admin.channels.perRequestPriceRequired → admin.channels.form.perRequestPriceRequired
      9b213115
    • erio's avatar
      fix: golangci-lint test assertion and gofmt · c9145ad4
      erio authored
      c9145ad4
    • erio's avatar
      fix: resolve golangci-lint issues · 3851628a
      erio authored
      - Fix errcheck: defer rows.Close() with nolint
      - Fix errcheck: type assertion with ok check in channel cache
      - Fix staticcheck ST1005: lowercase error string
      - Fix staticcheck SA5011: nil check cost before use in openai gateway
      - Fix gofmt: format chatcompletions_to_responses.go
      3851628a
    • erio's avatar
      feat: image output token billing, channel-mapped billing source, credits balance precheck · d72ac926
      erio authored
      - Parse candidatesTokensDetails from Gemini API to separate image/text output tokens
      - Add image_output_tokens and image_output_cost to usage_log (migration 089)
      - Support per-image-token pricing via output_cost_per_image_token from model pricing data
      - Channel pricing ImageOutputPrice override works in token billing mode
      - Auto-fill image_output_price in channel pricing form from model defaults
      - Add "channel_mapped" billing model source as new default (migration 088)
      - Bills by model name after channel mapping, before account mapping
      - Fix channel cache error TTL sign error (115s → 5s)
      - Fix Update channel only invalidating new groups, not removed groups
      - Fix frontend model_mapping clearing sending undefined instead of {}
      - Credits balance precheck via shared AccountUsageService cache before injection
      - Skip credits injection for accounts with insufficient balance
      - Don't mark credits exhausted for "exhausted your capacity on this model" 429s
      d72ac926