Commit Graph
2779 Commits
Author SHA1 Message Date
ZheFox a519bcf705 Merge pull request #761 from zhefox/main
Fix response reasoning summaries for Chat
2026-08-29 00:03:16 +08:00
ZheFox 9461b1004f Merge branch 'main' of https://github.com/zhefox/Aether 2026-08-29 00:00:13 +08:00
ZheFox 3c15f523be Merge pull request #760 from zhefox/fix/gemini-tool-wire-model-gating
fix(formats): gate mixed Gemini tools by model
2026-08-28 23:36:08 +08:00
ZheFox 5bcdcca784 fix(formats): normalize Responses additional tools for Chat 2026-08-28 23:03:56 +08:00
ZheFox 83098f98b6 fix(formats): gate mixed Gemini tools by model 2026-08-28 20:42:49 +08:00
ZheFox 5ab35ae6ba Merge pull request #759 from zhefox/fix/gemini-tool-schema-compat
fix(formats): enable mixed Gemini tool calls
2026-08-28 17:58:03 +08:00
ZheFox f0b0064f3d fix(formats): enable mixed Gemini tool calls 2026-08-28 16:55:38 +08:00
ZheFox 4879295f23 Merge pull request #758 from zhefox/fix/gemini-tool-schema-compat
fix(formats): sanitize Gemini tool schemas
2026-08-28 16:15:24 +08:00
ZheFox 64e5725331 fix(formats): sanitize Gemini tool schemas 2026-08-28 16:11:16 +08:00
ZheFox 1995198b18 Merge pull request #757 from zhefox/fix/responses-chat-reasoning-summary
fix(formats): degrade Responses reasoning summaries for Chat
2026-08-28 14:09:57 +08:00
ZheFox 5b6fce1a77 fix(formats): degrade Responses reasoning summaries for Chat 2026-08-28 14:05:48 +08:00
ZheFox fa8e443f7b fix(formats): degrade Responses reasoning summaries for Chat 2026-08-28 14:03:43 +08:00
ZheFox 08e7530adb Merge pull request #756 from zhefox/main
fix: preserve Gemini signatures and enforce provider limits
2026-08-28 14:01:43 +08:00
ZheFox 5687dad177 fix(formats): scope signature helper to tests 2026-08-28 13:21:35 +08:00
ZheFox dd2958a458 fix(admin): persist provider settings and enforce quotas 2026-08-28 12:47:54 +08:00
ZheFox c4b4dfa996 fix(formats): preserve Gemini tool thought signatures 2026-08-28 12:47:34 +08:00
ZheFox d88c454a2c Merge pull request #754 from zhefox/main
fix(ai): align Gemini and Responses compatibility
2026-08-28 08:15:35 +08:00
ZheFox 8cdfa338e5 fix(gemini): pair idless tool history 2026-08-28 02:15:00 +08:00
ZheFox 4da8c57fe3 fix(ai): align Gemini and Responses compatibility 2026-08-27 22:13:20 +08:00
ZheFox 7892aa9485 Merge pull request #753 from zhefox/fix/codex-namespace-tool-conversion
fix(formats): preserve Responses namespace tools through Chat
2026-08-26 01:13:53 +08:00
ZheFox 9d9892be6a fix(gateway): finalize cross-format sync JSON responses 2026-08-26 00:43:58 +08:00
ZheFox e2b003af24 fix(formats): preserve Responses namespace tools through Chat 2026-08-26 00:43:50 +08:00
ZheFox ffca7e0402 Merge pull request #752 from zhefox/main
fix(codex): isolate Spark quotas and repair contaminated account state
2026-08-25 22:13:15 +08:00
ZheFox ec6ddb43a7 fix(data): retain applied legacy backfill for upgrades 2026-08-25 21:22:57 +08:00
ZheFox 2f2d444f97 fix(codex): self-heal Spark-contaminated account quotas
Keep model-scoped Spark windows out of account state, preserve authoritative WHAM exhaustion flags, and repair historical cross-family quota generations without weakening stale-response guards.
2026-08-25 19:13:41 +08:00
stabeyandClaude Opus 5 42deab67b3 fix(admin): keep a model-scoped 429 reset out of the account quota slot
`parse_codex_websocket_usage_limit_error` backfills `primary_reset_at` and
`primary_reset_after_seconds` from the error body whenever the embedded headers
did not supply them. Now that a named per-model limit no longer claims the
unprefixed window headers, that absence is exactly what a Spark 429 produces —
and `resets_at` on such an error is the Spark window's reset, so the backfill
put model-scoped timing back on the account's own quota.

