Commit Graph
4736 Commits
Author SHA1 Message Date
shaw 571d1e1d9b fix(openai): isolate websocket semantic rate limits 2026-08-29 11:14:54 +08:00
shaw 3c22e78af0 fix(openai): preserve Spark quota reset semantics 2026-08-29 11:08:01 +08:00
shaw 5d9c7abed5 fix(openai): scope Spark quota 429 to model 2026-08-29 09:57:08 +08:00
Wesley Liddick ac18c588c8 Merge PR #6165
修复上游倍率探测导致账号列表整页刷新的问题
2026-08-29 09:12:05 +08:00
Wesley Liddick b83284071a Merge PR #5348
fix(subscription): 周/月窗口展示的重置时间与实际推进锚点对齐
2026-08-29 09:10:07 +08:00
Wesley Liddick fd13c72e95 Merge PR #6330
feat(zhipu): support team GLM Coding Plan usage query
2026-08-29 09:08:42 +08:00
Wesley Liddick 3c2e9a43b9 Merge PR #5860
fix(antigravity): preserve mixed built-in chat tools
2026-08-29 09:07:27 +08:00
Wesley Liddick ca319d09f2 Merge PR #6280
fix(update): strip hyphenated pre-release/custom suffix in version comparison
2026-08-29 09:06:10 +08:00
Wesley Liddick 46598dd491 Merge PR #6306
fix(openai): emit pre-output SSE keepalive on the /v1/responses passthrough path
2026-08-29 09:04:46 +08:00
Wesley Liddick 3045b3ade1 Merge PR #6316
fix(openai): honor Claude Code session id for messages sticky routing
2026-08-29 09:03:14 +08:00
Wesley Liddick 25dc5742ea Merge PR #6332
fix(monitor): 配额抓取在 singleflight 执行体内重查缓存,消除重复上游查询
2026-08-29 09:01:15 +08:00
Wesley Liddick 6506c0ea68 Merge PR #6343
fix(gateway): preserve Anthropic tool arguments
2026-08-29 09:00:56 +08:00
RayWangX 5688bcba91 fix(openai): honor Claude Code session for messages sticky routing 2026-08-29 08:31:15 +08:00
风起 88cb79d8b3 fix(grok): honor client prompt_cache_key over X-Grok-Conv-Id for cache identity
grok-build CLI recap-style side-calls (turn-summary / title-refresh) send
a fresh per-call X-Grok-Conv-Id label but set body prompt_cache_key to
the parent session id, so a direct xAI connection shares the main turn's
server-side prompt cache prefix.

sub2api's explicitGrokCacheSeed preferred X-Grok-Conv-Id over the body
key, which fragmented the cache identity on every side-call and forced a
full-price replay of the whole conversation (~300K+ tokens each, ~2.3%
cache hit rate observed over 44 side-calls / 4.9M input tokens in 7
days — about a third of all grok-4.6 input).

Priority is now: CC session > generic session headers > body
prompt_cache_key > X-Grok-Conv-Id > explicitKey > previous_response_id.
IDE header-stability tests updated; new regression test locks in the
side-call/parent key-sharing contract.
2026-08-28 21:56:22 +08:00
Ryan ChouandClaude Opus 5 32ad1dcdce fix(subscription): 周/月窗口展示的重置时间与实际推进锚点对齐
订阅首窗初始化在开通日零点时,automaticWindowStartAt 会把实际推进锚点修正为
StartsAt,但展示层(calculateProgress / WeeklyResetTime / MonthlyResetTime)
仍按窗口起点+周期计算,导致仪表盘显示的重置时间早于实际重置时间(差值 =
StartsAt 的时分秒)。用户在显示的重置时间过后请求仍会收到 429
WEEKLY_LIMIT_EXCEEDED。

抽出 windowResetAnchor 供推进与展示共用,保证两者恒一致。非 legacy 锚点
(手动重置或已推进过的窗口)保持权威,不做修正。

