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,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.
|