Gate the backfill on the same ownership rule the window parsing uses.

Reported by Cursor Bugbot on the fork PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 15:22:29 +08:00
stabeyandClaude Opus 5 1b1be918a9 fix(admin): keep Codex model-scoped rate-limit headers out of the account quota
`parse_codex_usage_headers` reads the unprefixed `x-codex-primary/secondary-*`
headers as the account's own quota. They are not: they carry whichever limit
governed the request, and `x-codex-active-limit` names it — `premium` for the
plan's own limit, or a metered feature such as `codex_bengalfox` for a named
per-model limit. On a request billed against a named limit the unprefixed
headers repeat that limit's windows verbatim.

So a single request to a model with its own limit writes that model's windows
into the account slots. The paid-window swap then makes it worse: a named
limit's secondary window is active, unlike the plan's disabled one, so the swap
promotes the model's weekly window into the account's weekly slot — the slot
the UI labels and the scheduler reads through `quota_usage_ratio`.

It also sticks. Both weekly windows share `window_minutes`, so
`codex_quota_same_window_identity` treats them as one window, and
`codex_quota_merge_same_window` drops an observation whose deadline is earlier
than the stored one. The two weeks start at different instants, so every later
account observation looks like a stale sample of a window that already rolled
over and is discarded until the model window's own deadline passes.

Observed on a `pro` key running both model families: one `gpt-5.3-codex-spark`
request replaced the account weekly window with the Spark weekly one, and the
~3000 plan-limit responses over the next 100 minutes were all discarded. The
account's real weekly usage never landed, and its reset time was reported nine
hours late.

The header set describes itself — every named limit announces
`x-codex-<feature>-limit-name` and carries its windows under the same prefix —
so parse the named families directly and only claim the unprefixed windows for
the account when no announced limit owns them. Responses without
`x-codex-active-limit` keep the previous behaviour.

This also stops the Spark windows from going stale: they were only ever written
by the `wham/usage` admin probe even though every response carries them.