日窗口不在本 PR 范围内:#5380 已把日额度改为按日历日对齐(DailyResetTime =
窗口起点所在日的次日 0 点),与周/月的期限对齐滚动窗口语义不同,不共用本修正。
附一个守护用例钉死这条边界。

Fixes #5348

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 21:46:44 +08:00
wucm667 da10822d7d fix(gateway): preserve Anthropic tool arguments 2026-08-28 19:08:26 +08:00
wucm667 5345881b1d Merge remote-tracking branch 'upstream/main' into fix/issue-5843-antigravity-mixed-tools
# Conflicts:
#	backend/internal/repository/group_usage_rollup_trigger_integration_test.go
2026-08-28 17:36:43 +08:00
wucm667 c5ff640dff test(repository): pin rollup trigger session timezone 2026-08-28 17:34:42 +08:00
li e652f6e20a fix(monitor): 配额抓取在 singleflight 执行体内重查缓存,消除重复上游查询
ChannelMonitorQuotaFetcher.Fetch 顶部的 cachedSnapshot 与随后的 flight.DoChan
之间存在一个窗口:goroutine A 判定缓存未命中之后、真正入队之前,goroutine B 的
flight 可能已经跑完、写好缓存,并且它的 singleflight key 也已被摘掉。A 于是不会
并入 B 的那次飞行,而是另起一个新 flight,对同一账号再打一次上游——正是
singleflight 想要消除的重复查询。

多个监控可以关联同一账号,interval 最小 15s,国产配额服务自身无缓存,
这条路径的全部意义就是防止打爆上游配额端点(见文件头注释)。

修复:把 flight 执行体提为具名方法 fetchShared,开头重查一次缓存。
这次重查必然命中,所以合并是确定的而非尽力而为:storeSnapshot 发生在执行体
返回之前,singleflight 删 key 发生在返回之后,因此「能新起一次飞行」必然蕴含
「上一次的快照已经可见」。

该窗口也是主线 CI 上 TestQuotaFetcher_ConcurrentFetchesShareSingleFlight
间歇性失败(expected: 1, actual: 2)的成因,本地 -count=900 复现 3 次(约 0.33%),
带修复后 -count=1700 零失败。

新增两条确定性用例:既有并发用例只能以约 1/300 的概率撞上该窗口,故直接钉住
窗口本身——fetchShared 就是 flight 的执行体,直接调用它等价于「已越过顶层缓存
判定、拿到属于自己的那次飞行」这一状态。另一条守住重查不得退化成无条件短路:
缓存过期后必须照常回源。
2026-08-28 16:49:49 +08:00
wall-wxk c4e46c3be8 feat(zhipu): support team GLM Coding Plan usage query
Team GLM Coding Plan accounts fail the quota probe with
"当前用户不存在coding plan" because the personal-plan query path
does not carry team context.

- quota probe: when credentials contain zhipu_organization, switch the
  Zhipu quota endpoint to the team variant (?type=2 + bigmodel-organization
  / bigmodel-project headers); personal plans keep the existing path
- account create/edit modals: optional organization/project ID inputs for
  zhipu coding-plan accounts, persisted in credentials; edit modal
  backfills them and clears back to personal plan when emptied
- add a click-to-open tooltip tutorial (zh/en) explaining how to grab the
  IDs from the bigmodel.cn web console via the /api/biz/v1/organization
  network request, with a sample URL mapping org-/proj_ segments to fields
