@albinocrabs/o-switcher 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/LICENSE +199 -21
- package/README.md +88 -288
- package/dist/{chunk-BTDKGS7P.js → chunk-7ITX5623.js} +122 -237
- package/dist/chunk-TJ7ZGZHD.cjs +1736 -0
- package/dist/index.cjs +567 -1976
- package/dist/index.js +281 -225
- package/dist/plugin.cjs +111 -1024
- package/dist/plugin.d.cts +1 -1
- package/dist/plugin.d.ts +1 -1
- package/dist/plugin.js +100 -35
- package/package.json +56 -14
- package/src/registry/types.ts +65 -0
- package/src/state-bridge.ts +119 -0
- package/src/tui.tsx +214 -0
- package/CONTRIBUTING.md +0 -72
- package/dist/chunk-BTDKGS7P.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/plugin.cjs.map +0 -1
- package/dist/plugin.js.map +0 -1
- package/docs/api-reference.md +0 -286
- package/docs/architecture.md +0 -511
- package/docs/examples.md +0 -190
- package/docs/getting-started.md +0 -316
- package/scripts/collect-errors.ts +0 -159
- package/scripts/corpus.jsonl +0 -5
package/README.md
CHANGED
|
@@ -1,52 +1,77 @@
|
|
|
1
1
|
# O-Switcher
|
|
2
2
|
|
|
3
|
-
[
|
|
3
|
+
[](https://www.npmjs.com/package/@albinocrabs/o-switcher)
|
|
4
|
+
[](https://github.com/apolenkov/o-switcher/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
Seamless OpenRouter profile rotation for [OpenCode](https://opencode.ai). Buy multiple cheap subscriptions — when one hits its quota or rate limit, O-Switcher silently switches to the next profile. No manual intervention needed.
|
|
6
8
|
|
|
7
|
-
##
|
|
9
|
+
## Why?
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
- OpenRouter profiles have quota limits and rate limits
|
|
12
|
+
- Without O-Switcher: quota runs out, you manually re-auth with another profile, lose context
|
|
13
|
+
- With O-Switcher: automatic rotation across profiles, work continues uninterrupted
|
|
14
|
+
- Buy 5 cheap subscriptions → use them as one seamless pool
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
### Why?
|
|
14
|
-
|
|
15
|
-
- Provider APIs go down, hit rate limits, or return errors
|
|
16
|
-
- Without O-Switcher: you wait, manually switch providers, lose context
|
|
17
|
-
- With O-Switcher: automatic retry, failover to backup provider, work continues
|
|
18
|
-
|
|
19
|
-
### Install
|
|
16
|
+
## Quick Start
|
|
20
17
|
|
|
21
18
|
Add to your `opencode.json`:
|
|
22
19
|
|
|
23
20
|
```json
|
|
24
21
|
{
|
|
25
|
-
"plugin": [
|
|
26
|
-
"@apolenkov/o-switcher@latest"
|
|
27
|
-
]
|
|
22
|
+
"plugin": ["@albinocrabs/o-switcher@latest"]
|
|
28
23
|
}
|
|
29
24
|
```
|
|
30
25
|
|
|
31
|
-
|
|
26
|
+
That's it. O-Switcher auto-discovers all your configured OpenCode providers.
|
|
32
27
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
## How It Works
|
|
29
|
+
|
|
30
|
+
```mermaid
|
|
31
|
+
flowchart LR
|
|
32
|
+
subgraph pool["Subscription Pool"]
|
|
33
|
+
A["Profile A<br/>$5/mo"]
|
|
34
|
+
B["Profile B<br/>$5/mo"]
|
|
35
|
+
C["Profile C<br/>$5/mo"]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
R([LLM Request]) --> SW{O-Switcher}
|
|
39
|
+
SW -->|healthy| A
|
|
40
|
+
A -.->|"quota hit ❌"| SW
|
|
41
|
+
SW -->|rotate| B
|
|
42
|
+
B -.->|"quota hit ❌"| SW
|
|
43
|
+
SW -->|rotate| C
|
|
44
|
+
C -->|"✅ success"| RES([Response])
|
|
45
|
+
|
|
46
|
+
style pool fill:#f0f9ff,stroke:#0284c7
|
|
47
|
+
style SW fill:#4a9eff,color:#fff
|
|
48
|
+
style RES fill:#6f6,color:#000
|
|
39
49
|
```
|
|
40
50
|
|
|
41
|
-
|
|
51
|
+
1. **Install the plugin** — add `@albinocrabs/o-switcher@latest` to your `opencode.json`
|
|
52
|
+
2. **Auto-discovers your profiles** — O-Switcher reads all configured OpenRouter profiles at startup
|
|
53
|
+
3. **Re-authenticate to add more** — run `opencode auth login openrouter` with another key, O-Switcher saves both profiles
|
|
54
|
+
4. **Switcher rotates** — when one profile hits quota or rate limit, requests automatically route to the next
|
|
55
|
+
|
|
56
|
+
When a request fails, O-Switcher classifies the error and acts:
|
|
57
|
+
|
|
58
|
+
| Error | Action |
|
|
59
|
+
|-------|--------|
|
|
60
|
+
| Rate limited (429) | Waits and retries with backoff (up to 3 times) |
|
|
61
|
+
| Server error (5xx) | Retries with exponential backoff |
|
|
62
|
+
| Model unavailable | Immediately tries next target |
|
|
63
|
+
| Auth error (401) | Stops, marks target for re-authentication |
|
|
64
|
+
| All retries exhausted | Fails over to backup target (up to 2 failovers) |
|
|
65
|
+
|
|
66
|
+
All routing decisions are logged as structured NDJSON for debugging.
|
|
42
67
|
|
|
43
|
-
|
|
68
|
+
## Configuration
|
|
44
69
|
|
|
45
|
-
|
|
70
|
+
Optional — O-Switcher works with zero config. Customize retry and timeout if needed:
|
|
46
71
|
|
|
47
72
|
```json
|
|
48
73
|
{
|
|
49
|
-
"plugin": ["@
|
|
74
|
+
"plugin": ["@albinocrabs/o-switcher@latest"],
|
|
50
75
|
"switcher": {
|
|
51
76
|
"retry": 3,
|
|
52
77
|
"timeout": 30000
|
|
@@ -54,308 +79,83 @@ Want to customize retry and timeout?
|
|
|
54
79
|
}
|
|
55
80
|
```
|
|
56
81
|
|
|
57
|
-
|
|
82
|
+
| Option | Default | Description |
|
|
83
|
+
|--------|---------|-------------|
|
|
84
|
+
| `retry` | `3` | Max retry attempts per request |
|
|
85
|
+
| `timeout` | `30000` | Request timeout in milliseconds |
|
|
58
86
|
|
|
59
|
-
|
|
87
|
+
## Examples
|
|
60
88
|
|
|
61
89
|
**Zero config — just add the plugin:**
|
|
62
90
|
|
|
63
91
|
```json
|
|
64
92
|
{
|
|
65
|
-
"plugin": ["@
|
|
93
|
+
"plugin": ["@albinocrabs/o-switcher@latest"]
|
|
66
94
|
}
|
|
67
95
|
```
|
|
68
96
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
**Multiple API keys for one provider (rate limit distribution):**
|
|
97
|
+
**Multiple OpenRouter profiles (quota pool rotation):**
|
|
72
98
|
|
|
73
99
|
```json
|
|
74
100
|
{
|
|
75
101
|
"provider": {
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
102
|
+
"openrouter-main": { "api": "openrouter", "apiKey": "sk-or-v1-aaa..." },
|
|
103
|
+
"openrouter-backup": { "api": "openrouter", "apiKey": "sk-or-v1-bbb..." },
|
|
104
|
+
"openrouter-spare": { "api": "openrouter", "apiKey": "sk-or-v1-ccc..." }
|
|
79
105
|
},
|
|
80
|
-
"plugin": ["@
|
|
81
|
-
}
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
O-Switcher sees 3 separate targets (all OpenAI). When `work` key hits rate limit — switches to `personal`, then `backup`. Same model, different keys.
|
|
85
|
-
|
|
86
|
-
**Custom retry and timeout:**
|
|
87
|
-
|
|
88
|
-
```json
|
|
89
|
-
{
|
|
90
|
-
"switcher": {
|
|
91
|
-
"retry": 5,
|
|
92
|
-
"timeout": 60000
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
### Advanced: Manual Targets
|
|
98
|
-
|
|
99
|
-
For full control, you can specify targets explicitly. When `targets` is present, auto-discovery is disabled.
|
|
100
|
-
|
|
101
|
-
**Two providers — primary + backup:**
|
|
102
|
-
|
|
103
|
-
```json
|
|
104
|
-
{
|
|
105
|
-
"switcher": {
|
|
106
|
-
"targets": [
|
|
107
|
-
{
|
|
108
|
-
"target_id": "claude",
|
|
109
|
-
"provider_id": "anthropic",
|
|
110
|
-
"capabilities": ["chat"],
|
|
111
|
-
"enabled": true,
|
|
112
|
-
"operator_priority": 2,
|
|
113
|
-
"policy_tags": []
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
"target_id": "gpt",
|
|
117
|
-
"provider_id": "openai",
|
|
118
|
-
"capabilities": ["chat"],
|
|
119
|
-
"enabled": true,
|
|
120
|
-
"operator_priority": 1,
|
|
121
|
-
"policy_tags": []
|
|
122
|
-
}
|
|
123
|
-
]
|
|
124
|
-
}
|
|
106
|
+
"plugin": ["@albinocrabs/o-switcher@latest"]
|
|
125
107
|
}
|
|
126
108
|
```
|
|
127
109
|
|
|
128
|
-
|
|
110
|
+
O-Switcher sees 3 profiles (all OpenRouter). When `main` hits quota — switches to `backup`, then `spare`. Same models, different subscriptions.
|
|
129
111
|
|
|
130
|
-
|
|
112
|
+
## Operator Commands
|
|
131
113
|
|
|
132
|
-
|
|
133
|
-
{
|
|
134
|
-
"switcher": {
|
|
135
|
-
"targets": [/* ... */],
|
|
136
|
-
"retry_budget": 5,
|
|
137
|
-
"failover_budget": 3,
|
|
138
|
-
"backoff": {
|
|
139
|
-
"base_ms": 500,
|
|
140
|
-
"max_ms": 15000
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
```
|
|
114
|
+
Control targets at runtime without code changes:
|
|
145
115
|
|
|
146
|
-
|
|
116
|
+
| Command | Description |
|
|
117
|
+
|---------|-------------|
|
|
118
|
+
| `list` | Show all targets with health score, circuit breaker state, cooldown status |
|
|
119
|
+
| `pause <target>` | Temporarily stop routing to a target |
|
|
120
|
+
| `resume <target>` | Resume routing to a paused target |
|
|
121
|
+
| `drain <target>` | Stop new requests, let in-flight complete |
|
|
122
|
+
| `disable <target>` | Fully disable a target |
|
|
123
|
+
| `inspect <request_id>` | Show full request trace (attempts, failovers, segments) |
|
|
147
124
|
|
|
148
|
-
|
|
149
|
-
2. O-Switcher picks the healthiest target
|
|
150
|
-
3. If the request fails:
|
|
151
|
-
- **Rate limited** → waits and retries (up to 3 times)
|
|
152
|
-
- **Server error** → retries with backoff
|
|
153
|
-
- **Model unavailable** → immediately tries next target
|
|
154
|
-
- **Auth error** → stops, marks target for re-authentication
|
|
155
|
-
4. If all retries fail → switches to backup target (up to 2 failovers)
|
|
156
|
-
5. All decisions are logged for debugging
|
|
157
|
-
|
|
158
|
-
### Docs
|
|
125
|
+
## Documentation
|
|
159
126
|
|
|
160
127
|
- [Getting Started](docs/getting-started.md) — installation, configuration, troubleshooting
|
|
161
128
|
- [API Reference](docs/api-reference.md) — all config options, operator tools, error classes, log format
|
|
162
129
|
- [Examples](docs/examples.md) — multi-key, multi-provider, log analysis, circuit breaker tuning
|
|
163
130
|
- [Architecture](docs/architecture.md) — C4 diagrams, sequence flows, internals
|
|
164
131
|
|
|
165
|
-
|
|
132
|
+
## Development
|
|
166
133
|
|
|
167
134
|
```bash
|
|
168
135
|
git clone https://github.com/apolenkov/o-switcher.git
|
|
169
136
|
cd o-switcher
|
|
170
137
|
npm install
|
|
171
|
-
npm test #
|
|
138
|
+
npm test # 455 tests
|
|
172
139
|
npm run typecheck # TypeScript strict
|
|
173
140
|
npm run build # ESM + CJS
|
|
174
141
|
```
|
|
175
142
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
[MIT](LICENSE)
|
|
179
|
-
|
|
180
|
-
---
|
|
181
|
-
|
|
182
|
-
## Русский
|
|
183
|
-
|
|
184
|
-
### Что это?
|
|
185
|
-
|
|
186
|
-
Плагин для [OpenCode](https://opencode.ai), который автоматически переключается между LLM-провайдерами, когда один падает. Если Anthropic возвращает ошибки — запрос тихо уходит в OpenAI. Никакого ручного вмешательства.
|
|
187
|
-
|
|
188
|
-
### Зачем?
|
|
189
|
-
|
|
190
|
-
- API провайдеров падают, упираются в rate limit, возвращают ошибки
|
|
191
|
-
- Без O-Switcher: ждёшь, вручную переключаешь провайдера, теряешь контекст
|
|
192
|
-
- С O-Switcher: автоматический retry, failover на резервного провайдера, работа продолжается
|
|
193
|
-
|
|
194
|
-
### Установка
|
|
195
|
-
|
|
196
|
-
Добавь в `opencode.json`:
|
|
143
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full development guide.
|
|
197
144
|
|
|
198
|
-
|
|
199
|
-
{
|
|
200
|
-
"plugin": [
|
|
201
|
-
"@apolenkov/o-switcher@latest"
|
|
202
|
-
]
|
|
203
|
-
}
|
|
204
|
-
```
|
|
145
|
+
## Contributing
|
|
205
146
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
```json
|
|
209
|
-
{
|
|
210
|
-
"plugin": [
|
|
211
|
-
"/путь/до/o-switcher"
|
|
212
|
-
]
|
|
213
|
-
}
|
|
214
|
-
```
|
|
147
|
+
O-Switcher is open source and we welcome contributions! Whether it's bug reports, feature ideas, documentation improvements, or code — every contribution matters.
|
|
215
148
|
|
|
216
|
-
|
|
149
|
+
Check out our [Contributing Guide](CONTRIBUTING.md) to get started. Look for issues labeled [`good first issue`](https://github.com/apolenkov/o-switcher/labels/good%20first%20issue) — these are curated for new contributors.
|
|
217
150
|
|
|
218
|
-
|
|
151
|
+
## Roadmap
|
|
219
152
|
|
|
220
|
-
|
|
153
|
+
O-Switcher started as a plugin, but the goal is to contribute this as a core feature to [OpenCode](https://github.com/opencode-ai/opencode) itself — so that any provider (not just OpenRouter) can have multiple profiles that automatically substitute each other when problems occur on any of them.
|
|
221
154
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
"switcher": {
|
|
226
|
-
"retry": 3,
|
|
227
|
-
"timeout": 30000
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
Оба поля опциональны и имеют разумные дефолты.
|
|
233
|
-
|
|
234
|
-
### Примеры
|
|
235
|
-
|
|
236
|
-
**Нулевой конфиг — просто добавь плагин:**
|
|
237
|
-
|
|
238
|
-
```json
|
|
239
|
-
{
|
|
240
|
-
"plugin": ["@apolenkov/o-switcher@latest"]
|
|
241
|
-
}
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
O-Switcher читает настроенных провайдеров (anthropic, openai и т.д.) и создает таргеты автоматически. Получаешь: автоматический retry, circuit breaker, мониторинг здоровья.
|
|
245
|
-
|
|
246
|
-
**Несколько API ключей для одного провайдера (распределение rate limit):**
|
|
247
|
-
|
|
248
|
-
```json
|
|
249
|
-
{
|
|
250
|
-
"provider": {
|
|
251
|
-
"openai-work": { "api": "openai", "apiKey": "sk-work-key-111" },
|
|
252
|
-
"openai-personal": { "api": "openai", "apiKey": "sk-personal-222" },
|
|
253
|
-
"openai-backup": { "api": "openai", "apiKey": "sk-backup-333" }
|
|
254
|
-
},
|
|
255
|
-
"plugin": ["@apolenkov/o-switcher@latest"]
|
|
256
|
-
}
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
O-Switcher видит 3 отдельных таргета (все OpenAI). Когда ключ `work` упирается в rate limit — переключается на `personal`, потом `backup`. Та же модель, разные ключи.
|
|
260
|
-
|
|
261
|
-
Посмотреть настроенные провайдеры и ключи:
|
|
262
|
-
|
|
263
|
-
```bash
|
|
264
|
-
opencode providers list # все провайдеры и авторизации
|
|
265
|
-
opencode providers login # добавить новый ключ
|
|
266
|
-
opencode providers logout # удалить ключ
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
**Свои retry и timeout:**
|
|
270
|
-
|
|
271
|
-
```json
|
|
272
|
-
{
|
|
273
|
-
"switcher": {
|
|
274
|
-
"retry": 5,
|
|
275
|
-
"timeout": 60000
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
### Продвинутое: Ручная настройка таргетов
|
|
281
|
-
|
|
282
|
-
Для полного контроля можно указать таргеты явно. Когда `targets` присутствует, автообнаружение отключается.
|
|
283
|
-
|
|
284
|
-
**Два провайдера — основной + резервный:**
|
|
285
|
-
|
|
286
|
-
```json
|
|
287
|
-
{
|
|
288
|
-
"switcher": {
|
|
289
|
-
"targets": [
|
|
290
|
-
{
|
|
291
|
-
"target_id": "claude",
|
|
292
|
-
"provider_id": "anthropic",
|
|
293
|
-
"capabilities": ["chat"],
|
|
294
|
-
"enabled": true,
|
|
295
|
-
"operator_priority": 2,
|
|
296
|
-
"policy_tags": []
|
|
297
|
-
},
|
|
298
|
-
{
|
|
299
|
-
"target_id": "gpt",
|
|
300
|
-
"provider_id": "openai",
|
|
301
|
-
"capabilities": ["chat"],
|
|
302
|
-
"enabled": true,
|
|
303
|
-
"operator_priority": 1,
|
|
304
|
-
"policy_tags": []
|
|
305
|
-
}
|
|
306
|
-
]
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
Claude основной (приоритет 2). Если Claude упал 3 раза — переключается на GPT.
|
|
312
|
-
|
|
313
|
-
**Свои лимиты retry и failover:**
|
|
314
|
-
|
|
315
|
-
```json
|
|
316
|
-
{
|
|
317
|
-
"switcher": {
|
|
318
|
-
"targets": [/* ... */],
|
|
319
|
-
"retry_budget": 5,
|
|
320
|
-
"failover_budget": 3,
|
|
321
|
-
"backoff": {
|
|
322
|
-
"base_ms": 500,
|
|
323
|
-
"max_ms": 15000
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
### Как это работает
|
|
330
|
-
|
|
331
|
-
1. Отправляешь запрос в OpenCode
|
|
332
|
-
2. O-Switcher выбирает самый здоровый таргет
|
|
333
|
-
3. Если запрос упал:
|
|
334
|
-
- **Rate limit** → ждёт и повторяет (до 3 раз)
|
|
335
|
-
- **Ошибка сервера** → повтор с backoff
|
|
336
|
-
- **Модель недоступна** → сразу пробует следующий таргет
|
|
337
|
-
- **Ошибка авторизации** → останавливается, помечает таргет
|
|
338
|
-
4. Если все retry исчерпаны → переключение на резервный таргет (до 2 failover)
|
|
339
|
-
5. Все решения логируются для отладки
|
|
340
|
-
|
|
341
|
-
### Документация
|
|
342
|
-
|
|
343
|
-
- [Getting Started](docs/getting-started.md) — установка, настройка, траблшутинг
|
|
344
|
-
- [API Reference](docs/api-reference.md) — все опции конфига, операторские команды, классы ошибок, формат логов
|
|
345
|
-
- [Примеры](docs/examples.md) — мульти-ключи, мульти-провайдеры, анализ логов, тюнинг circuit breaker
|
|
346
|
-
- [Архитектура](docs/architecture.md) — C4 диаграммы, sequence-диаграммы, внутреннее устройство
|
|
347
|
-
|
|
348
|
-
### Разработка
|
|
349
|
-
|
|
350
|
-
```bash
|
|
351
|
-
git clone https://github.com/apolenkov/o-switcher.git
|
|
352
|
-
cd o-switcher
|
|
353
|
-
npm install
|
|
354
|
-
npm test # 359 тестов
|
|
355
|
-
npm run typecheck # TypeScript strict
|
|
356
|
-
npm run build # ESM + CJS
|
|
357
|
-
```
|
|
155
|
+
- **Now:** Plugin for OpenCode — works with OpenRouter profiles
|
|
156
|
+
- **Next:** PR to OpenCode — built-in multi-profile rotation for every provider
|
|
157
|
+
- **Vision:** Each provider in OpenCode config can have N profiles, auto-failover between them is transparent and zero-config
|
|
358
158
|
|
|
359
|
-
|
|
159
|
+
## License
|
|
360
160
|
|
|
361
|
-
[
|
|
161
|
+
[Apache-2.0](LICENSE)
|