Refs #746

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 15:22:29 +08:00
ZheFox 3f2b67f191 Merge pull request #749 from zhefox/main
test(gateway): fix Codex Realtime route fixture
2026-08-23 12:48:10 +08:00
ZheFox 6a9eea34a0 test(gateway): fix Codex Realtime route fixture 2026-08-23 04:00:16 +08:00
ZheFox 453a0b3ee7 Merge pull request #747 from zhefox/main
fix(gateway): support current Codex Realtime live routes
2026-08-23 03:18:46 +08:00
ZheFox 2cd20da1ec fix(gateway): support current Codex Realtime live routes 2026-08-23 02:51:49 +08:00
ZheFox ea4453321d Merge pull request #743 from zhefox/main
fix(usage): unify OpenAI Live and WebSocket records
2026-08-21 12:47:37 +08:00
ZheFox acde38b8e7 fix(usage): unify OpenAI Live and WebSocket records 2026-08-21 11:53:53 +08:00
ZheFox 9996e75a34 fix(ci): align database snapshot migration cutoff 2026-08-21 11:11:45 +08:00
ZheFox 16f96d73ec Merge pull request #742 from zhefox/main
feat(gateway): add Codex Live and OpenAI Realtime
2026-08-21 08:36:19 +08:00
ZheFox 2c89202001 feat(gateway): add Codex Live and OpenAI Realtime
Implement preflighted Live/Realtime WebSocket transports, protocol-aware authentication, usage auditing, UI filtering, and legacy Codex permission migration.
2026-08-21 04:27:34 +08:00
ZheFox fe38dcd294 Merge pull request #741 from zhefox/main
feat(gateway): add Codex Live transport
2026-08-20 22:22:37 +08:00
ZheFox 4185ad1b1e feat(gateway): add Codex Live transport 2026-08-20 21:59:05 +08:00
ZheFox 6916e9da76 Merge pull request #739 from zhefox/main
fix(ws): secure Responses continuation and opaque reasoning replay
2026-08-20 10:01:46 +08:00
ZheFox 654f798d25 fix(ws): harden Responses continuation state 2026-08-20 08:51:16 +08:00
ZheFox bef282cfee fix(responses): replay DeepSeek opaque reasoning state 2026-08-20 00:40:48 +08:00
ZheFox d21d8ce9f5 fix(codex): avoid replaying static config on websocket continuations 2026-08-20 00:40:40 +08:00
ZheFox 342f8b6a5f Merge pull request #737 from zhefox/main
fix(ws): preserve Codex continuations across turn metadata
2026-08-18 18:20:49 +08:00
ZheFox c50a1c6c46 fix(ws): preserve Codex continuation bindings 2026-08-18 17:17:45 +08:00
elky 535ee098c3 fix(auth): reject unsigned admin identity headers v0.7.13 2026-08-18 11:12:17 +08:00
ZheFox b45df89ce4 Merge pull request #730 from zhefox/fix/responses-websocket-current
feat(gateway): add OpenAI Responses WebSocket mode
2026-08-17 21:14:50 +08:00
ZheFox c8118edf36 fix(ws): harden Responses connection lifecycle
Revalidate control policy per turn, isolate downstream credentials, and make planner/turn ownership cancellation-safe.

Preserve opaque protocol events, align configurable timeout semantics, and extend end-to-end security and settlement coverage.
2026-08-17 18:50:29 +08:00
AAEE86 4a0775c4ea style: apply cargo fmt across gateway and aether-ai crates 2026-08-17 14:53:53 +08:00
AAEE86 6fc02dad3e fix(ws): restore redacted PII in provider frames before client delivery
Responses WebSocket 只实现了脱敏的一半:请求侧 mask 之后,provider 事件帧在推给
客户端之前没有还原,于是 session 映射内的占位符以 <AETHER:EMAIL:...> 的形式直接
透给客户端。这里补齐响应侧,语义与 HTTP 路径对齐。

- 还原点是 relay loop 的最后一跳(send_client_message 之前、capture_client_frame
  之前),对应 HTTP 的 restore_sync_response_body / StreamingResponseRestorer 所在
  位置。审计与终态观测继续消费脱敏态事件,只有发往客户端的那一份拷贝被还原。
- 复用 privacy::restore_json_strings(改为 pub(crate))与
  RedactionSession::restore_text,不复制任何还原逻辑:只还原本 session mask 过的
  映射,未映射的占位符原样保留;type / model / id 等协议字段不可能命中 sentinel,
  因此不受影响。批量 {"chunks":[...]} 帧一并递归还原。
- session 生命周期:mask 仍然是 per-turn(slot 依旧每轮新建),但 session 改由连接
  持有,按有界 FIFO 留最近 8 轮。理由是 WS 的会话历史留在上游,continuation 只发
  增量输入,per-turn 释放会漏还原后续响应里回显的更早轮次占位符;HTTP 不会漏,是
  因为它每次重发整段历史、重新 mask 会派生出同一个 sentinel。被挤出窗口的轮次退回
  「占位符原样透传」,不会错误还原成别的值。
- 未命中还原时不改写字节;连接上没有任何 mask session 时(未启用脱敏)连事件 clone
  都不做。

