@albinocrabs/o-switcher 0.1.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/CONTRIBUTING.md +72 -0
- package/LICENSE +21 -0
- package/README.md +361 -0
- package/dist/chunk-BTDKGS7P.js +1777 -0
- package/dist/chunk-BTDKGS7P.js.map +1 -0
- package/dist/index.cjs +2585 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2021 -0
- package/dist/index.d.ts +2021 -0
- package/dist/index.js +835 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.cjs +1177 -0
- package/dist/plugin.cjs.map +1 -0
- package/dist/plugin.d.cts +22 -0
- package/dist/plugin.d.ts +22 -0
- package/dist/plugin.js +194 -0
- package/dist/plugin.js.map +1 -0
- package/docs/api-reference.md +286 -0
- package/docs/architecture.md +511 -0
- package/docs/examples.md +190 -0
- package/docs/getting-started.md +316 -0
- package/package.json +60 -0
- package/scripts/collect-errors.ts +159 -0
- package/scripts/corpus.jsonl +5 -0
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Contributing to O-Switcher
|
|
2
|
+
|
|
3
|
+
Thank you for your interest in contributing!
|
|
4
|
+
|
|
5
|
+
## Getting Started
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git clone https://github.com/<owner>/o-switcher.git
|
|
9
|
+
cd o-switcher
|
|
10
|
+
npm install
|
|
11
|
+
npm test # 351 tests, all must pass
|
|
12
|
+
npm run typecheck # zero TypeScript errors
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Development Workflow
|
|
16
|
+
|
|
17
|
+
1. **Fork** the repository
|
|
18
|
+
2. **Create a branch** from `main`: `git checkout -b feat/my-feature`
|
|
19
|
+
3. **Write tests first** (TDD) — tests live next to source files (`*.test.ts`)
|
|
20
|
+
4. **Implement** — make the tests pass
|
|
21
|
+
5. **Verify** — `npm test && npm run typecheck`
|
|
22
|
+
6. **Commit** — use [conventional commits](https://www.conventionalcommits.org/):
|
|
23
|
+
`feat(routing):`, `fix(config):`, `test(execution):`, `docs:`
|
|
24
|
+
7. **Open a PR** against `main`
|
|
25
|
+
|
|
26
|
+
## Project Structure
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
src/
|
|
30
|
+
├── config/ Config loading, Zod schema validation
|
|
31
|
+
├── registry/ Target registry, health scoring, mode detection
|
|
32
|
+
├── errors/ Error taxonomy (10 classes), dual-mode classifier
|
|
33
|
+
├── retry/ Bounded retry with backoff, jitter, Retry-After
|
|
34
|
+
├── routing/ Policy engine, circuit breaker, admission, failover
|
|
35
|
+
├── execution/ Mode adapters, stream stitcher, audit collector
|
|
36
|
+
├── operator/ Operator commands, config reload, plugin tools
|
|
37
|
+
├── integration/ End-to-end integration tests
|
|
38
|
+
├── audit/ Pino-based structured audit logger
|
|
39
|
+
├── mode/ Deployment mode types and capabilities
|
|
40
|
+
└── spike/ Proof-of-concept explorations
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Code Conventions
|
|
44
|
+
|
|
45
|
+
- **TypeScript strict mode** — `noUncheckedIndexedAccess`, `verbatimModuleSyntax`
|
|
46
|
+
- **ESM** — `"type": "module"` in package.json
|
|
47
|
+
- **Factory functions** — `createXxx()` returning interfaces (not classes)
|
|
48
|
+
- **Pure functions** — routing/scoring logic has no I/O
|
|
49
|
+
- **No `let`** — use `const` only, restructure if needed
|
|
50
|
+
- **No `!!`** — use `Boolean()` for explicit coercion
|
|
51
|
+
- **Vitest** for testing — co-located `*.test.ts` files
|
|
52
|
+
|
|
53
|
+
## Testing
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm test # all tests
|
|
57
|
+
npx vitest run src/routing/ # specific module
|
|
58
|
+
npx vitest run --watch # watch mode
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Architecture
|
|
62
|
+
|
|
63
|
+
See [README.md](README.md) for the architecture diagram and module overview.
|
|
64
|
+
|
|
65
|
+
## Reporting Issues
|
|
66
|
+
|
|
67
|
+
- Use GitHub Issues
|
|
68
|
+
- Include: steps to reproduce, expected vs actual behavior, Node.js version
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
By contributing, you agree that your contributions will be licensed under the MIT License.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 O-Switcher Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# O-Switcher
|
|
2
|
+
|
|
3
|
+
[English](#english) | [Русский](#русский)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## English
|
|
8
|
+
|
|
9
|
+
### What is it?
|
|
10
|
+
|
|
11
|
+
An [OpenCode](https://opencode.ai) plugin that automatically switches between LLM providers when one goes down. If Anthropic returns errors — your request silently goes to OpenAI. No manual intervention needed.
|
|
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
|
|
20
|
+
|
|
21
|
+
Add to your `opencode.json`:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"plugin": [
|
|
26
|
+
"@apolenkov/o-switcher@latest"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or for local development:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"plugin": [
|
|
36
|
+
"/path/to/o-switcher"
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Configure
|
|
42
|
+
|
|
43
|
+
That's it. O-Switcher auto-discovers all configured providers from your OpenCode config. No target configuration needed.
|
|
44
|
+
|
|
45
|
+
Want to customize retry and timeout?
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"plugin": ["@apolenkov/o-switcher@latest"],
|
|
50
|
+
"switcher": {
|
|
51
|
+
"retry": 3,
|
|
52
|
+
"timeout": 30000
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Both fields are optional with sensible defaults.
|
|
58
|
+
|
|
59
|
+
### Examples
|
|
60
|
+
|
|
61
|
+
**Zero config — just add the plugin:**
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"plugin": ["@apolenkov/o-switcher@latest"]
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
O-Switcher reads your configured providers (anthropic, openai, etc.) and creates targets automatically. You get: automatic retry on errors, circuit breaker protection, health monitoring.
|
|
70
|
+
|
|
71
|
+
**Multiple API keys for one provider (rate limit distribution):**
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"provider": {
|
|
76
|
+
"openai-work": { "api": "openai", "apiKey": "sk-work-key-111" },
|
|
77
|
+
"openai-personal": { "api": "openai", "apiKey": "sk-personal-222" },
|
|
78
|
+
"openai-backup": { "api": "openai", "apiKey": "sk-backup-333" }
|
|
79
|
+
},
|
|
80
|
+
"plugin": ["@apolenkov/o-switcher@latest"]
|
|
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
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Claude is primary (priority 2). If Claude fails 3 times — switches to GPT.
|
|
129
|
+
|
|
130
|
+
**Custom retry and failover limits:**
|
|
131
|
+
|
|
132
|
+
```json
|
|
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
|
+
```
|
|
145
|
+
|
|
146
|
+
### How it works
|
|
147
|
+
|
|
148
|
+
1. You send a request in OpenCode
|
|
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
|
|
159
|
+
|
|
160
|
+
- [Getting Started](docs/getting-started.md) — installation, configuration, troubleshooting
|
|
161
|
+
- [API Reference](docs/api-reference.md) — all config options, operator tools, error classes, log format
|
|
162
|
+
- [Examples](docs/examples.md) — multi-key, multi-provider, log analysis, circuit breaker tuning
|
|
163
|
+
- [Architecture](docs/architecture.md) — C4 diagrams, sequence flows, internals
|
|
164
|
+
|
|
165
|
+
### Development
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
git clone https://github.com/apolenkov/o-switcher.git
|
|
169
|
+
cd o-switcher
|
|
170
|
+
npm install
|
|
171
|
+
npm test # 396 tests
|
|
172
|
+
npm run typecheck # TypeScript strict
|
|
173
|
+
npm run build # ESM + CJS
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### License
|
|
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`:
|
|
197
|
+
|
|
198
|
+
```json
|
|
199
|
+
{
|
|
200
|
+
"plugin": [
|
|
201
|
+
"@apolenkov/o-switcher@latest"
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Или для локальной разработки:
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{
|
|
210
|
+
"plugin": [
|
|
211
|
+
"/путь/до/o-switcher"
|
|
212
|
+
]
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Настройка
|
|
217
|
+
|
|
218
|
+
Все. O-Switcher автоматически находит все настроенные провайдеры из конфига OpenCode. Настраивать таргеты не нужно.
|
|
219
|
+
|
|
220
|
+
Хочешь задать свои retry и timeout?
|
|
221
|
+
|
|
222
|
+
```json
|
|
223
|
+
{
|
|
224
|
+
"plugin": ["@apolenkov/o-switcher@latest"],
|
|
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
|
+
```
|
|
358
|
+
|
|
359
|
+
### Лицензия
|
|
360
|
+
|
|
361
|
+
[MIT](LICENSE)
|