bug: codex 链路使用 Anthropic 订阅时,账号额度余量不会刷新
- Dominant language
- TypeScript
- Stars
- 2.7k
- Forks
- 395
- Avg merge
- 21h 48m
- Merged PRs (30d)
- 776
Description
### 问题描述 / What happened
**实际行为**:同一个 Anthropic 订阅账号、同一个模型,用 codex harness 发消息时额度确实被消耗,但 Cindy 里显示的余量停在上一次用 claude-code harness 时的数值,直到用户碰巧再用一次 claude-code 才更新。
**期望行为**:额度是账号级的,不应该因为用哪个 harness 而决定刷不刷新。
**源码侧的链路**(均为读代码所得,非推测;`main @ 1d3c652ef`):
1. `maker-host/claude-rate-limit-headers-observer.ts` 本身不区分 harness,它只按 hostname 全等 `api.anthropic.com` 过滤(`:60`);
2. 但它全仓只注册在 `maker-host/anthropic-compat-proxy-host.ts:377` 的 `composeResponseObservers` 里;`codex-proxy-host.ts:2412` 的 observer 组合是 `createCodexResponseObserver` / `createProviderUpstreamErrorObserver` / `createXaiProxyAuthInvalidationObserver`,**不含它**;
3. 更关键的是,codex 侧走 Anthropic(`wireProtocol === 'anthropic-messages'`,见 `codex-proxy-host.ts:1009`)时命中的是 `createAnthropicBridgeDecision` 的本地桥(localHandler 在 `:974`),而 **localHandler 在设计上就绕过 responseObserver** —— 这一点代码里已有明确记载:
> `// localHandler 绕过 proxy 的 responseObserver,自定义(user)供应商的上游错误不会被 createProviderUpstreamErrorObserver 看到。`
> —— `codex-proxy-host.ts:618`(`:2382` 另有一处同义说明)
对应实现在 `packages/anthropic-compat-proxy/src/server.ts:1717`:命中 localHandler 即 `runLocalHandler(...)` 之后直接 `return`,不进入 `responseObserver` 所在的转发层。
第 3 点的含义是:即使把同一个 observer 也注册到 codex proxy 上,它依然看不到这些响应 —— 挂点本身被绕过了。既有代码已经为「上游错误」这一类信息在桥路径上补过一次显式回喂(就是上面那段注释所描述的做法),额度余量是同一个洞里漏掉的另一类信息。
**推测(未抓包验证)**:桥路径在 `isAnthropicSubscriptionOAuth`(`codex-proxy-host.ts:874-877`,条件为 provider-oauth + `providerId === 'anthropic'` + `providerSource === 'builtin'`,正是本场景)成立时,经 `:885` 注入 Claude OAuth bearer,因此 Anthropic 的响应应当同样带 `anthropic-ratelimit-unified-*`,只是这条路径上没有读取的挂点。这一条我没有抓包验证,如果方向可行我可以补。
### 环境 / Environment
- Cindy 版本或 commit / version or commit: 正式版客户端 v0.1.46,源码对照 main @ 1d3c652ef
- 平台与版本 / platform & OS version: macOS
- 安装方式 / install method: 官方安装包
### 复现步骤 / Steps to reproduce
固定模型为 Claude 订阅模型(我用的是 Opus 4.8),订阅、账号都不变,只换 harness:
1. 先查一次当前快照,记下 `claude-code` 行的 `updated_at`:
```bash
D=~/Library/Application\ Support/CindyGlobal
T=$(mktemp -d)
for x in "" -wal -shm; do cp "$D"/cindy-*.db$x "$T"/ 2>/dev/null; done
sqlite3 "$T"/cindy-*.db "SELECT agent_kind, updated_at FROM account_usage_snapshots;"
```
2. 用 **codex harness** 发一条消息,等回合结束
3. 重新执行第 1 步的查询 → `claude-code` 行的 `updated_at` **没有变化**;但 `daily_model_usage` 里已经记上了这一轮
4. 改用 **claude-code harness** 再发一条消息
5. 再查一次 → 快照立刻刷新
### 日志与截图 / Logs & screenshots
2026-08-11 的对照实验(同模型、同订阅、同账号):
**第一次,codex harness。** `daily_model_usage` 正常记账:
```
2026-08-11 | codex | claude-opus-4-8#billing=subscription | 145434 | 93
```
而 `account_usage_snapshots` 的 `claude-code` 行 `updated_at` 一个字节都没变,仍停在 8/8。
**第二次,改用 claude-code harness。** 快照立刻刷新:
```
claude-code | updated_at = 1786435510126 (2026-08-11 20:05:10)
{
"fiveHour": {"utilization": 34, "resetsAt": 1786447200},
"sevenDay": {"utilization": 9, "resetsAt": 1786960800},
"source": "unified-headers",
"rateLimitStatus": "allowed",
"representativeClaim": "five_hour"
}
```
`sevenDay` 从 54% 变成 9%(旧窗口结束后重新计),`resetsAt` 推到 8/17 22:00。
---
### 补充:想请教的方向
能想到两种做法,代价差很多,所以想先问一下你们的规划:
- **A · 在桥路径上补一个响应头旁路**(形态类似现有的 `onUpstreamError`),解析后喂给同一个 `recordClaudeSubscriptionUsageSnapshot`。范围小、好评审,但如果以后还有别的 harness 接 Anthropic,同样的洞会再出现一次。
- **B · 把快照的维度从 `agent_kind` 换成账号 / 凭据**,解析上移到统一的 provider 层。这显然要动 schema 和不少调用方。
我注意到 observer 的 listener 签名里已经带了 `requestBearerToken`,测试也专门覆盖了「换号时数值相同也不去抖」,所以按账号归属这件事你们内部可能已经有既定方向,B 也可能本来就在排期里。
如果 A 可以接受,我可以先提一个只做 A 的 PR;如果你们更倾向 B 或已有计划,我也可以只把这些观测留在这里。
Contributor guide
Research direction
Start in codex-proxy-host.ts around createAnthropicBridgeDecision, its localHandler, and the anthropic-messages path, then compare the existing observer flow in anthropic-compat-proxy-host.ts and server.ts runLocalHandler. Trace how recordClaudeSubscriptionUsageSnapshot receives unified rate-limit headers. Done means codex requests using an Anthropic subscription refresh account_usage_snapshots without requiring a claude-code request, with coverage for switching harnesses.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100