网页显示还有额度,opencode 却报限额:一个 provider 挂两个账号 key 的坑
十月 14, 2026 [AI] #opencode #agent #调试现象
在终端里让 opencode 回一句话:
$ opencode run "reply with exactly: OK"
> build · glm-5.3
> build · glm-5.3
……(连续重试 11 次)
Error: 已达到 5 小时的使用上限。您的限额将在 2026-10-14 16:17:09 重置。
但登录智谱后台看用量,5 小时额度只用了 35%,还剩 65%。同一把 key 直接打模型接口,偶尔还能通:
$ curl -sS -X POST https://open.bigmodel.cn/api/coding/paas/v4/chat/completions \
-H "Authorization: Bearer $KEY" \
-d '{"model":"glm-5.3","messages":[{"role":"user","content":"hi"}],"max_tokens":8}'
{"error":{"code":"1302","message":"您的账户已达到速率限制,请您控制请求频率"}} ← 多数时候
{"choices":[{"message":{"content":"..."}}],...} ← 偶尔成功
矛盾点有两个:
- 有额度却报限额;
- 报的是「5 小时限额」(1308),而我手打接口拿到的是「速率限制」(1302)——两种完全不同的错误。
第一轮:查额度,结果对不上
先找 opencode 到底读哪把 key。常规位置是全局配置:
$ cat ~/.config/opencode/opencode.json
{
"model": "zhipuai-coding-plan/glm-5.3",
"small_model": "zhipuai-coding-plan/glm-5.3-flash",
"provider": {
"zhipuai-coding-plan": {
"options": { "apiKey": "7583f11a……" }
}
}
}
~/.local/share/opencode/auth.json 里同名 provider 也是这把 7583f11a……。看起来毫无疑问。
智谱提供了官方的额度查询接口(注意 Authorization 是裸 key,不加 Bearer):
$ curl -sS -H "Authorization: $KEY" https://open.bigmodel.cn/api/monitor/usage/quota/limit
{"code":200,"data":{"limits":[
{"type":"CREDIT_LIMIT","unit":3,"number":5,"usage":28000,"currentValue":9894,"remaining":18105,"percentage":35,"nextResetTime":1791963412620},
{"type":"CREDIT_LIMIT","unit":6,"number":1,"usage":140000,"currentValue":101405,"remaining":38594,"percentage":72,"nextResetTime":1792406188985}
],"level":"max"},"success":true}
level: max,5 小时窗口用了 35%,下一次重置 2026-10-14 15:36:52。根本没到限额。
期间还推翻过一个猜测:本机正跑着 BugHunter(它内嵌的 opencode 也在调智谱),一度怀疑是它把账号的并发/速率抢光了。实测那是另一个号上的行为叠加,不是这次报错的成因——留个记录,防止后面再往这条路上想。
第二轮:两个「重置时间」对不上
真正的线索藏在报错文本里:
报错: 限额将在 2026-10-14 16:17:09 重置
额度接口: nextResetTime = 2026-10-14 15:36:52
差 40 分钟。如果这俩说的是同一份额度,时间必须一致。说明它们压根不是同一份额度——报错来自另一把 key、另一个账号。
第三轮:去凭据库挖真正在用的那把
opencode 换到 v2 之后(本机 v2.0.28),凭据不再只存 auth.json,而是进了 SQLite:
$ sqlite3 ~/.local/share/opencode/opencode.db ".schema credential"
CREATE TABLE `credential` (
`id` text PRIMARY KEY,
`integration_id` text,
`label` text NOT NULL,
`value` text NOT NULL,
`connector_id` text,
`method_id` text,
`active` integer,
...
);
一查就露馅了:
$ sqlite3 ~/.local/share/opencode/opencode.db \
"select id,label,active from credential where integration_id='zhipuai-coding-plan';"
cred_0b34fab5e003sgLc73m1YE8RNC|API key|0
cred_12f7e5bcf001CEr7ffu3qF02M2|Zhipu AI Coding Plan|1
同一个 zhipuai-coding-plan 挂着两把 key,其中 active=1 的那把(67ca616f……)既不是配置文件里写的 7583f11a……,也不是我以为的任何一把。把两把分别拿去问额度接口:
| 凭据 label | active | 账号 | 套餐 | 5h 用量 | 5h 下次重置 |
|---|---|---|---|---|---|
| API key | 0 | 4231775790108884 | max | 35% | 15:36:52 |
| Zhipu AI Coding Plan | 1 | 58081771827682775 | lite | 2047 / 2000 = 100% | 16:17:09 |
active 那条是另一个智谱账号的 Lite 套餐,5 小时额度打满,重置时间 16:17:09——与报错一字不差。
实锤:opencode 用的是 active 那条,配置文件里的 options.apiKey 只是摆设。
为什么会并存两把 key
这是 v2 凭据模型的设计,不是配置写错了。integration(provider)与 credential 是 1 : N,但用唯一部分索引锁死只能有一条 active:
-- core/src/database/migration/20260611035744_credential.ts
CREATE UNIQUE INDEX `credential_connector_active_idx`
ON `credential` (`connector_id`) WHERE "active" = 1;
运行时解析优先级:
Integration 的 active connection → provider.options.apiKey(opencode.json) → env(ZHIPU_API_KEY)
active 连接一旦存在就赢。而 auth login / TUI 里点「连接 provider」的语义是**「新增凭据并激活」**,不是覆盖旧的。label 默认取 integration 名,重名才自动追加 2、 3——也就是说「一个 provider 多条凭据」是被预期的场景。
本机的时间线正好踩上去:
2026-09-18 14:58建API key(label 是流程默认名)→ Max 账号;2026-10-12 17:42走登录流程建了Zhipu AI Coding Plan(label = provider 名)→ 另一个账号的 Lite,创建即 active。
旧的那条不会删,只是被置成 active=0。于是「配置写 A、实际跑 B」,而 v1 时代只有 auth.json、一个 provider 一把 key、后写覆盖,根本不会出现这种并存——这也是为什么 BugHunter 内嵌的那份 v1.18.33 一直安安静静。
修复
# 看清楚有几条、哪条 active
$ opencode auth list --format json
$ opencode auth switch zhipuai-coding-plan cred_0b34fab5e003sgLc73m1YE8RNC
# 验证
$ opencode run "reply with exactly: OK"
OK
切完之后日志里再没出现「5 小时限额」,nextReset 也和 Max 账号的 15:36:52 对上了。剩下的偶发 > build · glm-5.3 重试是账号级 1302 速率限制,重试即过,与额度无关。
不需要的话,那条外账号凭据可以删掉——随后也确实删了:
$ opencode auth logout zhipuai-coding-plan cred_12f7e5bcf001CEr7ffu3qF02M2
┌ Log out of an account
◇ Removed account from Zhipu AI Coding Plan
└ Done
# 删完自检:只剩一条
$ opencode auth list
Zhipu AI Coding Plan API key stored
注意 logout 删的只是本机这条登记,智谱侧那把 67ca616f…… 依然有效(拿去 curl 照样通),要彻底作废得去智谱控制台删 key。
追问:credential id 和 provider 签发的 api-key 是什么关系
修完顺手把模型捋清。疑惑很自然:auth list 里那个 cred_0b34fab5…… 并不是认证时填的 key——对,它本来就不是,两者是「登记号」和「钥匙」的关系:
| provider 签发的 API key | SQLite 里的 credential id | |
|---|---|---|
| 签发方 | 智谱控制台(「新建 API Key」得到的那串) | opencode 本地生成(ID.create(),cred_ 前缀) |
| 在哪有效 | 你 ↔ 智谱之间,服务端按它反查账号/套餐/额度 | 本机 ↔ 本地 DB 之间,只是表主键 |
| 出现在哪 | 每个 HTTP 请求的 Authorization 头 | 只在本地 DB、auth list --format json、switch/logout 参数里,从不上网络 |
| 生命周期 | 控制台创建/吊销,与 opencode 无关 | 随 auth login/logout 增删,与智谱无关 |
一行 credential = 登记号(id)+ 钥匙本体(value.key)+ 人读名字(label)+ 生效位(active):
$ sqlite3 opencode.db "select id, label, substr(json_extract(value,'\$.key'),1,10), active …"
cred_0b34fab5e003sgLc73m1YE8RNC | API key | a3feeb6656 | 1
↑ opencode 生成的主键 ↑ 认证时填的 key 在 value 列
由这个模型直接推出三件事:
- 两把不同的 key 必然占两行(两个 cred_id)——
value.key是单值;但反向不成立,两个 cred_id 未必是两把不同的 key(同一把 key 重复 login 会登记成两行)。 - 多把 key 可同属一个账号:实测
7583f11a/a3feeb66/9ca0e2a2三把查额度返回同一账号、同一数值——额度跟账号走,跟哪把 key 无关。所以同账号多行并存无害;这次的事故需要三个条件同时成立:多行 + 跨账号 + active 恰好落在外账号那行。 - logout ≠ 吊销:
auth logout删登记号,智谱侧 key 照常可用;反过来控制台删了 key,本地登记也不会自动消失,只会开始 401。轮换 key 的正确顺序:控制台建新 key → opencodelogin新 key 并switch→ 验证可用 → 控制台删旧 key → 本地logout旧行。
还有一个易踩点:auth list 默认输出三列是「provider 名 | label | 来源」,没有 ID。好在 auth logout 的两个参数本来就接受「ID 或名字」「ID 或 label」,默认输出够用:
opencode auth logout "Zhipu AI Coding Plan" "API key" # 名字 + label
但 label 与 provider 名撞车时(这次那条的 label 恰好就叫 Zhipu AI Coding Plan)极易搞混,要 ID 就走 JSON:
$ opencode auth list --format json | jq -r \
'.[] | select(.id=="zhipuai-coding-plan") | .connections[] | "\(.id)\t\(.label)"'
cred_0b34fab5e003sgLc73m1YE8RNC API key
想看登记的 key 本体,用 opencode auth export(会打印明文,别往外贴)。
复盘
- 先确认「运行时实际用的哪把钥匙」,再查额度。 配置文件里的
apiKey不代表运行时用它;v2 有独立的凭据库,且 active 凭据优先级更高。排查入口就一句:opencode auth list --format json,再拿 active 行的 key 去查服务商侧状态。 - 报错文本里的重置时间是金线索。 它和后台/接口显示的时间对不上时,基本可以断定「报错来自另一个账号/另一份额度」。
- 「登记号」和「钥匙」分两层看。 cred_id 是本地主键、从不上网络;key 才随请求出门。所有「我明明配了 A」的争论,都要落到「active 行的
value是什么」才有意义。 - 错误的假设也要留档:中途把锅甩给并发的 BugHunter,实测站不住,记下来省得下次再绕。
- 同账号的多把 key 共存无害(指向同一份额度);真正危险的是跨账号——一次
auth login就能静默把运行时切走。