Commit Graph
3918 Commits
Author SHA1 Message Date
Wesley Liddick f204216918 Merge pull request #3243 from alfadb/feature/chinese-llm-fallback-pricing
feat(billing): 国产 LLM 兜底定价 (GLM / Kimi / MiniMax) + 收编 DeepSeek V4
2026-06-16 20:28:49 +08:00
Wesley Liddick 03ec90a25a Merge pull request #3223 from feitianbubu/fix/intercept-streaming-haiku-probe
fix: 修复CC Switch改为流式测试后请求失败的问题
2026-06-16 20:27:33 +08:00
shaw 6c7203d83b fix(gateway): preserve SSE event:error body so ops logs reflect real upstream errors
When an Anthropic upstream returned HTTP 200 but then emitted an SSE
`event: error` frame (overloaded_error / rate_limit_error / api_error /
etc.), Forward's stream branch matched on `err.Error() == "have error in
stream"` and returned `UpstreamFailoverError{StatusCode: 403}` with no
ResponseBody. That dropped three pieces of evidence:

- handleFailoverExhausted → ExtractUpstreamErrorMessage(nil) = "" →
  ops_error_logs.upstream_error_message was empty.
- errorPassthroughService.MatchRule(_, 403, nil) could only match rules
  without keywords, so keyword-based passthrough rules silently never
  fired.
- upstream_errors carried no stream_error record, leaving ops looking at
  a generic 403 with no clue whether the upstream was throttled,
  overloaded, or rejecting the request. ping-during-slot-wait amplified
  this by skipping failover (writerSizeBeforeForward guard), so 403s
  ballooned in the ops view well past the upstream's actual rate.

Fix:
- Introduce *sseStreamErrorEventError that carries the SSE data line.
  Error() still returns "have error in stream" so existing log searches
  keep working.
- Forward extracts via errors.As, appends an OpsUpstreamErrorEvent
  (kind="stream_error", with the sanitized message and a truncated raw
  body honoring LogUpstreamErrorBody*), and returns
  UpstreamFailoverError{StatusCode: 403, ResponseBody: rawJSON}.