2026-08-28 16:36:30 +08:00
Wesley Liddick 7b693ae429 Merge pull request #6188 from JialinLiu-codedance/feat/usage-requested-reasoning-effort
feat(usage): 使用记录展示映射前的推理强度
2026-08-28 16:18:50 +08:00
Wesley Liddick d4754c211a Merge pull request #6291 from Whisper-stark/fix/grok-codex-responses
fix(grok): sanitize Codex Responses requests
2026-08-28 15:58:39 +08:00
Wesley Liddick c8a6e93f33 Merge pull request #6310 from fengshao1227/fix/nonstreaming-terminal-failure-failover-v2
fix(openai): 非流式路径按流式同一套判定处理 HTTP 200 终止失败事件
2026-08-28 15:57:39 +08:00
Wesley Liddick 446042e518 Merge pull request #6309 from fengshao1227/fix/easypay-resolve-relative-payurl-qrcode
fix(payment): EasyPay 补全 mapi.php 回传的相对 payurl/qrcode
2026-08-28 15:57:10 +08:00
Wesley Liddick 1e6926cb9c Merge pull request #6312 from fengshao1227/fix/openai-ws-client-close-not-account-failure
fix(openai): 入站 WS 的客户端正常关闭与断开不再计为账号故障
2026-08-28 15:56:34 +08:00
li c83dced4ba fix(openai): 入站 WS 的客户端正常关闭与断开不再计为账号故障
ingress WebSocket 收尾处只把 *service.OpenAIWSClientCloseError 且状态码为 1000
的情况认作正常关闭,其余全部落到 shouldReportOpenAIWSProxyAccountFailure ——
而它只排除 model-switch 与 session-preempted 两种。两种完全正常的结束因此被
误判为上游/账号故障:

- 客户端干净关闭:coder/websocket 直接回裸 coderws.CloseError{Code: 1000},
  ReadOpenAIWSClientMessage 把 conn.Read 的错误原样返回,没有任何一层把它包成
  *OpenAIWSClientCloseError,所以 errors.As 对该类型必然为假。
- 客户端中途断开:ReadOpenAIWSClientMessage 在 controlCtx.Done() 分支用
  StatusGoingAway(1001) 收尾并以 context.Canceled 为 cause,「只认 1000」匹配不到。

两者都会走到 ReportOpenAIAccountScheduleResult(success=false),进而喂给
ObserveOpenAIAPIKeyHealthFailure 与 scheduler.ReportResult,累积到阈值即把上游
账号熔断出调度池——客户端按一次 Ctrl-C 就在扣上游账号的健康分。

同文件的 summarizeWSCloseErrorForLog 一直用 coderws.CloseStatus 读关闭码,就在
该判断之后 12 行被调用,于是同一条 WARN 会打印
close_status=1000(StatusNormalClosure) 的同时把账号记为故障。

失败归因的政策仓库早有明文:同包 failover_loop.go 的 failoverClientGone 写着
「客户端断开后 failover 必须静默终止……并被误报成账号耗尽」。本次把 WS ingress
补齐到同一条规矩。

判定收窄:只豁免 1000 与 context.Canceled。1001 不整类放行——网关也会因自身原因
用 GoingAway 收场,客户端取消那一支已由 context.Canceled 覆盖;
context.DeadlineExceeded 同样不放行,空闲超时那条本就被包成 1000 走前一个判据,
其余 deadline 是真实停滞。

