Skip to content

Commit 3659617

Browse files
weizhoubluegithub-actions[bot]
authored andcommitted
fallback feature
Signed-off-by: weizhoublue <weizhou.lan@daocloud.io>
1 parent d5ba5c8 commit 3659617

22 files changed

Lines changed: 2923 additions & 18 deletions

docs/key-rotation.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# API Key Rotation & Throttle Suppression
2+
3+
## 用户使用方式
4+
5+
### 配置多个 API Key
6+
7+
`OPENCODE_API_KEY` 中用英文逗号分隔多个 key:
8+
9+
```bash
10+
export OPENCODE_API_KEY="sk-key1,sk-key2,sk-key3"
11+
opencode run "帮我写一个排序函数"
12+
```
13+
14+
opencode 会依次从 key1 开始尝试。遇到限额(429)或无效 key(401)时自动切换到下一个,对用户完全透明。
15+
16+
### 相关环境变量
17+
18+
| 变量 | 默认值 | 说明 |
19+
|---|---|---|
20+
| `OPENCODE_API_KEY` || 逗号分隔的 API key 列表 |
21+
| `OPENCODE_THROTTLE_ENABLE` | `true` | 设为 `false` 可禁用跨进程限流记录 |
22+
| `OPENCODE_THROTTLE_DURATION` | `120`(分钟) | 限流记录的有效期 |
23+
| `OPENCODE_WELAN_LOG` | `true` | 设为 `false` 可禁用 welan.txt 日志 |
24+
25+
### 跨进程限流(多个 opencode 进程共存时)
26+
27+
限流状态保存在 `~/.config/opencode/throttle.json`。当某个 key 触发 429 时,当前进程把该 key 写入此文件并附上失效时间。其他进程启动时读取该文件,自动跳过仍在限流期内的 key——无需任何手动操作。
28+
29+
```
30+
~/.config/opencode/
31+
├── throttle.json # 跨进程限流状态(自动管理)
32+
└── welan-log.txt # key 轮转决策日志
33+
```
34+
35+
### 日志(welan.txt)
36+
37+
key 轮转决策会写两份日志:
38+
39+
- `~/.config/opencode/welan-log.txt`:专用排查日志,保留进程前缀。
40+
- `~/.local/state/opencode/log/opencode.log`(随 `Global.Path.log` 配置变化):遵循 opencode 原有结构化日志格式。设置 `OPENCODE_PRINT_LOGS=1` 时,也按原有日志机制输出到 stderr。
41+
42+
`welan-log.txt` 示例:
43+
44+
```
45+
[012300000Z] [2026-07-10T09:23:00.000] [INFO] key-rotation: start, 2 key(s) configured
46+
[012300000Z] [2026-07-10T09:23:05.000] [ERROR] key-rotation: key ***key-1 throttled for 120 minutes, writing throttle record
47+
[012300000Z] [2026-07-10T09:23:05.000] [ERROR] key-rotation: key ***key-1 quota_limit, trying next
48+
[012300000Z] [2026-07-10T09:23:05.000] [INFO] key-rotation: attempt 2 with key ***key-2
49+
[012300000Z] [2026-07-10T09:23:10.000] [INFO] key-rotation: success with key ***key-2
50+
```
51+
52+
Key 在日志中脱敏,只显示最后 6 位。第二列(如 `[012300000Z]`)是进程级前缀,同一 CLI 进程的所有日志共享该值,方便在多进程并发时区分来源。
53+
54+
`opencode.log` 示例:
55+
56+
```
57+
timestamp=2026-07-10T09:23:05.000Z level=ERROR run=abcd1234 message="key-rotation: key ***key-1 quota_limit, trying next"
58+
```
59+
60+
### CLI 错误输出
61+
62+
单 key 命中已识别的 quota/rate-limit 时,CLI 在 stderr 输出:
63+
64+
```text
65+
Error: OPENCODE_QUOTA_LIMIT: <provider 或 retry 消息>
66+
```
67+
68+
多 key 轮转中,某个 key 命中 quota 而后续 key 成功时,CLI 不输出该中间错误。所有 key 最终耗尽时,CLI 只输出一次 `OPENCODE_QUOTA_LIMIT` 前缀;若最后一次请求有 provider 消息则保留该消息,否则输出 `all configured API keys are exhausted or throttled`。最后终态是无效 key 时,输出 `OPENCODE_INVALID_API_KEY: <message>`
69+
70+
---
71+
72+
## 内部实现
73+
74+
### 模块结构
75+
76+
```
77+
packages/opencode/src/
78+
├── provider/
79+
│ ├── throttle-store.ts # 读写 throttle.json,Flock 写锁
80+
│ ├── rotation-logger.ts # 追加写 welan-log.txt,fire-and-forget
81+
│ └── key-rotator.ts # 解析 key 列表,管理轮转状态
82+
├── session/
83+
│ └── retry.ts # isInvalidKeyAPIError(401 检测)
84+
└── cli/cmd/
85+
└── run/key-rotation.ts # runWithKeyRotation 轮转主循环
86+
```
87+
88+
这三个新模块没有任何 Effect / Provider / Session 依赖,可以单独使用。
89+
90+
### 轮转流程
91+
92+
```
93+
opencode run "prompt"
94+
95+
96+
runWithKeyRotation({ createSdk, execute, reset, onExhausted })
97+
98+
├─ KeyRotator.selectKey()
99+
│ ├─ 读 throttle.json(无锁)
100+
│ ├─ 跳过限流期内的 key
101+
│ └─ 跳过本进程已标记无效的 key
102+
103+
├─ process.env.OPENCODE_API_KEY = selectedKey
104+
├─ disposeInstance(directory) ← 仅在第 2 次以后调用
105+
├─ Server.Default.reset() ← 仅在第 2 次以后调用
106+
│ └─ 清除目录级 InstanceState 与惰性 Server,下次 fetch 读新 key
107+
108+
├─ execute(sdk)
109+
│ └─ 遇到可轮换错误时抛出 KeyRotationRetry
110+
111+
├─ quota_limit → KeyRotator.recordThrottle(key) → 写 throttle.json → 换 key
112+
├─ invalid_key → KeyRotator.markInvalid(key) → 进程内跳过 → 换 key
113+
└─ success → 结束
114+
```
115+
116+
### 关键设计决策
117+
118+
**disposeInstance() + Server.Default.reset()** — Provider key 缓存在目录级 `InstanceState` 中。换 key 前先清除当前目录的 InstanceState,再 reset 惰性 Server,下一次 HTTP 请求会用新的 `process.env.OPENCODE_API_KEY` 重建本地执行路径。这样不需要在 Provider/Env 层监听全局 env 变化。
119+
120+
**KeyRotationRetry**`execute()` 保留原来的成功/失败返回语义。只有 429/quota/rate-limit 和 401 这两类可轮换错误会抛出 `KeyRotationRetry`,由外层 `runWithKeyRotation()` 捕获并换 key。
121+
122+
**Session 复用** — 显式传入 session ID 时,每次 `execute()` 都按原始 CLI 参数解析并复用同一 session。未传入 session ID 时,每次轮转沿用原有 CLI 语义创建新 session;失败 key 创建但未成功执行的 session 不会被下一次尝试继承。
123+
124+
**锁策略**`isThrottled`(读)不加锁,宁可偶发读到旧数据也不阻塞 API 调用。`addThrottle` / `cleanExpired`(写)使用 `Flock.acquire`,超时 2s 后放弃写入(宁漏记,不阻塞)。进程崩溃导致的僵尸锁通过 `staleMs: 10_000` 自动清理。
125+
126+
**throttleEnabled 默认开启**`OPENCODE_THROTTLE_ENABLE !== "false"`,未设置时视为开启。`retry.ts` 中的同名判断也用同一逻辑,确保 429 能直接冒泡到外层轮转循环,而不是在 SDK 内部重试同一个已限流的 key。

0 commit comments

Comments
 (0)