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.
@@ -0,0 +1,286 @@
1
+ # API Reference
2
+
3
+ ## Configuration
4
+
5
+ ### Minimal Config
6
+
7
+ ```json
8
+ {
9
+ "plugin": ["@apolenkov/o-switcher@latest"]
10
+ }
11
+ ```
12
+
13
+ Zero config. O-Switcher reads providers from OpenCode automatically.
14
+
15
+ ### Optional Settings
16
+
17
+ ```json
18
+ {
19
+ "switcher": {
20
+ "retry": 3,
21
+ "timeout": 30000
22
+ }
23
+ }
24
+ ```
25
+
26
+ | Field | Type | Default | Description |
27
+ |-------|------|---------|-------------|
28
+ | `retry` | number | 3 | Max retry attempts per target before failover |
29
+ | `timeout` | number | 30000 | Max expected latency in ms |
30
+
31
+ ### Full Config (advanced)
32
+
33
+ ```json
34
+ {
35
+ "switcher": {
36
+ "retry": 3,
37
+ "timeout": 30000,
38
+ "retry_budget": 3,
39
+ "failover_budget": 2,
40
+ "backoff": {
41
+ "base_ms": 1000,
42
+ "multiplier": 2,
43
+ "max_ms": 30000,
44
+ "jitter": "full"
45
+ },
46
+ "routing_weights": {
47
+ "health": 1.0,
48
+ "latency": 0.5,
49
+ "failure": 0.8,
50
+ "priority": 0.3
51
+ },
52
+ "queue_limit": 100,
53
+ "concurrency_limit": 10,
54
+ "backpressure_threshold": 50,
55
+ "circuit_breaker": {
56
+ "failure_threshold": 5,
57
+ "failure_rate_threshold": 0.5,
58
+ "sliding_window_size": 10,
59
+ "half_open_after_ms": 30000,
60
+ "half_open_max_probes": 1,
61
+ "success_threshold": 2
62
+ },
63
+ "max_expected_latency_ms": 30000,
64
+ "targets": []
65
+ }
66
+ }
67
+ ```
68
+
69
+ | Field | Type | Default | Description |
70
+ |-------|------|---------|-------------|
71
+ | `retry_budget` | number | 3 | Max retries per target |
72
+ | `failover_budget` | number | 2 | Max target switches per request |
73
+ | `backoff.base_ms` | number | 1000 | Initial backoff delay |
74
+ | `backoff.multiplier` | number | 2 | Backoff multiplier |
75
+ | `backoff.max_ms` | number | 30000 | Max backoff delay |
76
+ | `backoff.jitter` | string | "full" | Jitter mode: "full", "equal", "none" |
77
+ | `routing_weights.health` | number | 1.0 | Weight for health score |
78
+ | `routing_weights.latency` | number | 0.5 | Weight for latency (negative — lower is better) |
79
+ | `routing_weights.failure` | number | 0.8 | Weight for failure rate (negative — lower is better) |
80
+ | `routing_weights.priority` | number | 0.3 | Weight for operator priority |
81
+ | `queue_limit` | number | 100 | Max requests in admission queue |
82
+ | `concurrency_limit` | number | 10 | Max concurrent requests |
83
+ | `backpressure_threshold` | number | 50 | Queue depth triggering degraded mode |
84
+ | `circuit_breaker.failure_threshold` | number | 5 | Consecutive failures to open circuit |
85
+ | `circuit_breaker.failure_rate_threshold` | number | 0.5 | Failure rate (0-1) to open circuit |
86
+ | `circuit_breaker.sliding_window_size` | number | 10 | Request count for rate calculation |
87
+ | `circuit_breaker.half_open_after_ms` | number | 30000 | ms before probe after circuit opens |
88
+ | `circuit_breaker.half_open_max_probes` | number | 1 | Probe requests allowed in half-open |
89
+ | `circuit_breaker.success_threshold` | number | 2 | Successes to close circuit from half-open |
90
+ | `max_expected_latency_ms` | number | 30000 | Latency normalization ceiling |
91
+ | `targets` | array | auto-discovered | Manual target list (disables auto-discovery) |
92
+
93
+ ### Manual Target Config
94
+
95
+ When `targets` is present, auto-discovery is disabled:
96
+
97
+ ```json
98
+ {
99
+ "switcher": {
100
+ "targets": [
101
+ {
102
+ "target_id": "anthropic-main",
103
+ "provider_id": "anthropic",
104
+ "profile": "work-key",
105
+ "capabilities": ["chat"],
106
+ "enabled": true,
107
+ "operator_priority": 2,
108
+ "policy_tags": ["primary"],
109
+ "concurrency_limit": 5
110
+ }
111
+ ]
112
+ }
113
+ }
114
+ ```
115
+
116
+ | Field | Type | Required | Description |
117
+ |-------|------|----------|-------------|
118
+ | `target_id` | string | yes | Unique target identifier |
119
+ | `provider_id` | string | yes | Provider name (openai, anthropic, etc.) |
120
+ | `profile` | string | no | Auth profile name (for multi-key setups) |
121
+ | `capabilities` | string[] | yes | What the target can do (e.g. ["chat"]) |
122
+ | `enabled` | boolean | yes | Whether target is active |
123
+ | `operator_priority` | number | yes | Higher = preferred (for ranking) |
124
+ | `policy_tags` | string[] | yes | Tags for policy-based filtering |
125
+ | `concurrency_limit` | number | no | Per-target concurrent request limit |
126
+
127
+ ## Operator Tools
128
+
129
+ Available as OpenCode tools when the plugin is loaded.
130
+
131
+ ### profiles-list
132
+
133
+ List all saved auth profiles.
134
+
135
+ **Args:** none
136
+
137
+ **Returns:**
138
+ ```json
139
+ [
140
+ {
141
+ "id": "openai-1",
142
+ "provider": "openai",
143
+ "type": "oauth",
144
+ "created": "2026-04-11T10:00:00Z"
145
+ },
146
+ {
147
+ "id": "openai-2",
148
+ "provider": "openai",
149
+ "type": "api-key",
150
+ "created": "2026-04-11T12:00:00Z"
151
+ }
152
+ ]
153
+ ```
154
+
155
+ ### profiles-remove
156
+
157
+ Remove an auth profile by ID.
158
+
159
+ **Args:**
160
+ | Field | Type | Description |
161
+ |-------|------|-------------|
162
+ | `id` | string | Profile ID to remove (e.g. "openai-2") |
163
+
164
+ **Returns:**
165
+ ```json
166
+ { "removed": true, "id": "openai-2" }
167
+ ```
168
+
169
+ ### listTargets
170
+
171
+ Show all routing targets with current state.
172
+
173
+ **Args:** none
174
+
175
+ **Returns:**
176
+ ```json
177
+ [
178
+ {
179
+ "target_id": "openai-1",
180
+ "provider_id": "openai",
181
+ "profile": "work-key",
182
+ "state": "Active",
183
+ "health_score": 0.95,
184
+ "latency_ema_ms": 450,
185
+ "circuit_breaker": "Closed"
186
+ }
187
+ ]
188
+ ```
189
+
190
+ ### pauseTarget
191
+
192
+ Stop routing to a target. Can be resumed later.
193
+
194
+ **Args:** `{ "target_id": "openai-2" }`
195
+
196
+ ### resumeTarget
197
+
198
+ Resume a paused or disabled target.
199
+
200
+ **Args:** `{ "target_id": "openai-2" }`
201
+
202
+ ### drainTarget
203
+
204
+ Let in-flight requests finish, reject new ones.
205
+
206
+ **Args:** `{ "target_id": "openai-2" }`
207
+
208
+ ### disableTarget
209
+
210
+ Fully disable a target.
211
+
212
+ **Args:** `{ "target_id": "openai-2" }`
213
+
214
+ ### inspectRequest
215
+
216
+ Show full trace for a request.
217
+
218
+ **Args:** `{ "request_id": "550e8400-e29b-41d4-a716-446655440000" }`
219
+
220
+ **Returns:** Full request trace with all attempts, targets, outcomes.
221
+
222
+ ### reloadConfig
223
+
224
+ Hot-reload configuration without restart.
225
+
226
+ **Args:** `{ "config": { ... } }`
227
+
228
+ ## Error Classes
229
+
230
+ | Class | Retryable | What O-Switcher Does |
231
+ |-------|-----------|---------------------|
232
+ | `RateLimited` | yes | Cooldown with Retry-After, then retry |
233
+ | `QuotaExhausted` | no | Disable target |
234
+ | `AuthFailure` | no | One recovery attempt → ReauthRequired |
235
+ | `PermissionFailure` | no | PolicyBlocked |
236
+ | `PolicyFailure` | no | PolicyBlocked |
237
+ | `RegionRestriction` | no | PolicyBlocked |
238
+ | `ModelUnavailable` | no | Immediate failover to next target |
239
+ | `TransientServerFailure` | yes | Retry with backoff |
240
+ | `TransportFailure` | yes | Retry with backoff |
241
+ | `InterruptedExecution` | yes | Save confirmed output, resume |
242
+
243
+ ## Routing Score Formula
244
+
245
+ ```
246
+ score(target) = w_health * health_score
247
+ - w_latency * normalize(latency_ema_ms)
248
+ - w_failure * failure_score
249
+ + w_priority * operator_priority
250
+ ```
251
+
252
+ Highest score wins. Ties broken by `target_id` lexicographic sort.
253
+
254
+ ## Log Output
255
+
256
+ All logs are structured pino NDJSON. Filter by component:
257
+
258
+ ```bash
259
+ # All routing decisions
260
+ opencode 2>&1 | jq 'select(.component == "policy-engine")'
261
+
262
+ # All failover events
263
+ opencode 2>&1 | jq 'select(.component == "failover")'
264
+
265
+ # Circuit breaker transitions
266
+ opencode 2>&1 | jq 'select(.component == "log-subscriber") | select(.event == "circuit_state_change")'
267
+
268
+ # Request summaries
269
+ opencode 2>&1 | jq 'select(.msg == "request_summary")'
270
+
271
+ # Profile changes
272
+ opencode 2>&1 | jq 'select(.component == "auth-watcher")'
273
+ ```
274
+
275
+ ## Library API
276
+
277
+ For programmatic use (not as plugin):
278
+
279
+ ```typescript
280
+ import { validateConfig, createRegistry } from 'o-switcher/api';
281
+
282
+ const config = validateConfig({ targets: [...] });
283
+ const registry = createRegistry(config);
284
+ ```
285
+
286
+ Full API exported from `o-switcher/api` — see TypeScript definitions.