测试:redaction.rs 新增 8 条单测(还原命中/批量帧/未映射占位符原样/未命中不改写/
无 session 不介入/空 session 不留存/审计侧入参不被改写/跨轮还原/窗口有界);
responses_websocket_e2e 新增一条用例,mock 上游回显收到的 input,断言上游只看到
占位符而客户端拿到真实邮箱。
2026-08-17 14:53:46 +08:00
AAEE86 dbf2809bd6 fix(ws): settle the previous attempt before transparent retry replanning
评审第 2 条。配额透明重试原来的顺序是「detach 旧 attempt → 规划并绑定新
attempt → 把旧 attempt 的结算排进队列」。规划因此读到的是旧 attempt 还没投射的
health / adaptive / pool 状态,而且旧 attempt 仍占着自己的 pool key lease——替代
key 的挑选看到的是一把仍被占用的 key,最坏情况下判成「无可用供应商」而放弃一次
本可以成功的重试。

普通的新 turn 早就挡住了这件事:client.rs 在处理 response.create 前调用
await_pending_turn_finalization,注释写的正是「不要让新 turn 基于陈旧的 health /
adaptive / pool 状态规划」。透明重试是同一个问题的另一条入口,漏了这一步。

现在顺序是:detach → 释放准入 → 结算旧 attempt 并等它落地 → 规划/绑定新 attempt。

新增 lifecycle::settle_turn_finalization:与 queue_turn_finalization 的区别只在于
「等」。后者把 handle 挂在连接上让 relay loop 继续跑,用在结算之后不再读取共享
状态的出口;前者用在必须先看到结算结果才能继续的路径上。

顺序用类型固定,而不是靠注释:settle_turn_finalization 返回
PreviousAttemptSettled,retry_active_turn_after_quota_exhaustion 要求这个参数。
凭证只能由 lifecycle 颁发(结算完成,或明确「没有 attempt 要结算」),所以把顺序
写反连编译都过不了。

重试失败路径随之变化:旧 attempt 已经结算,不再 resume 回去。logical turn 仍停在
Replanning,后续分支的 end() / finalize_active_turn 只清 logical turn、不交出
attempt,因此不存在重复结算。结算 outcome 取值不变(两条路径用的都是
terminal_outcome.unwrap_or_else(upstream_closed),而这条分支里 terminal_outcome
必为 Some——usage_limit_error 成立意味着有一个已解析的 error 终态帧)。

代价(都落在「重试失败」这一侧,且只影响已终态 attempt 的报告注解,不影响计费):
- 那条最终转发给客户端的 429 事件不再进旧 attempt 的 client capture;
  provider 侧 capture 早在 observe_upstream_frame 里就记下了。
- 如果转发 429 给客户端也失败,record_client_delivery_aborted 落在一个已经结算的
  attempt 上,成为 no-op。

测试:
- lifecycle:await_turn_finalization_handle 必须「等到落地」而不是「排进队列」
  (C6 依赖的性质);结算完成后规划才读状态的顺序型断言(计数器替身);结算任务
  panic 也必须放行调用方,不能卡死 relay loop。
- turn_state:Replanning 状态下 end() 不再交出第二个 attempt(无重复结算)。
- e2e 新增 provider_quota_exhaustion_transparently_retries_onto_another_key:
  mock 上游首轮只回 Codex 的 429 usage_limit_reached,网关换到第二把 key 重放同一个
  response.create;断言客户端看不到 429、上游被连两次、两次用的不是同一把 key、两个
  attempt 各留一条终态行(429 的那条 + 计费的那条)。已验证它在改动前后都通过——
  它覆盖的是整条路径可用,顺序由上面的单测确定性覆盖。
  夹具随之参数化出 ProviderFixture::CodexKeyPair:透明重试只有 Codex adapter 会
  开启,而 codex 候选要求 auth_type = oauth,所以这个夹具用未过期的 oauth 凭证。
2026-08-17 14:53:40 +08:00