Fixes #6105
2026-08-28 13:17:43 +08:00
Wesley Liddick d674a04f2a Merge branch 'main' into feat/per-user-public-group-access 2026-08-28 13:08:41 +08:00
Wesley Liddick add86cc3bc Merge branch 'main' into fix/openai-reset-usage-sync 2026-08-28 12:57:48 +08:00
Wesley Liddick 59bb131df4 Merge branch 'main' into fix/anthropic 2026-08-28 12:47:15 +08:00
Wesley Liddick b588c0bc42 Merge branch 'main' into fix/ws-v2-passthrough-cyber 2026-08-28 12:36:22 +08:00
Wesley Liddick a0b313018a Merge branch 'main' into fix/openai-raw-stream-truncation 2026-08-28 12:26:01 +08:00
Wesley Liddick de084cdfc2 Merge branch 'main' into fix/grok46-xhigh 2026-08-28 12:15:53 +08:00
Wesley Liddick f7dca22ea5 Merge branch 'main' into fix/responses-synthesized-created-at 2026-08-28 12:05:38 +08:00
Wesley Liddick 002aaaa3d0 Merge branch 'main' into fix/anthropic-chat-bridge-reasoning-passback 2026-08-28 11:55:10 +08:00
Wesley Liddick 28fa458dc7 Merge branch 'main' into fix/issue-6240-preserve-multimodal-tool-output 2026-08-28 11:37:40 +08:00
li 81ac8ccd64 fix(openai): 非流式路径按流式同一套判定处理 HTTP 200 终止失败事件
stream=false 时上游仍可能回 SSE(其他 sub2api 实例、部分 OpenAI 兼容上游),
容量/限流错误经 HTTP 200 的 response.failed / error 终止事件回传。
handleSSEToJSON 与 handlePassthroughSSEToJSON 把所有终止事件塞进固定 502
(writeOpenAINonStreamingProtocolError),而几百行外的流式读取器对同一帧走
openAIStreamFailedEventShouldFailover / openAIStreamErrorEventShouldFailover
判定并返回 UpstreamFailoverError。

同一个上游、同一个事件,仅因请求上的 stream 标志而结果相反:流式换号,
非流式把上游原文直接抛给客户端,池里还有可调度账号也不换。

新增 nonStreamingTerminalFailureFailover,在两条非流式路径上复用流式的同一套
分类,并按 terminalType 分派——裸 error 帧走更保守的那个(仅正向识别为瞬时才
换号),response.failed 走完整的那个,与流式读取器逐字一致。

