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 +2582 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2020 -0
- package/dist/index.d.ts +2020 -0
- package/dist/index.js +832 -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
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
### As OpenCode Plugin (recommended)
|
|
6
|
+
|
|
7
|
+
Add to your `opencode.json`:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"plugin": [
|
|
12
|
+
"@apolenkov/o-switcher@latest"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Local Development
|
|
18
|
+
|
|
19
|
+
Clone and link locally:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
git clone https://github.com/apolenkov/o-switcher.git
|
|
23
|
+
cd o-switcher
|
|
24
|
+
npm install
|
|
25
|
+
npm run build
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
In your project's `opencode.json`:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"plugin": [
|
|
33
|
+
"/path/to/o-switcher"
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Configuration
|
|
39
|
+
|
|
40
|
+
O-Switcher auto-discovers all configured providers from your OpenCode config. No target configuration needed.
|
|
41
|
+
|
|
42
|
+
### Minimal Config (zero targets)
|
|
43
|
+
|
|
44
|
+
Just add the plugin. O-Switcher reads your providers and creates targets automatically:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"plugin": ["@apolenkov/o-switcher@latest"]
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Auto-Discovery
|
|
53
|
+
|
|
54
|
+
When O-Switcher starts, it reads the `provider` section of your OpenCode config. Each provider entry becomes a target automatically with these defaults:
|
|
55
|
+
|
|
56
|
+
- `capabilities`: `["chat"]`
|
|
57
|
+
- `enabled`: `true`
|
|
58
|
+
- `operator_priority`: `0` (all targets start equal)
|
|
59
|
+
- `policy_tags`: `[]`
|
|
60
|
+
|
|
61
|
+
The routing engine differentiates via health scores over time. If one provider starts returning errors, traffic shifts to healthier providers.
|
|
62
|
+
|
|
63
|
+
**Optional retry and timeout:**
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"plugin": ["@apolenkov/o-switcher@latest"],
|
|
68
|
+
"switcher": {
|
|
69
|
+
"retry": 3,
|
|
70
|
+
"timeout": 30000
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- `retry` — max retry attempts per request (default: 3)
|
|
76
|
+
- `timeout` — max expected latency in ms (default: 30000)
|
|
77
|
+
|
|
78
|
+
### Multiple API Keys for One Provider
|
|
79
|
+
|
|
80
|
+
Register the same provider multiple times with different API keys in your `opencode.json`. O-Switcher treats each as a separate target and distributes load across them:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"provider": {
|
|
85
|
+
"openai-work": { "api": "openai", "apiKey": "sk-work-key-111" },
|
|
86
|
+
"openai-personal": { "api": "openai", "apiKey": "sk-personal-222" },
|
|
87
|
+
"openai-backup": { "api": "openai", "apiKey": "sk-backup-333" }
|
|
88
|
+
},
|
|
89
|
+
"plugin": ["@apolenkov/o-switcher@latest"]
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
When one key hits rate limits, O-Switcher automatically switches to the next key — same model, different credentials. This is the primary use case for running multiple targets.
|
|
94
|
+
|
|
95
|
+
**Managing keys via CLI:**
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
opencode providers list # see all configured providers and credentials
|
|
99
|
+
opencode providers login # add a new provider/key
|
|
100
|
+
opencode providers logout # remove a provider/key
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Manual Target Configuration (Advanced)
|
|
104
|
+
|
|
105
|
+
For full control, specify targets explicitly. When `targets` is present, auto-discovery is disabled:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"plugin": ["@apolenkov/o-switcher@latest"],
|
|
110
|
+
"switcher": {
|
|
111
|
+
"targets": [
|
|
112
|
+
{
|
|
113
|
+
"target_id": "anthropic-main",
|
|
114
|
+
"provider_id": "anthropic",
|
|
115
|
+
"capabilities": ["chat"],
|
|
116
|
+
"enabled": true,
|
|
117
|
+
"operator_priority": 1,
|
|
118
|
+
"policy_tags": []
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"target_id": "openai-backup",
|
|
122
|
+
"provider_id": "openai",
|
|
123
|
+
"capabilities": ["chat"],
|
|
124
|
+
"enabled": true,
|
|
125
|
+
"operator_priority": 0,
|
|
126
|
+
"policy_tags": []
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Full Config (all options with defaults shown)
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"switcher": {
|
|
138
|
+
"targets": [
|
|
139
|
+
{
|
|
140
|
+
"target_id": "anthropic-main",
|
|
141
|
+
"provider_id": "anthropic",
|
|
142
|
+
"capabilities": ["chat"],
|
|
143
|
+
"enabled": true,
|
|
144
|
+
"operator_priority": 1,
|
|
145
|
+
"policy_tags": ["primary"],
|
|
146
|
+
"concurrency_limit": 5,
|
|
147
|
+
"circuit_breaker": {
|
|
148
|
+
"failure_threshold": 5,
|
|
149
|
+
"failure_rate_threshold": 0.5,
|
|
150
|
+
"sliding_window_size": 10,
|
|
151
|
+
"half_open_after_ms": 30000,
|
|
152
|
+
"half_open_max_probes": 1,
|
|
153
|
+
"success_threshold": 2
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"target_id": "openai-backup",
|
|
158
|
+
"provider_id": "openai",
|
|
159
|
+
"capabilities": ["chat"],
|
|
160
|
+
"enabled": true,
|
|
161
|
+
"operator_priority": 0,
|
|
162
|
+
"policy_tags": ["backup"]
|
|
163
|
+
}
|
|
164
|
+
],
|
|
165
|
+
"retry_budget": 3,
|
|
166
|
+
"failover_budget": 2,
|
|
167
|
+
"backoff": {
|
|
168
|
+
"base_ms": 1000,
|
|
169
|
+
"multiplier": 2,
|
|
170
|
+
"max_ms": 30000,
|
|
171
|
+
"jitter": "full"
|
|
172
|
+
},
|
|
173
|
+
"routing_weights": {
|
|
174
|
+
"health": 1.0,
|
|
175
|
+
"latency": 0.5,
|
|
176
|
+
"failure": 0.8,
|
|
177
|
+
"priority": 0.3
|
|
178
|
+
},
|
|
179
|
+
"queue_limit": 100,
|
|
180
|
+
"concurrency_limit": 10,
|
|
181
|
+
"backpressure_threshold": 50,
|
|
182
|
+
"circuit_breaker": {
|
|
183
|
+
"failure_threshold": 5,
|
|
184
|
+
"failure_rate_threshold": 0.5,
|
|
185
|
+
"sliding_window_size": 10,
|
|
186
|
+
"half_open_after_ms": 30000,
|
|
187
|
+
"half_open_max_probes": 1,
|
|
188
|
+
"success_threshold": 2
|
|
189
|
+
},
|
|
190
|
+
"max_expected_latency_ms": 30000,
|
|
191
|
+
"deployment_mode_hint": "auto"
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## What Happens When You Start
|
|
197
|
+
|
|
198
|
+
1. **OpenCode loads the plugin** from `opencode.json`
|
|
199
|
+
2. **Config hook fires** — O-Switcher reads the `switcher` config, validates via Zod
|
|
200
|
+
3. **Registry initialized** — each target gets health score 1.0, circuit breaker Closed
|
|
201
|
+
4. **Hooks active:**
|
|
202
|
+
- `chat.params` — monitors target health, adjusts params for degraded targets
|
|
203
|
+
- `event` — watches `session.error` events, updates health scores
|
|
204
|
+
|
|
205
|
+
## How Routing Works
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
Request arrives
|
|
209
|
+
↓
|
|
210
|
+
Admission Control: is system overloaded?
|
|
211
|
+
↓ admitted
|
|
212
|
+
Policy Engine: which target is healthiest?
|
|
213
|
+
↓ target selected (score = health - latency - failures + priority)
|
|
214
|
+
Execute request on target
|
|
215
|
+
↓
|
|
216
|
+
Success? → done, record health +1
|
|
217
|
+
↓ failure
|
|
218
|
+
Classify error:
|
|
219
|
+
• RateLimited → cooldown + retry (up to 3 times)
|
|
220
|
+
• TransientServerFailure → retry with backoff
|
|
221
|
+
• ModelUnavailable → immediately try next target
|
|
222
|
+
• AuthFailure → abort, mark target as ReauthRequired
|
|
223
|
+
↓ retry budget exhausted
|
|
224
|
+
Failover to next target (up to 2 failovers)
|
|
225
|
+
↓ all targets exhausted
|
|
226
|
+
Return error to user
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Deployment Modes
|
|
230
|
+
|
|
231
|
+
O-Switcher detects how it's running and adjusts capabilities:
|
|
232
|
+
|
|
233
|
+
| Mode | What it means | Failover capability |
|
|
234
|
+
|------|--------------|---------------------|
|
|
235
|
+
| **plugin-only** | Installed as OpenCode plugin | Same-target parameter adjustment only. Cannot switch providers. |
|
|
236
|
+
| **server-companion** | Running as HTTP sidecar server | Full cross-provider failover with direct HTTP status codes |
|
|
237
|
+
| **SDK-control** | Embedded via OpenCode SDK | Full cross-provider failover with direct HTTP status codes |
|
|
238
|
+
|
|
239
|
+
**Most users will use plugin-only mode.** This is the default when you add O-Switcher to `opencode.json`.
|
|
240
|
+
|
|
241
|
+
## Operator Commands
|
|
242
|
+
|
|
243
|
+
Once running, you have 7 operator commands available as OpenCode tools:
|
|
244
|
+
|
|
245
|
+
| Command | What it does |
|
|
246
|
+
|---------|-------------|
|
|
247
|
+
| `listTargets` | Show all targets with health, state, circuit breaker |
|
|
248
|
+
| `pauseTarget` | Stop routing to a target (can resume later) |
|
|
249
|
+
| `resumeTarget` | Resume a paused target |
|
|
250
|
+
| `drainTarget` | Let in-flight requests finish, reject new ones |
|
|
251
|
+
| `disableTarget` | Fully disable a target |
|
|
252
|
+
| `inspectRequest` | Show full trace for a request by ID |
|
|
253
|
+
| `reloadConfig` | Hot-reload config without restart |
|
|
254
|
+
|
|
255
|
+
## Error Classes
|
|
256
|
+
|
|
257
|
+
O-Switcher classifies every provider error into one of 10 classes:
|
|
258
|
+
|
|
259
|
+
| Error | What it means | What O-Switcher does |
|
|
260
|
+
|-------|--------------|---------------------|
|
|
261
|
+
| **RateLimited** | Provider says "too many requests" | Cooldown with backoff, respect Retry-After |
|
|
262
|
+
| **QuotaExhausted** | Billing/quota limit reached | Disable target, no retry |
|
|
263
|
+
| **AuthFailure** | Invalid credentials (401) | One recovery attempt, then ReauthRequired |
|
|
264
|
+
| **PermissionFailure** | Forbidden (403) | PolicyBlocked, no retry |
|
|
265
|
+
| **PolicyFailure** | Content policy violation | PolicyBlocked, no retry |
|
|
266
|
+
| **RegionRestriction** | Region not supported | PolicyBlocked, no retry |
|
|
267
|
+
| **ModelUnavailable** | Model overloaded/not found | Immediately failover to next target |
|
|
268
|
+
| **TransientServerFailure** | Server error (500/502/503) | Retry with backoff |
|
|
269
|
+
| **TransportFailure** | Network error, timeout | Retry with backoff |
|
|
270
|
+
| **InterruptedExecution** | Stream cut mid-output | Save confirmed chunks, resume on next target |
|
|
271
|
+
|
|
272
|
+
## Troubleshooting
|
|
273
|
+
|
|
274
|
+
### Plugin not loading
|
|
275
|
+
|
|
276
|
+
Check that `opencode.json` has the correct plugin path:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
# npm package
|
|
280
|
+
"plugin": ["@apolenkov/o-switcher@latest"]
|
|
281
|
+
|
|
282
|
+
# local path (must be absolute or relative to project root)
|
|
283
|
+
"plugin": ["/absolute/path/to/o-switcher"]
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Config validation error on start
|
|
287
|
+
|
|
288
|
+
O-Switcher validates config strictly. If using manual targets, check:
|
|
289
|
+
- `targets` array is not empty (if provided)
|
|
290
|
+
- Each target has `target_id` and `provider_id`
|
|
291
|
+
- `target_id` values are unique
|
|
292
|
+
|
|
293
|
+
If not using manual targets, O-Switcher auto-discovers from your OpenCode providers. If no providers are configured, it runs in passthrough mode (no routing).
|
|
294
|
+
|
|
295
|
+
The error message will show exactly which field is invalid.
|
|
296
|
+
|
|
297
|
+
### All targets showing CircuitOpen
|
|
298
|
+
|
|
299
|
+
This means too many failures. Check:
|
|
300
|
+
- Are your API keys valid?
|
|
301
|
+
- Are the providers actually reachable?
|
|
302
|
+
- Use `listTargets` tool to see health scores and states
|
|
303
|
+
- Use `resumeTarget` to manually reset a circuit breaker
|
|
304
|
+
|
|
305
|
+
### Audit logs
|
|
306
|
+
|
|
307
|
+
O-Switcher logs all routing decisions to stdout as NDJSON (structured JSON, one line per event). Filter by `request_id` to trace a single request:
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
opencode 2>&1 | grep '"request_id":"YOUR-ID"'
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Next Steps
|
|
314
|
+
|
|
315
|
+
- [Architecture diagrams](architecture.md) — C4 diagrams and sequence flows
|
|
316
|
+
- [Contributing](../CONTRIBUTING.md) — how to develop and contribute
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "o-switcher",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Routing and execution resilience layer for OpenCode",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/apolenkov/o-switcher.git"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"opencode",
|
|
13
|
+
"llm",
|
|
14
|
+
"routing",
|
|
15
|
+
"resilience",
|
|
16
|
+
"circuit-breaker",
|
|
17
|
+
"failover",
|
|
18
|
+
"retry"
|
|
19
|
+
],
|
|
20
|
+
"main": "dist/plugin.js",
|
|
21
|
+
"module": "dist/plugin.js",
|
|
22
|
+
"types": "dist/plugin.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"import": "./dist/plugin.js",
|
|
26
|
+
"require": "./dist/plugin.cjs",
|
|
27
|
+
"types": "./dist/plugin.d.ts"
|
|
28
|
+
},
|
|
29
|
+
"./api": {
|
|
30
|
+
"import": "./dist/index.js",
|
|
31
|
+
"require": "./dist/index.cjs",
|
|
32
|
+
"types": "./dist/index.d.ts"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:watch": "vitest",
|
|
39
|
+
"typecheck": "tsc --noEmit"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"cockatiel": "^3.2.1",
|
|
43
|
+
"eventemitter3": "^5.0.4",
|
|
44
|
+
"p-queue": "^9.1.2",
|
|
45
|
+
"pino": "^10.3.1",
|
|
46
|
+
"zod": "^4.3.6"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@opencode-ai/plugin": "^1.4.3",
|
|
50
|
+
"@tsconfig/node20": "^20.1.9",
|
|
51
|
+
"@types/node": "^22",
|
|
52
|
+
"pino-pretty": "^13",
|
|
53
|
+
"tsup": "^8.5.1",
|
|
54
|
+
"typescript": "^5.4.0",
|
|
55
|
+
"vitest": "^4.1.4"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=20"
|
|
59
|
+
}
|
|
60
|
+
}
|