makecindy / makecindy/cindy

bug: codex 链路使用 Anthropic 订阅时,账号额度余量不会刷新

Open
#2,626 3 comments 0 reactions 0 assignees View on GitHub
bug
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.