安全前提:响应体已被 ReadUpstreamResponseBody 完整缓冲,判定发生在写出任何
语义字节之前。能否真正换号仍由 handler 的 openAIForwardMayFailover 用
keepalive 调整后的写出量仲裁(#3887),service 侧只额外拒绝 IsResponseCommitted
与 account 为 nil 的场景,不重复实现心跳判定,避免与 handler 口径分叉。

行为变更:未被分类为不可重试的泛化 response.failed 在非流式路径上由「回写 502」
变为「换号」——这正是与流式对齐的结果。既有用例
TestHandleSSEToJSON_ResponseFailedReturnsProtocolError 传的是 nil account,
无账号则无可换对象,故其断言原样保留,仅更名为 ...WithoutAccountReturnsProtocolError
并注明原因;带真实账号的同一报文由新增用例钉死。

Fixes #5281
2026-08-28 11:36:08 +08:00
Wesley Liddick d24de611f8 Merge branch 'main' into fix/openai-reasoning-replay 2026-08-28 11:27:39 +08:00
Wesley Liddick ee2c8b97bb Merge branch 'main' into fix/apikey-namespace-roundtrip 2026-08-28 11:16:48 +08:00
li 1e8745c887 fix(payment): EasyPay 补全 mapi.php 回传的相对 payurl/qrcode
createAPIPayment 把上游 mapi.php 返回的 payurl/payurl2/qrcode 原样落库。
EasyPay 兼容上游对该字段并不统一:部分实现回站点根相对路径(如
`/api/pay/toapp/<order>`)而非完整地址。而链路上没有任何一层修复它——
sanitizeCreatePaymentResponseDetails 只剥 NUL 字节就写进 pay_url/qr_code,
前端 PaymentQRCodeView.renderQR 又把 qr_code 直接喂给 QRCode.toCanvas。
结果二维码的载荷是一段裸路径:微信扫出来是文本,pay_url 则落到网关自己的
域名下 404,用户无法进入上游收银台。

同文件的 createRedirectPayment 本就自行拼绝对地址(apiBase + "/submit.php?"),
只有 mapi.php 这一支透传;Alipay provider 也早已写明「Setting it as QRCode
would let the frontend render an unscannable image」并拒绝产出不可扫的值。
本提交让 EasyPay 与这两处对齐:按实例配置的 apiBase 解析上游回传的引用。

仅补全以 "/" 开头的引用。带 scheme 的一律原样返回,覆盖绝对 https:// 收银台
地址与 weixin:// / wxp:// / alipays:// 等 app 深链;无前导斜杠的引用同样不动,
因为二维码载荷常是不透明 token,把 "OrderToken123" 改写成
"<apiBase>/OrderToken123" 会毁掉一个当前可用的载荷,比本 bug 更糟。

payurl2 优先于 payurl 的选择语义保持不变。

Fixes #6292
2026-08-28 11:09:34 +08:00
Wesley Liddick 96e9ab8662 Merge branch 'main' into main 2026-08-28 11:06:29 +08:00
Wesley Liddick acd2f09ddd Merge branch 'main' into fix/openai-images-capability-loss-cooldown 2026-08-28 10:54:47 +08:00
HuntercodeT 60756c0ca4 fix(openai): emit pre-output SSE keepalive on the /v1/responses passthrough path
The passthrough streaming loop buffers leading events in `pendingLines` until
the first visible output, so a client receives **zero bytes** — not even the
HTTP response headers, since gin's ResponseWriter only commits them on the
first write — for as long as the upstream takes to produce its first visible
event.

The Forward path already recognises this exact hazard and solves it with a
downstream keepalive:

    // Track downstream writes separately from upstream reads: pre-output
    // failover can buffer response.created / response.in_progress, so
    // keepalive must be based on downstream idle time.

The passthrough path has the same buffering but never starts a keepalive.
Reasoning models routinely think for several minutes before emitting a visible
event, so intermediate proxies close the connection on idle timeout.

Production evidence from one deployment (12h window, gpt-5.6 family):

  * 44 `/v1/responses` requests produced their first visible output only after
    600–900 s, each carrying 30k–50k output tokens — the upstream had finished
    the work.
  * Every one of them was killed by an intermediate nginx `proxy_read_timeout`
    (600 s) and returned 504. The user received nothing.
  * Cutting early does not even save resources: the code deliberately keeps
    draining the upstream for usage after the client disconnects.

Fix: start the existing keepalive helper for the passthrough stream as well.

* `StartOpenAICompactSSEKeepalive` keeps its compact-marker gate; the gate-free
  body is extracted as `startOpenAISSEKeepalive` for callers that already know
  they are in an SSE context.
* The passthrough loop starts it when `gateway.stream_keepalive_interval > 0`
  and stops it immediately before its first real write, so the ResponseWriter
  is never shared.

Pre-output failover is unaffected: keepalive bytes are already excluded by
`OpenAICompactKeepaliveAdjustedWrittenSize`, which `openAIStreamClientOutputStarted`
on this very path already consults (#3887). No pendingLines and no
account-specific headers are ever emitted by a keepalive.

Behaviour is unchanged when `gateway.stream_keepalive_interval` is 0.
2026-08-28 10:19:43 +08:00
akihitohyhandClaude Opus 5 b56c61eccd feat(admin): let admins restrict which public groups a user may access
Public groups have always been bindable by every user: CanBindGroup returned
true for any non-exclusive group, and user_allowed_groups only ever carried the
exclusive groups an admin had granted. Admins had no way to hand a single user
a subset of the public groups short of converting a group to exclusive, which
changes it for everyone already using it.

A user now carries restrict_public_groups. While it is false, which is the
default and what every existing row migrates to, nothing changes: every public
group stays bindable. Once an admin turns it on for a user, that user's public
groups are narrowed to the ones listed in user_allowed_groups, the same table
that already gates exclusive groups.

The flag is an administrative control, so it rides on the admin user DTO only
and leaves the shape of the end-user endpoints alone.

The model plaza filter honours the flag as well, so a restricted user is not
shown groups they would be refused when binding a key. Anonymous visitors have
no user record and keep the previous view.

Enforcement rides on the existing CanBindGroup choke point, so it covers key
creation, key updates, and per-request authorization together. An API key bound
to a group that is later withdrawn stops working at request time rather than
lingering as a key that can be listed but not used.

The admin dialog gains a toggle over the public group list. Turning it off
re-checks every public group, so an admin cannot save a list that reads as
restrictive while the restriction itself is disabled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 02:40:17 +08:00
Qi HU f4e3eb1c5a fix(openai): detect cyber policy in ws v2 passthrough 2026-08-27 21:50:53 +08:00
Whisper-stark fd872550d5 fix(grok): handle typed invalid tool unions 2026-08-27 21:03:23 +08:00
Whisper-stark f4820c00dc fix(grok): simplify invalid tool union roots 2026-08-27 19:24:50 +08:00
Whisper-stark de6ef7134d fix(grok): sanitize Codex Responses requests 2026-08-27 19:24:50 +08:00
Ryan ChouandClaude Opus 5 e6ea7b9af0 fix(openai): 图像能力丢失时冷却 image 调度,不再反复选中坏号
OAuth 账号被上游收走 image_generation 能力后,/v1/images/* 稳定返回同一句 400
(Tool choice 'image_generation' not found in 'tools' parameter),但网关不做
任何处置,坏号持续被排程选中(报告者实测连续 17 次同 body 400、窗口内 0 成功、
持续 3.5 小时,直到人工停调度)。

RateLimitService 的 400 分支是白名单式,只认 organization has been disabled /
credit balance / identity verification is required 三句,其余 400 明确不处理;
唯一能兜任意 400 的 tryTempUnschedulable 又要求账号开启 temp-unschedulable
开关,OAuth 账号默认没开。两条路都不命中,shouldDisable 恒为 false,也没有任何
model 作用域冷却。

修复收窄到 sub2api 自建 body 的 images 管线:buildOpenAIImagesResponsesRequest
产出的 body 必定同时带 tool_choice 与匹配的 image_generation tool,这条路上出现
该 400 只可能是上游收了账号能力。forwardOpenAIImagesOAuth 在 upstreamCtx 上打
自建标记(覆盖其下游全部三个错误分支),共用错误入口读到标记才把该 400 判为能力
丢失,对 openai:image_generation 作用域冷却 30 分钟(对齐 upstreamModelNotFound
Cooldown),不禁用整颗账号——账号对文本请求依然健康。

未采用 issue 建议①的全局签名匹配:handleOpenAIAccountUpstreamError 同时服务
passthrough 路径,客户端可自带「tool_choice 指 image_generation 但 tools 不带」
的请求触发一字不差的同一句 400,全局匹配等于任何 API 用户一句话即可禁用健康
账号。附 5 个测试,其中防毒杀用例在撤掉该闸门后会实测失败。建议②(连续 N 次
400 熔断)涉及跨请求状态与配置项,另议。

Fixes #4466

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 16:36:01 +08:00
WUJI-Labs 9e7aff59df fix(update): strip hyphenated suffix from version string during comparison
When running a custom build or pre-release version (e.g. v0.1.183-custom),
parseVersion failed to parse the patch segment because of the non-numeric suffix,
causing compareVersions to incorrectly report an available update.

This commit strips any hyphenated suffix before splitting SemVer components.

Signed-off-by: WUJI-Labs <zoujincool@gmail.com>
2026-08-27 15:54:22 +08:00
kk 00efee4303 🐛 fix(grok): 为 Grok 4.6 广告 xhigh 推理档位
Grok Build 的 /v1/models 原先给所有可配 reasoning 的 Grok 模型写死三档,
导致 grok-4.6 无法在 /model 里选出官方支持的 xhigh。转发与 Codex catalog
已按模型判断,现与同一条件对齐。
2026-08-27 15:50:20 +08:00