StatusCode 403 is preserved verbatim: mapUpstreamError, failover
decisions (shouldFailoverUpstreamError(403)=true), client-visible message,
RetryableOnSameAccount, and rateLimitService side-effects (this path
already didn't invoke them) all match prior behavior. OAuth and API Key
accounts share this path; the API-Key passthrough branch is independent
and already forwards SSE error frames untouched, so it's unaffected.

Adds four unit tests: typed-error contract + RawData, empty data line,
event:error after partial stream output (streamStarted=true), and
non-JSON data line.
2026-06-16 20:25:09 +08:00
alfadb 262fe1230d feat(billing): 为 doubao-embedding-vision 添加图文差别兜底定价
火山方舟 doubao-embedding-vision 多模态向量化按量付费官方价为文本 ¥0.7/MTok、图片 ¥1.8/MTok,二者不同价。原计费引擎对 embedding 仅记单一 input token、无图片输入档位,无法表达该差别。

变更:
- ModelPricing 新增 ImageInputPricePerToken 字段
- OpenAIUsage / UsageTokens 新增 ImageInputTokens 字段
- extractOpenAIEmbeddingsUsage 解析 usage.prompt_tokens_details.image_tokens
- CalculateCost 拆分文本/图片输入计费;ImageInputTokens 为 0 时走原单价路径,存量 chat/vision 流量行为不变
- getFallbackPricing 新增 doubao-embedding-vision 分支(most-specific-first,覆盖带版本后缀别名),fallback 表填入 $0.098/$0.252 per MTok(汇率 ÷7.14)

测试:新增图文混合/纯文本/图片 token 超额三类用例及定价回退断言;service 包单测与 golangci-lint 全通过。
2026-06-16 19:37:02 +08:00
alfadb 4f5f2788e1 fix(billing): add kimi-for-coding fallback pricing 2026-06-16 19:37:02 +08:00
alfadb c90089c814 fix(billing): address Copilot review feedback
应 PR #3243 review 修正三项问题:

1. 删除 DeepSeek V4 fallback 定价的重复块(cherry-pick 时
   的旧位置 + 新国产 LLM 分段都写了一遍,后写覆盖前写,
   当前值相同所以无 bug,但冗余,未来改一处易漏另一处)。
2. 删除 minimax-m3 匹配中的死代码——modelLower 已经
   strings.ToLower(),'MiniMax-M3' 字面量永远不可能匹中。
3. 测试结构改 *float64 替代 0 sentinel,让 free-tier
   模型(GLM-4.5-Flash / GLM-4.7-Flash)的 0 价能被
   真正断言而非默默跳过;定义内联 floatPtr 辅助函数。
2026-06-16 19:37:02 +08:00
alfadb a4ce73391b feat(billing): add GLM / Kimi / MiniMax fallback pricing for Chinese LLM providers
将 fix/deepseek-fallback-pricing 的 DeepSeek V4 Pro/Flash 定价收编,
并扩展国产 LLM 兜底定价覆盖:

- 智谱 GLM (z.ai 公开 SKU 13 个): glm-5.1 / glm-5 / glm-5-turbo / glm-4.7 /
  glm-4.7-flashx / glm-4.6 / glm-4.5 / glm-4.5-x / glm-4.5-air / glm-4.5-airx /
  glm-4-32b-0414-128k / glm-4.5-flash / glm-4.7-flash
- 月之暗面 Kimi K 系列 4 个: kimi-k2.6 / kimi-k2.5 / kimi-k2-thinking /
  kimi-k2 (K2-0905/K2-0711 官方未保留定价,隐性回退到 kimi-k2)
- MiniMax M 系列 6 个: minimax-m3 (≤512K) / minimax-m2.7 / minimax-m2.7-highspeed /
  minimax-m2.5 / minimax-m2.1 / minimax-m2
- DeepSeek V4 沿用原 fix/deepseek-fallback-pricing 的 entry 移植过来

所有定价数据来自各家官方定价页 (USD/MTok 口径),与现有
Claude/GPT 风格保持一致 (硬编码在 Go 代码里,admin channel 优先)。

匹配策略:长 key 优先 (glm-5.1 优先于 glm-5, k2.6 优先于 k2);
未列出的国产厂商 alias (qwen/doubao/hunyuan) 一律不返回兜底价,
避免误计价——这与 fix/deepseek-fallback-pricing 的白名单语义一致。

数据源注释 (Source URL) 内嵌在每个 initFallbackPricing 分组开头,
便于后续 PR 同步上游价格变化。
2026-06-16 19:37:02 +08:00
alfadb f597d98bdf test(openai): use unpriced model in usage test 2026-06-16 19:37:02 +08:00
alfadb 5a593a511e test(billing): tighten DeepSeek V4 fallback assertions; clarify branch comments
Address copilot-pull-request-reviewer feedback on #2157:

- billing_service_test.go: extend TestGetFallbackPricing_FamilyMatching
  with optional expectedOutput / expectedCacheRead fields and assert
  full Input/Output/CacheRead pricing for all 4 DeepSeek cases
  (v4-pro, v4-flash, deepseek-chat→flash, deepseek-reasoner→flash),
  preventing silent regression to 0 output/cache cost.
- billing_service.go: rewrite the DeepSeek block comment to explicitly
  describe its scope (V4 Pro/Flash + chat/reasoner aliases, no
  unknown-deepseek fallback) and tighten the OpenAI comment to make
  it unambiguous that it only describes the OpenAI/Codex branch
  immediately below it.
2026-06-16 19:37:02 +08:00
alfadb 27e26a3a90 chore: fix gofmt alignment 2026-06-16 19:37:02 +08:00
alfadb c906bf000e feat(billing): add DeepSeek V4 Pro / Flash fallback pricing
Pricing source: https://api-docs.deepseek.com/quick_start/pricing

deepseek-v4-pro:  input $0.435/M, output $0.87/M, cache_read $0.003625/M
deepseek-v4-flash: input $0.14/M,  output $0.28/M, cache_read $0.0028/M

Also map legacy aliases deepseek-chat / deepseek-reasoner → v4-flash.
2026-06-16 19:37:02 +08:00
shaw b8a482e127 fix(ci): unblock main after recent merges
Three independent CI blockers landed on main from concurrent PR merges:

- openai_quota_service.go (introduced by b8169492): const block spacing
  not gofmt-compliant + trailing blank line. golangci-lint v2.9 flagged it
  on every push after the merge.
- openai_images_failover_test.go (introduced by PR #3155, da30c599):
  NewOpenAIGatewayHandler call missing the opsService argument added by
  PR #3230 (b62b573f). Test was authored before #3230 and merged without
  rebase, causing "not enough arguments" compile error.
- account_quota_reset_test.go: TestIsFixedDailyPeriodExpired_NotExpired
  and TestIsFixedWeeklyPeriodExpired_NotExpired used time.Now()-1min as
  periodStart, which crosses the 09:00 UTC reset boundary when CI runs in
  the 09:00:00-09:00:59 window. Anchoring periodStart to today's 12:00
  UTC removes the race.
2026-06-16 17:59:06 +08:00
Wesley Liddick 44f5791008 Merge pull request #3154 from wucm667/fix/responses-session-hash-input-field
fix(openai): Responses API session hash fallback 纳入 input 内容,避免 sticky 绑死账号
2026-06-16 17:01:16 +08:00
Wesley Liddick 9c2c8ab3e5 Merge pull request #3155 from wucm667/fix/openai-images-server-error-failover
fix(openai): 图像接口上游 server_error 触发 failover 换号,不再 5xx 透传
2026-06-16 17:00:40 +08:00
Wesley Liddick e4ccb75d0f Merge pull request #3220 from wucm667/fix/oauth-signup-apply-promo-code
fix(auth): OAuth 注册支持应用 URL 上的 promo_code 优惠码
2026-06-16 16:59:38 +08:00
Wesley Liddick 2ce8788929 Merge pull request #3299 from alfadb/fix/openai-responses-probe-tool-capability
fix(openai-probe): /responses 能力探测增加工具调用校验
2026-06-16 16:58:48 +08:00
Wesley Liddick 2e0ff1cfd5 Merge pull request #3258 from bwliangc/feat/channel-monitor-jitter
feat(渠道监控): 检测间隔支持正负随机抖动配置
2026-06-16 16:58:23 +08:00
Wesley Liddick 16765bde69 Merge pull request #3230 from DaydreamCoding/feat/openai-cyber-policy-passthrough
feat(openai): cyber_policy 硬阻断全链路透传、审计与计费
2026-06-16 16:55:51 +08:00
shaw b816949291 feat(openai-quota): query + reset rate-limit credits for OpenAI accounts
Adds an admin-side action that mirrors the Codex Desktop "rate-limit reset"
flow against chatgpt.com upstream for OpenAI OAuth accounts.

Backend
- OpenAIQuotaService.QueryUsage / ResetCredit hit /wham/usage and
  /wham/rate-limit-reset-credits/consume with the Codex Desktop header set,
  reusing OpenAITokenProvider for refreshed tokens and PrivacyClientFactory
  for the impersonated Chrome TLS fingerprint.
- Honors the account's configured proxy by reading the eager-loaded
  account.Proxy directly (falls back to proxyRepo only when missing).
- GET /api/v1/admin/openai/accounts/:id/quota
  POST /api/v1/admin/openai/accounts/:id/reset-quota
- Wire DI for the new service + handler dependency.

Frontend
- OpenAIQuotaResetCell renders a single action row in AccountUsageCell's
  OpenAI section: the existing local "查询" (active sampling) is injected
  via #pre-actions, alongside a "次数 N" button that doubles as the
  upstream query trigger and the available-credit indicator, and a "重置"
  button that consumes one credit.
- No duplicate 5h/7d window display; the local UsageProgressBar owns those
  bars to avoid confusion.
2026-06-16 16:55:07 +08:00
alfadb b88f8e4c04 fix(openai-probe): /responses 能力探测增加工具调用校验
原探测仅以 HTTP 状态码判定 /v1/responses 端点是否存在(404/405 视为不存在,其余视为存在),无法识别"端点存在、基础补全可用、但工具调用不可用"的上游。火山方舟 coding/v3 的 kimi-k2.6 即属此类:携带 tools 的请求仅返回 reasoning、不产出 function_call,导致账号被误判 openai_responses_supported=true。网关据此走 /responses 转换路径后工具调用结果丢失,带工具的请求确定性失败。

改动:
- 探测请求携带一个工具并以 tool_choice=required 强制调用,仅当响应 output 数组包含 function_call 项时判定为支持;2xx 但无 function_call 判定为不支持,使网关改走 /v1/chat/completions 直转路径(同一模型在该路径下工具调用正常)。
- 探测模型改用账号 model_mapping 中的真实上游模型;占位模型在第三方上游会返回 400 model-not-found,无法判定能力。
- 探测超时 8s 调整为 15s(探测在后台异步执行,为推理型模型先推理再产出工具调用预留时间)。
- 非 2xx(404/405 除外)仍保守判定为支持,不改变既有账号行为;运行时回退函数 isResponsesEndpointSupportedByStatus 保持不变。
- 新增单元测试覆盖判定逻辑与探测模型选择。
2026-06-16 15:59:00 +08:00
shaw f069c9ae00 fix(outbox-dedup): buildSchedulerGroupPayload typed-nil broke dedup_key consistency
#3255 introduced payload-aware dedup_key (sha256 over event_type, account_id,
group_id, payload_json). enqueueSchedulerOutbox checks "if payload != nil"
before json.Marshal — but Go interface containing a typed-nil map (returned
by buildSchedulerGroupPayload(empty)) is NOT == nil at the interface level.

So an ungrouped account's account_changed event went through this path:
  payload := buildSchedulerGroupPayload(account.GroupIDs)  // typed-nil map
  enqueueSchedulerOutbox(..., payload)                     // interface != nil
    → json.Marshal(typedNilMap) = "null"
    → dedup_key hash = sha256(... + "null")

While other call sites pass literal nil:
  enqueueSchedulerOutbox(..., nil)                         // interface == nil
    → payloadJSON stays empty
    → dedup_key hash = sha256(... + "")

The two dedup_keys differ for what should be the same logical event,
silently degrading dedup effectiveness in bursts on ungrouped accounts.

Fix: change buildSchedulerGroupPayload return type from map[string]any to
any so empty input returns true untyped-nil. All call sites pass the
result straight to enqueueSchedulerOutbox(payload any) — no inspection,
no breakage.

Adds regression test TestEnqueueSchedulerOutbox_UngroupedAccountDedupesWithLiteralNilPayload
asserting (1) typed-nil regression doesn't sneak back, (2) dedup_key for
empty-groups payload matches the literal-nil-payload key.
2026-06-16 14:21:09 +08:00
shaw acaffe29ec fix(account-repo): refresh candidates SQL excluded healthy accounts; fix CI build
Post-merge audit of #3272 found two regressions:

1. ListOAuthRefreshCandidates used "AND NOT (a AND b)" which, under PG
   3-valued logic, evaluates to NULL when both temp_unschedulable_until
   and temp_unschedulable_reason are NULL — i.e., the common healthy
   account state. Such rows were silently excluded from the background
   token refresh worker, so their OAuth access tokens would never get
   refreshed and eventually start returning 401.

   Verified empirically against PostgreSQL: only 3 of 5 test rows
   matched before the fix; after switching to "(a AND b) IS NOT TRUE"
   the expected 4 rows match.

2. The new ListOAuthRefreshCandidates method on AccountRepository was
   not implemented on stubAccountRepo in api_contract_test.go (build
   tag "unit"), breaking "make test-unit" which CI runs in
   .github/workflows/backend-ci.yml.

Tests:
- Added IS NOT TRUE and "AND NOT (" assertions to the SQL-shape unit
  test so the predicate can't regress to the broken form again.
- "go test -tags=unit ./internal/..." now passes cleanly.
2026-06-16 14:08:50 +08:00
shaw 2ba52bf4aa Merge pull request #3274 from jianjianai/fix/scheduler-outbox-cleanup
fix: cleanup consumed scheduler outbox rows / 添加 scheduler_outbox 表的清理代码,避免表过大

Additional hardening: WHERE clause adds a 10s grace via
created_at < NOW() - INTERVAL '10 seconds' to defend against the PG
sequence-id vs commit race (id assigned in tx, commit delayed past
watermark advance). Without it, slow committers could lose their
outbox row before the snapshot poller reads it.
2026-06-16 11:57:43 +08:00
shaw 31dc8913ac chore(outbox-cleanup): add 10s grace to defend against id-vs-commit race
PG sequences advance outside transactions, so a slow committer can hold
an id that gets surpassed by the watermark before its commit becomes
visible. Without the grace period, cleanup would delete such rows before
the snapshot poller ever sees them. 10s is a comfortable upper bound
on realistic enqueueSchedulerOutbox commit latency.
2026-06-16 11:57:32 +08:00
jjaw cb14935e9a fix: cleanup consumed scheduler outbox rows 2026-06-16 11:56:40 +08:00
shaw 4254dcb352 Merge pull request #3255 from jianjianai/fix1/outbox-scheduler-snapshot-coalesce
降低高频账号状态变更下的 outbox 写入压力 / safely coalesce scheduler outbox events

Migrations renumbered 151/152 -> 152/153 to avoid collision with #3232.
2026-06-16 11:50:35 +08:00
shaw 1fdbe52f96 chore(migrations): renumber scheduler outbox dedup migrations 151/152 -> 152/153
#3232 (already merged) occupies 151. Renumber #3255's dedup migrations to
avoid collision and keep linear ordering. Updated runner constant + tests.
2026-06-16 11:50:25 +08:00
jjaw b3ec6288ad fix: release scheduler outbox dedup on claim 2026-06-16 11:48:13 +08:00
jjaw 60cf89ae26 fix: recover scheduler outbox invalid dedup index 2026-06-16 11:48:13 +08:00
jjaw 3ef70b045d fix: safely coalesce scheduler outbox events 2026-06-16 11:48:12 +08:00
jjaw 34e66ec0a5 fix: outbox scheduler snapshot coalesce 2026-06-16 11:48:12 +08:00
shaw 45f3b0dd74 Merge pull request #3272 from jianjianai/fix/token-refresh-retry-backoff
fix: token refresh 筛选避免拉取全部账号导致数据库爆炸 / reduce token refresh retry amplification
2026-06-16 11:47:07 +08:00
jjaw 9b270f11d7 refactor: inline token refresh retry reason prefix 2026-06-16 11:44:39 +08:00
jjaw 74199b6a6e fix: reduce token refresh retry amplification 2026-06-16 11:44:39 +08:00
shaw b56c501d30 Merge pull request #3273 from jianjianai/fix/redis-wait-queue-hotpath
fix: move user wait queue accounting off hot path / 用户槽位立即成功的请求不调用等待队列
2026-06-16 11:43:07 +08:00
jjaw b0579c4891 fix: move user wait queue accounting off hot path 2026-06-16 11:41:54 +08:00
shaw 87d0f8da66 Merge pull request #3132 from jianjianai/main
修复 token refresh 拉取大量账号时触发 PostgreSQL 参数上限
2026-06-16 11:40:47 +08:00
jjaw 8b698ff4c1 fix account list parameter limit 2026-06-16 11:39:15 +08:00
Wesley Liddick 62fef6ebed Merge pull request #3178 from jianjianai/fix/fix-failover-error-body-reuse
fix: 修复 OpenAI failover 错误响应体重复读取 / reuse cached OpenAI failover error body
2026-06-16 11:13:10 +08:00
Wesley Liddick 8ddc138094 Merge pull request #3232 from jianjianai/fix1/account-expiry-autopause-index
fix:account expiry autopause index / 账号过期自动禁用索引优化
2026-06-16 11:12:57 +08:00
Wesley Liddick 2f1f1971b4 Merge pull request #3195 from jianjianai/fix/invalid-refresh-token-nonretryable
fix: treat invalid_refresh_token as non-retryable / 将 invalid_refresh_token 判定为不可重试
2026-06-16 11:12:45 +08:00
Wesley Liddick 9acd104151 Merge pull request #3201 from alfadb/fix/dockerfile-copy-docs-legal
fix(docker): 将 docs/legal 纳入构建上下文以支持 admin-compliance gate
2026-06-16 11:12:34 +08:00
Wesley Liddick 15b0753c1d Merge pull request #3283 from wucm667/fix/non-json-200-response-failover
fix(gateway): 上游 200 返回非 JSON 错误报文时纳入 failover,避免坏上游被反复命中误扣费
2026-06-16 11:12:21 +08:00
Wesley Liddick a0fc2c4143 Merge pull request #3218 from wucm667/fix/antigravity-system-role-message
fix(antigravity): 处理 messages 中 role:system 消息,修复 Claude Code 接入 400
2026-06-16 11:12:08 +08:00
Wesley Liddick 28bed8b866 Merge pull request #3252 from wucm667/fix/upstream-zstd-response-decompression
fix(gateway): 解压 zstd 上游响应体,修复非流式 usage 静默记 0 的计费丢失
2026-06-16 11:11:51 +08:00
shaw bbd9702496 fix(frontend): bump form-data to >=4.0.6 via pnpm override
Resolves GHSA-hmw2-7cc7-3qxx (CRLF injection) flagged by
frontend-security CI. axios pulls form-data ^4.0.5 which locked to the
vulnerable 4.0.5; override forces all transitive consumers to 4.0.6+
without needing an audit exception.
2026-06-16 10:48:12 +08:00
Wesley Liddick 715594847b Merge pull request #3267 from codeQuest-fly/codex/fix-sub2api-injection
feat: configure Claude OAuth system prompt blocks
2026-06-16 10:43:38 +08:00
Wesley Liddick 29978ac9a6 Merge pull request #3251 from wucm667/fix/chatcompletions-responses-tool-strict-default
fix(apicompat): chat-completions 转 Responses 显式补全 tool strict=false,避免可选参数被填零值
2026-06-16 10:26:39 +08:00
dailingfei b63b411654 fix: remove unused billing attribution helper 2026-06-15 17:16:26 +08:00
wucm667 ab9987b2e2 fix(gateway): fail over on non-JSON 2xx responses 2026-06-15 11:04:24 +08:00