@albinocrabs/o-switcher 0.1.0 → 0.1.1

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.
@@ -1,511 +0,0 @@
1
- # O-Switcher Architecture
2
-
3
- ## C4 Level 1: System Context
4
-
5
- Who uses O-Switcher and what it connects to.
6
-
7
- ```mermaid
8
- C4Context
9
- title O-Switcher — System Context
10
-
11
- Person(developer, "Developer", "Uses OpenCode for AI-assisted coding")
12
-
13
- System(opencode, "OpenCode Runtime", "AI coding assistant with plugin system")
14
- System(oswitcher, "O-Switcher", "Routing and execution resilience layer. Routes LLM requests to healthy targets with retry, failover, and circuit breaking")
15
-
16
- System_Ext(anthropic, "Anthropic API", "Claude models")
17
- System_Ext(openai, "OpenAI API", "GPT models")
18
- System_Ext(google, "Google AI API", "Gemini models")
19
- System_Ext(bedrock, "AWS Bedrock", "Multi-provider gateway")
20
-
21
- Rel(developer, opencode, "Writes code with", "CLI / IDE")
22
- Rel(opencode, oswitcher, "Routes LLM requests through", "Plugin hooks / SDK")
23
- Rel(oswitcher, anthropic, "Sends requests", "HTTPS")
24
- Rel(oswitcher, openai, "Sends requests", "HTTPS")
25
- Rel(oswitcher, google, "Sends requests", "HTTPS")
26
- Rel(oswitcher, bedrock, "Sends requests", "HTTPS")
27
- ```
28
-
29
- ## C4 Level 2: Container
30
-
31
- Internal modules of O-Switcher.
32
-
33
- ```mermaid
34
- C4Container
35
- title O-Switcher — Containers
36
-
37
- Person(developer, "Developer")
38
- System_Ext(opencode, "OpenCode Runtime")
39
-
40
- Container_Boundary(oswitcher, "O-Switcher") {
41
- Container(plugin, "Plugin Entry", "TypeScript", "chat.params + event hooks. Entry point for OpenCode plugin system")
42
- Container(config, "Config & Registry", "Zod 4", "Config validation, target registry with health scores and state")
43
- Container(errors, "Error Classifier", "TypeScript", "10 error classes, dual-mode: direct HTTP + heuristic events")
44
- Container(routing, "Routing Engine", "cockatiel + p-queue", "Policy engine, circuit breaker, admission control, failover orchestrator")
45
- Container(execution, "Execution Layer", "TypeScript", "Mode adapters, stream stitcher, audit collector")
46
- Container(operator, "Operator Surface", "TypeScript", "7 runtime commands, config reload, server auth")
47
- Container(audit, "Audit Trail", "pino", "Structured NDJSON logs with request correlation and credential redaction")
48
- }
49
-
50
- System_Ext(providers, "LLM Providers", "Anthropic, OpenAI, Google, Bedrock")
51
-
52
- Rel(developer, opencode, "Uses")
53
- Rel(opencode, plugin, "Loads plugin", "chat.params / event hooks")
54
- Rel(plugin, config, "Reads config")
55
- Rel(plugin, routing, "Routes requests")
56
- Rel(routing, errors, "Classifies failures")
57
- Rel(routing, execution, "Dispatches to adapter")
58
- Rel(execution, providers, "HTTP/SSE requests")
59
- Rel(execution, audit, "Emits audit events")
60
- Rel(operator, config, "Reloads config")
61
- Rel(operator, routing, "Pauses/drains targets")
62
- ```
63
-
64
- ## C4 Level 3: Components — Routing Engine
65
-
66
- The core decision-making layer.
67
-
68
- ```mermaid
69
- C4Component
70
- title O-Switcher — Routing Engine Components
71
-
72
- Container_Boundary(routing, "Routing Engine") {
73
- Component(policy, "Policy Engine", "selectTarget()", "Filter-then-score: 7 exclusion types, weighted scoring, deterministic tie-breaking")
74
- Component(circuit, "Circuit Breaker", "cockatiel wrapper", "Dual-trigger hysteresis: consecutive failures + sliding window rate. Per-target FSM: Closed → Open → HalfOpen")
75
- Component(admission, "Admission Controller", "p-queue", "3-layer: hard reject → backpressure detection → concurrency gating")
76
- Component(failover, "Failover Orchestrator", "nested loops", "Outer: failover across targets (budget=2). Inner: retry per target (budget=3). Max 6 attempts")
77
- Component(cooldown, "Cooldown Manager", "adaptive", "Retry-After priority, error-class-specific duration, 10-25% jitter")
78
- Component(concurrency, "Concurrency Tracker", "Map-based", "Per-target in-flight count with acquire/release/headroom")
79
- Component(events, "Event Bus", "eventemitter3", "Typed events: circuit_state_change, target_excluded, health_updated, admission_decision, cooldown_set/expired")
80
- }
81
-
82
- Container_Ext(registry, "Target Registry")
83
- Container_Ext(classifier, "Error Classifier")
84
- Container_Ext(executor, "Execution Layer")
85
-
86
- Rel(admission, policy, "Selects target via")
87
- Rel(failover, policy, "Re-selects on failover")
88
- Rel(failover, circuit, "Records success/failure")
89
- Rel(failover, cooldown, "Sets cooldown on RateLimited")
90
- Rel(policy, registry, "Reads target state")
91
- Rel(policy, circuit, "Checks allowRequest()")
92
- Rel(circuit, events, "Emits state changes")
93
- Rel(admission, events, "Emits admission decisions")
94
- Rel(cooldown, events, "Emits cooldown set/expired")
95
- Rel(failover, classifier, "Classifies errors")
96
- Rel(failover, executor, "Calls AttemptFn")
97
- ```
98
-
99
- ## C4 Level 3: Components — Execution Layer
100
-
101
- Stream handling and audit.
102
-
103
- ```mermaid
104
- C4Component
105
- title O-Switcher — Execution Layer Components
106
-
107
- Container_Boundary(execution, "Execution Layer") {
108
- Component(orchestrator, "Execution Orchestrator", "createExecutionOrchestrator()", "Wires adapter + failover + stitcher + audit. Produces ExecutionResult with provenance")
109
- Component(adapters, "Mode Adapters", "plugin / server / SDK", "Three implementations of ModeAdapter interface. Plugin: heuristic detection. Server/SDK: direct HTTP")
110
- Component(factory, "Adapter Factory", "createAdapterFactory()", "Selects adapter by DeploymentMode detected at startup")
111
- Component(buffer, "Stream Buffer", "createStreamBuffer()", "Append-only chunk accumulator. Tracks confirmed boundary for resume")
112
- Component(stitcher, "Stream Stitcher", "createStreamStitcher()", "Multi-segment assembly with provenance metadata and continuation boundaries")
113
- Component(collector, "Audit Collector", "createAuditCollector()", "Accumulates attempts + segments, flushes to pino NDJSON on request completion")
114
- }
115
-
116
- Container_Ext(failover, "Failover Orchestrator")
117
- Container_Ext(providers, "LLM Providers")
118
- Container_Ext(pino, "Pino Logger")
119
-
120
- Rel(orchestrator, factory, "Gets adapter for mode")
121
- Rel(orchestrator, failover, "Runs retry-failover loop")
122
- Rel(orchestrator, stitcher, "Assembles segments")
123
- Rel(orchestrator, collector, "Records audit trail")
124
- Rel(adapters, providers, "HTTP/SSE requests")
125
- Rel(adapters, buffer, "Appends chunks")
126
- Rel(collector, pino, "Flushes NDJSON")
127
- Rel(stitcher, buffer, "Reads confirmed chunks")
128
- ```
129
-
130
- ## Sequence: Happy Path (Success on First Target)
131
-
132
- ```mermaid
133
- sequenceDiagram
134
- participant OC as OpenCode
135
- participant PL as Plugin (chat.params)
136
- participant AD as Admission Controller
137
- participant PE as Policy Engine
138
- participant CB as Circuit Breaker
139
- participant EX as Executor
140
- participant PR as LLM Provider
141
- participant AU as Audit Collector
142
-
143
- OC->>PL: chat.params hook (model, provider, message)
144
- PL->>AD: admit(request_id, context)
145
- AD->>AD: checkHardRejects() → pass
146
- AD->>AD: backpressure check → ok
147
- AD-->>PL: admitted
148
-
149
- PL->>PE: selectTarget(snapshot, capabilities)
150
- PE->>PE: filter: 7 exclusion checks
151
- PE->>CB: allowRequest(target-1)?
152
- CB-->>PE: true (Closed)
153
- PE->>PE: score: w1*health - w2*latency - w3*failure + w4*priority
154
- PE-->>PL: SelectionRecord {selected: target-1, score: 0.85}
155
-
156
- PL->>EX: execute(target-1, request)
157
- EX->>PR: HTTP POST /chat/completions
158
- PR-->>EX: 200 OK (streaming chunks)
159
- EX->>EX: StreamBuffer.append(chunks)
160
- EX-->>PL: success {value, latency_ms: 1200}
161
-
162
- PL->>CB: recordSuccess()
163
- PL->>AU: recordAttempt(target-1, success)
164
- AU->>AU: flush to pino NDJSON
165
-
166
- PL-->>OC: output (params unchanged, target healthy)
167
- ```
168
-
169
- ## Sequence: Retry + Failover
170
-
171
- ```mermaid
172
- sequenceDiagram
173
- participant OC as OpenCode
174
- participant FO as Failover Orchestrator
175
- participant PE as Policy Engine
176
- participant CB as Circuit Breaker
177
- participant EX as Executor
178
- participant T1 as Target 1 (Anthropic)
179
- participant T2 as Target 2 (OpenAI)
180
- participant CD as Cooldown Manager
181
- participant AU as Audit
182
-
183
- OC->>FO: execute(request)
184
-
185
- Note over FO: Outer loop: failover_no=0
186
-
187
- FO->>PE: selectTarget(exclude: ∅)
188
- PE-->>FO: target-1 (score: 0.85)
189
-
190
- Note over FO: Inner loop: attempt 1/3
191
-
192
- FO->>EX: attemptFn(target-1)
193
- EX->>T1: POST /chat
194
- T1-->>EX: 429 Too Many Requests (Retry-After: 5s)
195
- EX-->>FO: fail {error_class: RateLimited, retry_after_ms: 5000}
196
-
197
- FO->>CB: recordFailure(target-1)
198
- FO->>CD: setCooldown(target-1, 5500ms)
199
- FO->>AU: recordAttempt(retry)
200
-
201
- Note over FO: Inner loop: attempt 2/3 (after backoff)
202
-
203
- FO->>EX: attemptFn(target-1)
204
- EX->>T1: POST /chat
205
- T1-->>EX: 429 Too Many Requests
206
- EX-->>FO: fail {error_class: RateLimited}
207
-
208
- FO->>CB: recordFailure(target-1)
209
- FO->>AU: recordAttempt(retry)
210
-
211
- Note over FO: Inner loop: attempt 3/3
212
-
213
- FO->>EX: attemptFn(target-1)
214
- EX->>T1: POST /chat
215
- T1-->>EX: 429 Too Many Requests
216
- EX-->>FO: fail {error_class: RateLimited}
217
-
218
- FO->>CB: recordFailure(target-1)
219
- FO->>AU: recordAttempt(failover)
220
-
221
- Note over FO: Retry budget exhausted → FAILOVER<br/>Outer loop: failover_no=1
222
-
223
- FO->>PE: selectTarget(exclude: {target-1})
224
- PE-->>FO: target-2 (score: 0.72)
225
-
226
- Note over FO: Inner loop: attempt 1/3
227
-
228
- FO->>EX: attemptFn(target-2)
229
- EX->>T2: POST /chat
230
- T2-->>EX: 200 OK (streaming)
231
- EX-->>FO: success {value, latency_ms: 800}
232
-
233
- FO->>CB: recordSuccess(target-2)
234
- FO->>AU: recordAttempt(success)
235
- FO-->>OC: FailoverResult {outcome: success, target: target-2, retries: 3, failovers: 1}
236
- ```
237
-
238
- ## Sequence: Stream Interruption + Resume
239
-
240
- ```mermaid
241
- sequenceDiagram
242
- participant FO as Failover Orchestrator
243
- participant EX as Executor
244
- participant BF as Stream Buffer
245
- participant T1 as Target 1
246
- participant T2 as Target 2
247
- participant ST as Stream Stitcher
248
- participant AU as Audit
249
-
250
- FO->>EX: attemptFn(target-1)
251
- EX->>T1: POST /chat (streaming)
252
-
253
- T1-->>EX: chunk 1: "Here is the"
254
- EX->>BF: append(chunk 1) ✓ confirmed
255
- T1-->>EX: chunk 2: " implementation"
256
- EX->>BF: append(chunk 2) ✓ confirmed
257
- T1-->>EX: chunk 3: " of the"
258
- EX->>BF: append(chunk 3) ✓ confirmed
259
- T1--xEX: CONNECTION RESET (stream interrupted)
260
-
261
- EX-->>FO: fail {error_class: InterruptedExecution}
262
-
263
- Note over BF: confirmed: "Here is the implementation of the"<br/>confirmedCharCount: 34
264
-
265
- FO->>ST: addSegment(segment_id=0, buffer, target-1, "same_target_resume")
266
- FO->>AU: recordAttempt(failover, InterruptedExecution)
267
-
268
- Note over FO: FAILOVER → target-2
269
-
270
- FO->>EX: attemptFn(target-2)
271
- EX->>T2: POST /chat (with context: continue from "...of the")
272
- T2-->>EX: chunk 4: " sorting algorithm"
273
- EX->>BF: append(chunk 4) ✓ confirmed
274
- T2-->>EX: chunk 5: " using quicksort..."
275
- EX->>BF: append(chunk 5) ✓ confirmed
276
- T2-->>EX: [DONE]
277
- EX-->>FO: success
278
-
279
- FO->>ST: addSegment(segment_id=1, buffer, target-2, "cross_model_continuation")
280
-
281
- ST->>ST: assemble()
282
- Note over ST: StitchedOutput:<br/>text: "Here is the implementation of the[→target-2] sorting algorithm using quicksort..."<br/>segments: [{target-1, chars 0-34}, {target-2, chars 34-72}]<br/>continuation_boundaries: [{offset: 34, non_deterministic: true}]
283
-
284
- FO->>AU: flush(request_id, 2 segments, 2 targets)
285
- ```
286
-
287
- ## Sequence: Circuit Breaker State Transitions
288
-
289
- ```mermaid
290
- sequenceDiagram
291
- participant RQ as Requests
292
- participant CB as Circuit Breaker
293
- participant DB as DualBreaker
294
- participant EB as Event Bus
295
-
296
- Note over CB: State: CLOSED (Active)
297
-
298
- RQ->>CB: recordFailure()
299
- CB->>DB: failure(Closed) → false (1/5 consecutive)
300
- RQ->>CB: recordFailure()
301
- CB->>DB: failure(Closed) → false (2/5)
302
- RQ->>CB: recordFailure()
303
- CB->>DB: failure(Closed) → false (3/5)
304
- RQ->>CB: recordFailure()
305
- CB->>DB: failure(Closed) → false (4/5)
306
- RQ->>CB: recordFailure()
307
- CB->>DB: failure(Closed) → TRUE (5/5 consecutive threshold!)
308
-
309
- CB->>EB: emit circuit_state_change {from: Active, to: CircuitOpen}
310
-
311
- Note over CB: State: OPEN (CircuitOpen)<br/>Rejects all requests for 30s
312
-
313
- RQ->>CB: allowRequest() → false ❌
314
- RQ->>CB: allowRequest() → false ❌
315
-
316
- Note over CB: 30 seconds pass (half_open_after_ms)
317
-
318
- Note over CB: State: HALF-OPEN (CircuitHalfOpen)<br/>Allows 1 probe request
319
-
320
- RQ->>CB: allowRequest() → true ✓ (probe)
321
- RQ->>CB: recordSuccess()
322
- CB->>DB: success(HalfOpen)
323
-
324
- CB->>EB: emit circuit_state_change {from: CircuitHalfOpen, to: Active}
325
-
326
- Note over CB: State: CLOSED (Active)<br/>Normal routing resumed
327
- ```
328
-
329
- ## Sequence: Admission Control Flow
330
-
331
- ```mermaid
332
- sequenceDiagram
333
- participant RQ as Request
334
- participant AC as Admission Controller
335
- participant HR as Hard Reject Layer
336
- participant BP as Backpressure Layer
337
- participant PQ as p-queue Layer
338
-
339
- RQ->>AC: admit(request_id, context)
340
-
341
- AC->>HR: checkHardRejects(context)
342
-
343
- alt No eligible targets
344
- HR-->>AC: REJECTED "no eligible targets"
345
- AC-->>RQ: ❌ rejected
346
- else Operator paused
347
- HR-->>AC: REJECTED "operator pause active"
348
- AC-->>RQ: ❌ rejected
349
- else Budgets exhausted
350
- HR-->>AC: REJECTED "retry/failover budget exhausted"
351
- AC-->>RQ: ❌ rejected
352
- else All checks pass
353
- HR-->>AC: null (no hard reject)
354
- end
355
-
356
- AC->>BP: check queue.size > backpressure_threshold?
357
-
358
- alt Queue depth > threshold
359
- BP-->>AC: DEGRADED "backpressure: N pending > threshold"
360
- AC-->>RQ: ⚠️ degraded (still executes, but signals pressure)
361
- else Queue normal
362
- BP-->>AC: ok
363
- end
364
-
365
- AC->>PQ: check total capacity
366
-
367
- alt Queue full (size >= queueLimit + concurrencyLimit)
368
- PQ-->>AC: REJECTED "queue full"
369
- AC-->>RQ: ❌ rejected
370
- else Capacity available
371
- PQ-->>AC: ADMITTED
372
- AC-->>RQ: ✅ admitted
373
- end
374
- ```
375
-
376
- ## Data Flow: Request Lifecycle
377
-
378
- ```mermaid
379
- flowchart TD
380
- A[OpenCode sends request] --> B{Admission Control}
381
- B -->|rejected| Z[Return error to user]
382
- B -->|degraded| C[Select Target with degraded flag]
383
- B -->|admitted| C
384
-
385
- C --> D[Policy Engine: filter + score]
386
- D --> E{Eligible targets?}
387
- E -->|none| Z
388
- E -->|found| F[Best target selected]
389
-
390
- F --> G[Execute via Mode Adapter]
391
- G --> H{Result?}
392
-
393
- H -->|success| I[Record success on circuit breaker]
394
- I --> J[Update health score +1]
395
- J --> K[Flush audit trail]
396
- K --> L[Return result with provenance]
397
-
398
- H -->|RateLimited| M[Set adaptive cooldown]
399
- M --> N{Retry budget left?}
400
- N -->|yes| O[Backoff + retry same target]
401
- O --> G
402
- N -->|no| P{Failover budget left?}
403
-
404
- H -->|ModelUnavailable| P
405
-
406
- H -->|TransientServerFailure| N
407
-
408
- H -->|AuthFailure / PermissionFailure| Q[Abort immediately]
409
- Q --> R[Update target state]
410
- R --> K
411
-
412
- P -->|yes| S[Exclude failed target]
413
- S --> D
414
- P -->|no| T[All budgets exhausted]
415
- T --> K
416
-
417
- H -->|InterruptedExecution| U[Save confirmed chunks to buffer]
418
- U --> V[Record segment provenance]
419
- V --> P
420
-
421
- style Z fill:#f66,color:#fff
422
- style L fill:#6f6,color:#000
423
- style Q fill:#f66,color:#fff
424
- style T fill:#f96,color:#000
425
- ```
426
-
427
- ## Module Dependency Graph
428
-
429
- ```mermaid
430
- graph TB
431
- subgraph "Layer 4: Entry Points"
432
- plugin[src/plugin.ts<br/>OpenCode Plugin Entry]
433
- end
434
-
435
- subgraph "Layer 3: Operator Surface"
436
- commands[operator/commands.ts<br/>7 operator commands]
437
- reload[operator/reload.ts<br/>Config reload diff-apply]
438
- tools[operator/plugin-tools.ts<br/>Plugin tool wrappers]
439
- auth[operator/server-auth.ts<br/>Bearer token auth]
440
- end
441
-
442
- subgraph "Layer 2: Execution"
443
- orchestrator[execution/orchestrator.ts<br/>Execution orchestrator]
444
- adapters[execution/adapters/<br/>Plugin / Server / SDK]
445
- stitcher[execution/stream-stitcher.ts<br/>Multi-segment assembly]
446
- buffer[execution/stream-buffer.ts<br/>Chunk buffer]
447
- collector[execution/audit-collector.ts<br/>Audit collector]
448
- end
449
-
450
- subgraph "Layer 1: Routing (pure functions)"
451
- policy[routing/policy-engine.ts<br/>Filter-then-score]
452
- failover[routing/failover.ts<br/>Nested retry-failover]
453
- admission[routing/admission.ts<br/>Layered admission]
454
- cb[routing/circuit-breaker.ts<br/>Circuit breaker FSM]
455
- cooldown[routing/cooldown.ts<br/>Adaptive cooldown]
456
- concurrency[routing/concurrency-tracker.ts<br/>Per-target tracking]
457
- events[routing/events.ts<br/>Typed event bus]
458
- end
459
-
460
- subgraph "Layer 0: Foundation"
461
- config[config/schema.ts<br/>Zod validation]
462
- registry[registry/registry.ts<br/>Target state]
463
- errors[errors/taxonomy.ts<br/>10 error classes]
464
- classifier[errors/classifier.ts<br/>Dual-mode classifier]
465
- retry[retry/retry-policy.ts<br/>Bounded retry]
466
- backoff[retry/backoff.ts<br/>Exponential backoff]
467
- logger[audit/logger.ts<br/>Pino + redaction]
468
- mode[mode/types.ts<br/>Deployment modes]
469
- end
470
-
471
- plugin --> orchestrator
472
- plugin --> commands
473
- plugin --> config
474
-
475
- commands --> registry
476
- reload --> config
477
- reload --> registry
478
- tools --> commands
479
- auth -.-> tools
480
-
481
- orchestrator --> failover
482
- orchestrator --> adapters
483
- orchestrator --> stitcher
484
- orchestrator --> collector
485
- adapters --> buffer
486
-
487
- failover --> policy
488
- failover --> cb
489
- failover --> cooldown
490
- failover --> concurrency
491
- admission --> policy
492
- policy --> registry
493
- policy --> cb
494
- cb --> events
495
- cooldown --> events
496
- cooldown --> backoff
497
-
498
- failover --> retry
499
- failover --> errors
500
- retry --> backoff
501
- classifier --> errors
502
-
503
- collector --> logger
504
- orchestrator --> logger
505
-
506
- style plugin fill:#4a9eff,color:#fff
507
- style policy fill:#2ecc71,color:#fff
508
- style failover fill:#2ecc71,color:#fff
509
- style cb fill:#e74c3c,color:#fff
510
- style registry fill:#f39c12,color:#fff
511
- style logger fill:#9b59b6,color:#fff
package/docs/examples.md DELETED
@@ -1,190 +0,0 @@
1
- # Examples
2
-
3
- ## Basic: Just Add the Plugin
4
-
5
- ```json
6
- {
7
- "plugin": ["@apolenkov/o-switcher@latest"]
8
- }
9
- ```
10
-
11
- That's it. O-Switcher reads your OpenCode providers and handles failures automatically.
12
-
13
- ## Multiple API Keys (Rate Limit Distribution)
14
-
15
- Register the same provider multiple times in `opencode.json`:
16
-
17
- ```json
18
- {
19
- "provider": {
20
- "openai-work": {
21
- "api": "openai",
22
- "apiKey": "sk-proj-work-111..."
23
- },
24
- "openai-personal": {
25
- "api": "openai",
26
- "apiKey": "sk-proj-personal-222..."
27
- },
28
- "openai-backup": {
29
- "api": "openai",
30
- "apiKey": "sk-proj-backup-333..."
31
- }
32
- },
33
- "plugin": ["@apolenkov/o-switcher@latest"]
34
- }
35
- ```
36
-
37
- When `work` key hits rate limit → switches to `personal` → then `backup`.
38
-
39
- ## Multiple Providers (Cross-Provider Failover)
40
-
41
- ```json
42
- {
43
- "provider": {
44
- "anthropic": {
45
- "apiKey": "sk-ant-..."
46
- },
47
- "openai": {
48
- "apiKey": "sk-proj-..."
49
- }
50
- },
51
- "plugin": ["@apolenkov/o-switcher@latest"]
52
- }
53
- ```
54
-
55
- If Anthropic is down → requests go to OpenAI. Note: in plugin-only mode, cross-provider failover is limited to parameter adjustment. Full cross-provider redirect requires server-companion mode.
56
-
57
- ## Custom Retry Settings
58
-
59
- ```json
60
- {
61
- "plugin": ["@apolenkov/o-switcher@latest"],
62
- "switcher": {
63
- "retry": 5,
64
- "timeout": 60000
65
- }
66
- }
67
- ```
68
-
69
- 5 retry attempts, 60 second timeout.
70
-
71
- ## Accumulating Keys via CLI
72
-
73
- ```bash
74
- # First login — O-Switcher saves the credential
75
- opencode auth login openai
76
-
77
- # Second login — O-Switcher saves the new one too
78
- # (OpenCode overwrites, but O-Switcher keeps both)
79
- opencode auth login openai
80
-
81
- # Check your profiles
82
- # In OpenCode, use the profiles-list tool
83
- ```
84
-
85
- ## Analyzing Logs
86
-
87
- O-Switcher logs everything as NDJSON. Pipe through `jq`:
88
-
89
- ```bash
90
- # How many failovers today?
91
- opencode 2>&1 | jq 'select(.msg == "failover_start")' | wc -l
92
-
93
- # Which profile gets rate limited most?
94
- opencode 2>&1 | jq 'select(.event == "cooldown_set") | .target_id' | sort | uniq -c | sort -rn
95
-
96
- # Average retries per request
97
- opencode 2>&1 | jq 'select(.msg == "request_summary") | .total_retries' | awk '{s+=$1; n++} END {print s/n}'
98
-
99
- # Circuit breaker flapping (opens and closes)
100
- opencode 2>&1 | jq 'select(.event == "circuit_state_change") | {target: .target_id, from: .from, to: .to}'
101
-
102
- # Requests that failed completely (no successful target)
103
- opencode 2>&1 | jq 'select(.msg == "request_summary") | select(.outcome == "failure")'
104
- ```
105
-
106
- ## Manual Target Control
107
-
108
- ```json
109
- {
110
- "plugin": ["@apolenkov/o-switcher@latest"],
111
- "switcher": {
112
- "targets": [
113
- {
114
- "target_id": "claude-primary",
115
- "provider_id": "anthropic",
116
- "capabilities": ["chat"],
117
- "enabled": true,
118
- "operator_priority": 10,
119
- "policy_tags": ["primary"]
120
- },
121
- {
122
- "target_id": "gpt-backup",
123
- "provider_id": "openai",
124
- "capabilities": ["chat"],
125
- "enabled": true,
126
- "operator_priority": 1,
127
- "policy_tags": ["backup"]
128
- },
129
- {
130
- "target_id": "gemini-fallback",
131
- "provider_id": "google",
132
- "capabilities": ["chat"],
133
- "enabled": true,
134
- "operator_priority": 0,
135
- "policy_tags": ["fallback"]
136
- }
137
- ]
138
- }
139
- }
140
- ```
141
-
142
- Priority: Claude (10) → GPT (1) → Gemini (0). Health scores adjust over time.
143
-
144
- ## Conservative Circuit Breaker
145
-
146
- For critical workloads — slower to open, faster to recover:
147
-
148
- ```json
149
- {
150
- "switcher": {
151
- "circuit_breaker": {
152
- "failure_threshold": 10,
153
- "failure_rate_threshold": 0.7,
154
- "sliding_window_size": 20,
155
- "half_open_after_ms": 15000,
156
- "success_threshold": 1
157
- }
158
- }
159
- }
160
- ```
161
-
162
- ## Aggressive Circuit Breaker
163
-
164
- For fast failover — quick to open on failures:
165
-
166
- ```json
167
- {
168
- "switcher": {
169
- "circuit_breaker": {
170
- "failure_threshold": 2,
171
- "failure_rate_threshold": 0.3,
172
- "sliding_window_size": 5,
173
- "half_open_after_ms": 5000,
174
- "success_threshold": 1
175
- }
176
- }
177
- }
178
- ```
179
-
180
- ## Local Development
181
-
182
- ```bash
183
- git clone https://github.com/apolenkov/o-switcher.git
184
- cd o-switcher
185
- npm install
186
- npm run build
187
-
188
- # Use local path in your project's opencode.json:
189
- # "plugin": ["/path/to/o-switcher"]
190
- ```