Skip to content

Commit 2fda351

Browse files
committed
fallback feature
Signed-off-by: weizhoublue <weizhou.lan@daocloud.io>
1 parent b35fe2e commit 2fda351

15 files changed

Lines changed: 2545 additions & 34 deletions

docs/key-rotation.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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+
`~/.config/opencode/welan-log.txt` 记录每次 key 轮转的决策,方便排查问题:
38+
39+
```
40+
[012300000Z] [2026-07-10T09:23:00.000] [INFO] key-rotation: start, 2 key(s) configured
41+
[012300000Z] [2026-07-10T09:23:05.000] [WARN] key-rotation: key ***key-1 throttled for 120 minutes, writing throttle record
42+
[012300000Z] [2026-07-10T09:23:05.000] [WARN] key-rotation: key ***key-1 quota_limit, trying next
43+
[012300000Z] [2026-07-10T09:23:05.000] [INFO] key-rotation: attempt 2 with key ***key-2
44+
[012300000Z] [2026-07-10T09:23:10.000] [INFO] key-rotation: success with key ***key-2
45+
```
46+
47+
Key 在日志中脱敏,只显示最后 6 位。第二列(如 `[012300000Z]`)是进程级前缀,同一 CLI 进程的所有日志共享该值,方便在多进程并发时区分来源。
48+
49+
---
50+
51+
## 内部实现
52+
53+
### 模块结构
54+
55+
```
56+
packages/opencode/src/
57+
├── provider/
58+
│ ├── throttle-store.ts # 读写 throttle.json,Flock 写锁
59+
│ ├── rotation-logger.ts # 追加写 welan-log.txt,fire-and-forget
60+
│ └── key-rotator.ts # 解析 key 列表,管理轮转状态
61+
├── session/
62+
│ └── retry.ts # isInvalidKeyAPIError(401 检测)
63+
└── cli/cmd/
64+
└── run.ts # runWithKeyRotation 轮转主循环
65+
```
66+
67+
这三个新模块没有任何 Effect / Provider / Session 依赖,可以单独使用。
68+
69+
### 轮转流程
70+
71+
```
72+
opencode run "prompt"
73+
74+
75+
runWithKeyRotation(createSdk)
76+
77+
├─ KeyRotator.selectKey()
78+
│ ├─ 读 throttle.json(无锁)
79+
│ ├─ 跳过限流期内的 key
80+
│ └─ 跳过本进程已标记无效的 key
81+
82+
├─ process.env.OPENCODE_API_KEY = selectedKey
83+
├─ disposeInstance(directory) ← 仅在第 2 次以后调用
84+
├─ Server.Default.reset() ← 仅在第 2 次以后调用
85+
│ └─ 清除目录级 InstanceState 与惰性 Server,下次 fetch 读新 key
86+
87+
├─ execute(sdk, overrideSessionID) ← 透传上一次的 sessionID
88+
│ └─ 遇到可轮换错误时抛出 KeyRotationRetry
89+
90+
├─ quota_limit → KeyRotator.recordThrottle(key) → 写 throttle.json → 换 key
91+
├─ invalid_key → KeyRotator.markInvalid(key) → 进程内跳过 → 换 key
92+
└─ success → 结束
93+
```
94+
95+
### 关键设计决策
96+
97+
**disposeInstance() + Server.Default.reset()** — Provider key 缓存在目录级 `InstanceState` 中。换 key 前先清除当前目录的 InstanceState,再 reset 惰性 Server,下一次 HTTP 请求会用新的 `process.env.OPENCODE_API_KEY` 重建本地执行路径。这样不需要在 Provider/Env 层监听全局 env 变化。
98+
99+
**KeyRotationRetry**`execute()` 保留原来的成功/失败返回语义。只有 429/quota/rate-limit 和 401 这两类可轮换错误会抛出 `KeyRotationRetry`,由外层 `runWithKeyRotation()` 捕获并换 key。
100+
101+
**Session ID 透传**`overrideSessionID` 在轮转后传给下一次 `execute()`,使新 key 的请求继续使用同一个 SQLite session,保留上下文。
102+
103+
**锁策略**`isThrottled`(读)不加锁,宁可偶发读到旧数据也不阻塞 API 调用。`addThrottle` / `cleanExpired`(写)使用 `Flock.acquire`,超时 2s 后放弃写入(宁漏记,不阻塞)。进程崩溃导致的僵尸锁通过 `staleMs: 10_000` 自动清理。
104+
105+
**throttleEnabled 默认开启**`OPENCODE_THROTTLE_ENABLE !== "false"`,未设置时视为开启。`retry.ts` 中的同名判断也用同一逻辑,确保 429 能直接冒泡到外层轮转循环,而不是在 SDK 内部重试同一个已限流的 key。

0 commit comments

Comments
 (0)