@albinocrabs/o-switcher 0.1.0 → 0.2.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/CHANGELOG.md +27 -0
- package/LICENSE +199 -21
- package/README.md +88 -288
- package/dist/{chunk-BTDKGS7P.js → chunk-7ITX5623.js} +122 -237
- package/dist/chunk-TJ7ZGZHD.cjs +1736 -0
- package/dist/index.cjs +567 -1976
- package/dist/index.js +281 -225
- package/dist/plugin.cjs +111 -1024
- package/dist/plugin.d.cts +1 -1
- package/dist/plugin.d.ts +1 -1
- package/dist/plugin.js +100 -35
- package/package.json +56 -14
- package/src/registry/types.ts +65 -0
- package/src/state-bridge.ts +119 -0
- package/src/tui.tsx +214 -0
- package/CONTRIBUTING.md +0 -72
- package/dist/chunk-BTDKGS7P.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/plugin.cjs.map +0 -1
- package/dist/plugin.js.map +0 -1
- package/docs/api-reference.md +0 -286
- package/docs/architecture.md +0 -511
- package/docs/examples.md +0 -190
- package/docs/getting-started.md +0 -316
- package/scripts/collect-errors.ts +0 -159
- package/scripts/corpus.jsonl +0 -5
package/dist/plugin.cjs
CHANGED
|
@@ -1,1021 +1,64 @@
|
|
|
1
|
-
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __export = (target, all) => {
|
|
9
|
-
for (var name in all)
|
|
10
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
-
};
|
|
12
|
-
var __copyProps = (to, from, except, desc) => {
|
|
13
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
-
for (let key of __getOwnPropNames(from))
|
|
15
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
-
}
|
|
18
|
-
return to;
|
|
19
|
-
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
-
|
|
30
|
-
// src/plugin.ts
|
|
31
|
-
var plugin_exports = {};
|
|
32
|
-
__export(plugin_exports, {
|
|
33
|
-
default: () => plugin_default,
|
|
34
|
-
server: () => server
|
|
35
|
-
});
|
|
36
|
-
module.exports = __toCommonJS(plugin_exports);
|
|
37
|
-
|
|
38
|
-
// src/config/schema.ts
|
|
39
|
-
var import_zod = require("zod");
|
|
40
|
-
|
|
41
|
-
// src/config/defaults.ts
|
|
42
|
-
var DEFAULT_RETRY_BUDGET = 3;
|
|
43
|
-
var DEFAULT_FAILOVER_BUDGET = 2;
|
|
44
|
-
var DEFAULT_BACKOFF_BASE_MS = 1e3;
|
|
45
|
-
var DEFAULT_BACKOFF_MULTIPLIER = 2;
|
|
46
|
-
var DEFAULT_BACKOFF_MAX_MS = 3e4;
|
|
47
|
-
var DEFAULT_BACKOFF_JITTER = "full";
|
|
48
|
-
var DEFAULT_ROUTING_WEIGHT_HEALTH = 1;
|
|
49
|
-
var DEFAULT_ROUTING_WEIGHT_LATENCY = 0.5;
|
|
50
|
-
var DEFAULT_ROUTING_WEIGHT_FAILURE = 0.8;
|
|
51
|
-
var DEFAULT_ROUTING_WEIGHT_PRIORITY = 0.3;
|
|
52
|
-
var DEFAULT_MAX_EXPECTED_LATENCY_MS = 3e4;
|
|
53
|
-
var DEFAULT_QUEUE_LIMIT = 100;
|
|
54
|
-
var DEFAULT_GLOBAL_CONCURRENCY_LIMIT = 10;
|
|
55
|
-
var DEFAULT_BACKPRESSURE_THRESHOLD = 50;
|
|
56
|
-
var DEFAULT_CB_FAILURE_THRESHOLD = 5;
|
|
57
|
-
var DEFAULT_CB_FAILURE_RATE_THRESHOLD = 0.5;
|
|
58
|
-
var DEFAULT_CB_SLIDING_WINDOW_SIZE = 10;
|
|
59
|
-
var DEFAULT_CB_HALF_OPEN_AFTER_MS = 3e4;
|
|
60
|
-
var DEFAULT_CB_HALF_OPEN_MAX_PROBES = 1;
|
|
61
|
-
var DEFAULT_CB_SUCCESS_THRESHOLD = 2;
|
|
62
|
-
|
|
63
|
-
// src/config/schema.ts
|
|
64
|
-
var BackoffConfigSchema = import_zod.z.object({
|
|
65
|
-
base_ms: import_zod.z.number().positive().default(DEFAULT_BACKOFF_BASE_MS),
|
|
66
|
-
multiplier: import_zod.z.number().positive().default(DEFAULT_BACKOFF_MULTIPLIER),
|
|
67
|
-
max_ms: import_zod.z.number().positive().default(DEFAULT_BACKOFF_MAX_MS),
|
|
68
|
-
jitter: import_zod.z.enum(["full", "equal", "none"]).default(DEFAULT_BACKOFF_JITTER)
|
|
69
|
-
});
|
|
70
|
-
var RoutingWeightsSchema = import_zod.z.object({
|
|
71
|
-
health: import_zod.z.number().default(DEFAULT_ROUTING_WEIGHT_HEALTH),
|
|
72
|
-
latency: import_zod.z.number().default(DEFAULT_ROUTING_WEIGHT_LATENCY),
|
|
73
|
-
failure: import_zod.z.number().default(DEFAULT_ROUTING_WEIGHT_FAILURE),
|
|
74
|
-
priority: import_zod.z.number().default(DEFAULT_ROUTING_WEIGHT_PRIORITY)
|
|
75
|
-
});
|
|
76
|
-
var CircuitBreakerConfigSchema = import_zod.z.object({
|
|
77
|
-
failure_threshold: import_zod.z.number().int().positive().default(DEFAULT_CB_FAILURE_THRESHOLD),
|
|
78
|
-
failure_rate_threshold: import_zod.z.number().gt(0).lt(1).default(DEFAULT_CB_FAILURE_RATE_THRESHOLD),
|
|
79
|
-
sliding_window_size: import_zod.z.number().int().positive().default(DEFAULT_CB_SLIDING_WINDOW_SIZE),
|
|
80
|
-
half_open_after_ms: import_zod.z.number().positive().default(DEFAULT_CB_HALF_OPEN_AFTER_MS),
|
|
81
|
-
half_open_max_probes: import_zod.z.number().int().positive().default(DEFAULT_CB_HALF_OPEN_MAX_PROBES),
|
|
82
|
-
success_threshold: import_zod.z.number().int().positive().default(DEFAULT_CB_SUCCESS_THRESHOLD)
|
|
83
|
-
});
|
|
84
|
-
var TargetConfigSchema = import_zod.z.object({
|
|
85
|
-
target_id: import_zod.z.string().min(1),
|
|
86
|
-
provider_id: import_zod.z.string().min(1),
|
|
87
|
-
profile: import_zod.z.string().optional(),
|
|
88
|
-
endpoint_id: import_zod.z.string().optional(),
|
|
89
|
-
capabilities: import_zod.z.array(import_zod.z.string()).default([]),
|
|
90
|
-
enabled: import_zod.z.boolean().default(true),
|
|
91
|
-
operator_priority: import_zod.z.number().int().default(0),
|
|
92
|
-
policy_tags: import_zod.z.array(import_zod.z.string()).default([]),
|
|
93
|
-
retry_budget: import_zod.z.number().int().positive().optional(),
|
|
94
|
-
failover_budget: import_zod.z.number().int().positive().optional(),
|
|
95
|
-
backoff: BackoffConfigSchema.optional(),
|
|
96
|
-
concurrency_limit: import_zod.z.number().int().positive().optional(),
|
|
97
|
-
circuit_breaker: CircuitBreakerConfigSchema.optional()
|
|
98
|
-
});
|
|
99
|
-
var SwitcherConfigSchema = import_zod.z.object({
|
|
100
|
-
targets: import_zod.z.array(TargetConfigSchema).min(1).optional(),
|
|
101
|
-
retry: import_zod.z.number().int().positive().optional(),
|
|
102
|
-
timeout: import_zod.z.number().positive().optional(),
|
|
103
|
-
retry_budget: import_zod.z.number().int().positive().default(DEFAULT_RETRY_BUDGET),
|
|
104
|
-
failover_budget: import_zod.z.number().int().positive().default(DEFAULT_FAILOVER_BUDGET),
|
|
105
|
-
backoff: BackoffConfigSchema.default({
|
|
106
|
-
base_ms: DEFAULT_BACKOFF_BASE_MS,
|
|
107
|
-
multiplier: DEFAULT_BACKOFF_MULTIPLIER,
|
|
108
|
-
max_ms: DEFAULT_BACKOFF_MAX_MS,
|
|
109
|
-
jitter: DEFAULT_BACKOFF_JITTER
|
|
110
|
-
}),
|
|
111
|
-
deployment_mode_hint: import_zod.z.enum(["plugin-only", "server-companion", "sdk-control", "auto"]).default("auto"),
|
|
112
|
-
routing_weights: RoutingWeightsSchema.default({
|
|
113
|
-
health: DEFAULT_ROUTING_WEIGHT_HEALTH,
|
|
114
|
-
latency: DEFAULT_ROUTING_WEIGHT_LATENCY,
|
|
115
|
-
failure: DEFAULT_ROUTING_WEIGHT_FAILURE,
|
|
116
|
-
priority: DEFAULT_ROUTING_WEIGHT_PRIORITY
|
|
117
|
-
}),
|
|
118
|
-
queue_limit: import_zod.z.number().int().positive().default(DEFAULT_QUEUE_LIMIT),
|
|
119
|
-
concurrency_limit: import_zod.z.number().int().positive().default(DEFAULT_GLOBAL_CONCURRENCY_LIMIT),
|
|
120
|
-
backpressure_threshold: import_zod.z.number().int().nonnegative().default(DEFAULT_BACKPRESSURE_THRESHOLD),
|
|
121
|
-
circuit_breaker: CircuitBreakerConfigSchema.default({
|
|
122
|
-
failure_threshold: DEFAULT_CB_FAILURE_THRESHOLD,
|
|
123
|
-
failure_rate_threshold: DEFAULT_CB_FAILURE_RATE_THRESHOLD,
|
|
124
|
-
sliding_window_size: DEFAULT_CB_SLIDING_WINDOW_SIZE,
|
|
125
|
-
half_open_after_ms: DEFAULT_CB_HALF_OPEN_AFTER_MS,
|
|
126
|
-
half_open_max_probes: DEFAULT_CB_HALF_OPEN_MAX_PROBES,
|
|
127
|
-
success_threshold: DEFAULT_CB_SUCCESS_THRESHOLD
|
|
128
|
-
}),
|
|
129
|
-
max_expected_latency_ms: import_zod.z.number().positive().default(DEFAULT_MAX_EXPECTED_LATENCY_MS)
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
// src/config/loader.ts
|
|
133
|
-
var ConfigValidationError = class extends Error {
|
|
134
|
-
diagnostics;
|
|
135
|
-
constructor(diagnostics) {
|
|
136
|
-
const summary = diagnostics.map((d) => ` ${d.path}: ${d.message}`).join("\n");
|
|
137
|
-
super(`Invalid O-Switcher configuration:
|
|
138
|
-
${summary}`);
|
|
139
|
-
this.name = "ConfigValidationError";
|
|
140
|
-
this.diagnostics = diagnostics;
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
var validateConfig = (raw) => {
|
|
144
|
-
const result = SwitcherConfigSchema.safeParse(raw);
|
|
145
|
-
if (result.success) {
|
|
146
|
-
const data = { ...result.data };
|
|
147
|
-
if (data.retry !== void 0 && data.retry_budget === DEFAULT_RETRY_BUDGET) {
|
|
148
|
-
data.retry_budget = data.retry;
|
|
149
|
-
}
|
|
150
|
-
if (data.timeout !== void 0 && data.max_expected_latency_ms === DEFAULT_MAX_EXPECTED_LATENCY_MS) {
|
|
151
|
-
data.max_expected_latency_ms = data.timeout;
|
|
152
|
-
}
|
|
153
|
-
if (data.targets !== void 0) {
|
|
154
|
-
const seen = /* @__PURE__ */ new Set();
|
|
155
|
-
const duplicateDiagnostics = [];
|
|
156
|
-
for (const target of data.targets) {
|
|
157
|
-
const key = `${target.provider_id}::${target.profile ?? "__default__"}`;
|
|
158
|
-
if (seen.has(key)) {
|
|
159
|
-
duplicateDiagnostics.push({
|
|
160
|
-
path: "targets",
|
|
161
|
-
message: `Duplicate provider_id + profile combination: ${key}`,
|
|
162
|
-
received: key
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
seen.add(key);
|
|
166
|
-
}
|
|
167
|
-
if (duplicateDiagnostics.length > 0) {
|
|
168
|
-
throw new ConfigValidationError(duplicateDiagnostics);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return Object.freeze(data);
|
|
172
|
-
}
|
|
173
|
-
const diagnostics = result.error.issues.map(
|
|
174
|
-
(issue) => ({
|
|
175
|
-
path: issue.path.join("."),
|
|
176
|
-
message: issue.message,
|
|
177
|
-
received: "expected" in issue ? issue.expected : void 0
|
|
178
|
-
})
|
|
179
|
-
);
|
|
180
|
-
throw new ConfigValidationError(diagnostics);
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
// src/config/discovery.ts
|
|
184
|
-
var discoverTargets = (providerConfig) => {
|
|
185
|
-
const targets = [];
|
|
186
|
-
for (const [key, value] of Object.entries(providerConfig)) {
|
|
187
|
-
if (!value) continue;
|
|
188
|
-
const raw = {
|
|
189
|
-
target_id: key,
|
|
190
|
-
provider_id: value.id ?? key,
|
|
191
|
-
capabilities: ["chat"],
|
|
192
|
-
enabled: true,
|
|
193
|
-
operator_priority: 0,
|
|
194
|
-
policy_tags: []
|
|
195
|
-
};
|
|
196
|
-
const parsed = TargetConfigSchema.parse(raw);
|
|
197
|
-
targets.push(parsed);
|
|
198
|
-
}
|
|
199
|
-
return targets;
|
|
200
|
-
};
|
|
201
|
-
var discoverTargetsFromProfiles = (store) => {
|
|
202
|
-
const targets = [];
|
|
203
|
-
for (const entry of Object.values(store)) {
|
|
204
|
-
if (!entry) continue;
|
|
205
|
-
const raw = {
|
|
206
|
-
target_id: entry.id,
|
|
207
|
-
provider_id: entry.provider,
|
|
208
|
-
profile: entry.id,
|
|
209
|
-
capabilities: ["chat"],
|
|
210
|
-
enabled: true,
|
|
211
|
-
operator_priority: 0,
|
|
212
|
-
policy_tags: []
|
|
213
|
-
};
|
|
214
|
-
const parsed = TargetConfigSchema.parse(raw);
|
|
215
|
-
targets.push(parsed);
|
|
216
|
-
}
|
|
217
|
-
return targets;
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
// src/registry/health.ts
|
|
221
|
-
var INITIAL_HEALTH_SCORE = 1;
|
|
222
|
-
var DEFAULT_ALPHA = 0.1;
|
|
223
|
-
var updateHealthScore = (currentScore, observation, alpha = DEFAULT_ALPHA) => alpha * observation + (1 - alpha) * currentScore;
|
|
224
|
-
var updateLatencyEma = (currentEma, observedMs, alpha = DEFAULT_ALPHA) => alpha * observedMs + (1 - alpha) * currentEma;
|
|
225
|
-
|
|
226
|
-
// src/registry/registry.ts
|
|
227
|
-
var TargetRegistry = class {
|
|
228
|
-
targets;
|
|
229
|
-
constructor(config) {
|
|
230
|
-
this.targets = new Map(
|
|
231
|
-
(config.targets ?? []).map((t) => [
|
|
232
|
-
t.target_id,
|
|
233
|
-
{
|
|
234
|
-
target_id: t.target_id,
|
|
235
|
-
provider_id: t.provider_id,
|
|
236
|
-
profile: t.profile,
|
|
237
|
-
endpoint_id: t.endpoint_id,
|
|
238
|
-
capabilities: [...t.capabilities],
|
|
239
|
-
enabled: t.enabled,
|
|
240
|
-
state: "Active",
|
|
241
|
-
health_score: INITIAL_HEALTH_SCORE,
|
|
242
|
-
cooldown_until: null,
|
|
243
|
-
latency_ema_ms: 0,
|
|
244
|
-
failure_score: 0,
|
|
245
|
-
operator_priority: t.operator_priority,
|
|
246
|
-
policy_tags: [...t.policy_tags]
|
|
247
|
-
}
|
|
248
|
-
])
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
/** Returns the target entry for the given id, or undefined if not found. */
|
|
252
|
-
getTarget(id) {
|
|
253
|
-
return this.targets.get(id);
|
|
254
|
-
}
|
|
255
|
-
/** Returns a readonly array of all target entries. */
|
|
256
|
-
getAllTargets() {
|
|
257
|
-
return [...this.targets.values()];
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Updates the state of a target.
|
|
261
|
-
* @returns true if the target was found and updated, false otherwise.
|
|
262
|
-
*/
|
|
263
|
-
updateState(id, newState) {
|
|
264
|
-
const target = this.targets.get(id);
|
|
265
|
-
if (!target) {
|
|
266
|
-
return false;
|
|
267
|
-
}
|
|
268
|
-
this.targets.set(id, { ...target, state: newState });
|
|
269
|
-
return true;
|
|
270
|
-
}
|
|
271
|
-
/**
|
|
272
|
-
* Records a success (1) or failure (0) observation for a target.
|
|
273
|
-
* Updates health_score via EMA.
|
|
274
|
-
*/
|
|
275
|
-
recordObservation(id, observation) {
|
|
276
|
-
const target = this.targets.get(id);
|
|
277
|
-
if (!target) {
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
const newHealthScore = updateHealthScore(
|
|
281
|
-
target.health_score,
|
|
282
|
-
observation
|
|
283
|
-
);
|
|
284
|
-
this.targets.set(id, { ...target, health_score: newHealthScore });
|
|
285
|
-
}
|
|
286
|
-
/** Sets the cooldown_until timestamp for a target. */
|
|
287
|
-
setCooldown(id, untilMs) {
|
|
288
|
-
const target = this.targets.get(id);
|
|
289
|
-
if (!target) {
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
this.targets.set(id, { ...target, cooldown_until: untilMs });
|
|
293
|
-
}
|
|
294
|
-
/** Updates the latency EMA for a target. */
|
|
295
|
-
updateLatency(id, ms) {
|
|
296
|
-
const target = this.targets.get(id);
|
|
297
|
-
if (!target) {
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
const newLatency = updateLatencyEma(target.latency_ema_ms, ms);
|
|
301
|
-
this.targets.set(id, { ...target, latency_ema_ms: newLatency });
|
|
302
|
-
}
|
|
303
|
-
/**
|
|
304
|
-
* Adds a new target entry to the registry.
|
|
305
|
-
* @returns true if added, false if target_id already exists.
|
|
306
|
-
*/
|
|
307
|
-
addTarget(entry) {
|
|
308
|
-
if (this.targets.has(entry.target_id)) {
|
|
309
|
-
return false;
|
|
310
|
-
}
|
|
311
|
-
this.targets.set(entry.target_id, entry);
|
|
312
|
-
return true;
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
* Removes a target entry from the registry.
|
|
316
|
-
* @returns true if found and removed, false if not found.
|
|
317
|
-
*/
|
|
318
|
-
removeTarget(id) {
|
|
319
|
-
return this.targets.delete(id);
|
|
320
|
-
}
|
|
321
|
-
/** Returns a deep-frozen snapshot of all targets. */
|
|
322
|
-
getSnapshot() {
|
|
323
|
-
const snapshot = {
|
|
324
|
-
targets: [...this.targets.values()].map((t) => ({ ...t }))
|
|
325
|
-
};
|
|
326
|
-
return Object.freeze(snapshot);
|
|
327
|
-
}
|
|
328
|
-
};
|
|
329
|
-
var createRegistry = (config) => new TargetRegistry(config);
|
|
1
|
+
'use strict';
|
|
330
2
|
|
|
331
|
-
|
|
332
|
-
var import_pino = __toESM(require("pino"), 1);
|
|
333
|
-
var import_node_crypto = __toESM(require("crypto"), 1);
|
|
334
|
-
var REDACT_PATHS = [
|
|
335
|
-
"api_key",
|
|
336
|
-
"token",
|
|
337
|
-
"secret",
|
|
338
|
-
"password",
|
|
339
|
-
"authorization",
|
|
340
|
-
"credential",
|
|
341
|
-
"credentials",
|
|
342
|
-
"*.api_key",
|
|
343
|
-
"*.token",
|
|
344
|
-
"*.secret",
|
|
345
|
-
"*.password",
|
|
346
|
-
"*.authorization",
|
|
347
|
-
"*.credential",
|
|
348
|
-
"*.credentials"
|
|
349
|
-
];
|
|
350
|
-
var createAuditLogger = (options) => {
|
|
351
|
-
const opts = {
|
|
352
|
-
level: options?.level ?? "info",
|
|
353
|
-
redact: {
|
|
354
|
-
paths: [...REDACT_PATHS],
|
|
355
|
-
censor: "[Redacted]"
|
|
356
|
-
}
|
|
357
|
-
};
|
|
358
|
-
if (options?.destination) {
|
|
359
|
-
return (0, import_pino.default)(opts, options.destination);
|
|
360
|
-
}
|
|
361
|
-
return (0, import_pino.default)(opts);
|
|
362
|
-
};
|
|
363
|
-
var generateCorrelationId = () => import_node_crypto.default.randomUUID();
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
364
4
|
|
|
365
|
-
|
|
366
|
-
var
|
|
367
|
-
var
|
|
5
|
+
var chunkTJ7ZGZHD_cjs = require('./chunk-TJ7ZGZHD.cjs');
|
|
6
|
+
var promises = require('fs/promises');
|
|
7
|
+
var path = require('path');
|
|
8
|
+
var os = require('os');
|
|
368
9
|
|
|
369
|
-
|
|
370
|
-
var
|
|
371
|
-
var
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
/** Serializable state for both internal breakers. */
|
|
379
|
-
get state() {
|
|
380
|
-
return {
|
|
381
|
-
consecutive: this.consecutive.state,
|
|
382
|
-
count: this.count.state
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
/** Restore state for both internal breakers. */
|
|
386
|
-
set state(value) {
|
|
387
|
-
const v = value;
|
|
388
|
-
this.consecutive.state = v.consecutive;
|
|
389
|
-
this.count.state = v.count;
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
* Record success on both breakers.
|
|
393
|
-
* ConsecutiveBreaker.success() takes no args (resets counter).
|
|
394
|
-
* CountBreaker.success() takes CircuitState.
|
|
395
|
-
*/
|
|
396
|
-
success(state) {
|
|
397
|
-
this.consecutive.success();
|
|
398
|
-
this.count.success(state);
|
|
399
|
-
}
|
|
400
|
-
/**
|
|
401
|
-
* Record failure on both breakers.
|
|
402
|
-
* Returns true if EITHER breaker trips (OR logic).
|
|
403
|
-
* ConsecutiveBreaker.failure() takes no args.
|
|
404
|
-
* CountBreaker.failure() takes CircuitState.
|
|
405
|
-
*/
|
|
406
|
-
failure(state) {
|
|
407
|
-
const consecutiveTripped = this.consecutive.failure();
|
|
408
|
-
const countTripped = this.count.failure(state);
|
|
409
|
-
return consecutiveTripped || countTripped;
|
|
410
|
-
}
|
|
10
|
+
var STATE_DIR = path.join(os.homedir(), ".local", "share", "o-switcher");
|
|
11
|
+
var STATE_FILE = path.join(STATE_DIR, "tui-state.json");
|
|
12
|
+
var STATE_TMP = path.join(STATE_DIR, "tui-state.json.tmp");
|
|
13
|
+
var writeStateAtomic = async (state) => {
|
|
14
|
+
await promises.mkdir(path.dirname(STATE_FILE), { recursive: true });
|
|
15
|
+
const json = JSON.stringify(state);
|
|
16
|
+
await promises.writeFile(STATE_TMP, json, "utf8");
|
|
17
|
+
await promises.rename(STATE_TMP, STATE_FILE);
|
|
411
18
|
};
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
]);
|
|
422
|
-
var toTargetState = (state) => COCKATIEL_TO_TARGET.get(state) ?? "Disabled";
|
|
423
|
-
var createCircuitBreaker = (targetId, config, eventBus) => {
|
|
424
|
-
const createPolicy = () => {
|
|
425
|
-
const breaker = new DualBreaker(config.failure_threshold, {
|
|
426
|
-
threshold: config.failure_rate_threshold,
|
|
427
|
-
size: config.sliding_window_size
|
|
428
|
-
});
|
|
429
|
-
const policy = (0, import_cockatiel2.circuitBreaker)(import_cockatiel2.handleAll, {
|
|
430
|
-
halfOpenAfter: config.half_open_after_ms,
|
|
431
|
-
breaker
|
|
432
|
-
});
|
|
433
|
-
return { policy, breaker };
|
|
19
|
+
var createStateWriter = (debounceMs = 500) => {
|
|
20
|
+
let pending;
|
|
21
|
+
let timer;
|
|
22
|
+
let writePromise = Promise.resolve();
|
|
23
|
+
const doWrite = () => {
|
|
24
|
+
if (!pending) return;
|
|
25
|
+
const snapshot = pending;
|
|
26
|
+
pending = void 0;
|
|
27
|
+
writePromise = writeStateAtomic(snapshot).catch(() => void 0);
|
|
434
28
|
};
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
policy.onStateChange((newState) => {
|
|
440
|
-
if (newState === import_cockatiel2.CircuitState.Open) {
|
|
441
|
-
openedAtMs = Date.now();
|
|
442
|
-
}
|
|
443
|
-
const from = toTargetState(previousState);
|
|
444
|
-
const to = toTargetState(newState);
|
|
445
|
-
previousState = newState;
|
|
446
|
-
if (from !== to && eventBus) {
|
|
447
|
-
eventBus.emit("circuit_state_change", {
|
|
448
|
-
target_id: targetId,
|
|
449
|
-
from,
|
|
450
|
-
to,
|
|
451
|
-
reason: `state_transition`
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
});
|
|
29
|
+
const write = (state) => {
|
|
30
|
+
pending = state;
|
|
31
|
+
if (timer) clearTimeout(timer);
|
|
32
|
+
timer = setTimeout(doWrite, debounceMs);
|
|
455
33
|
};
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
return import_cockatiel2.CircuitState.HalfOpen;
|
|
461
|
-
}
|
|
462
|
-
return raw;
|
|
463
|
-
};
|
|
464
|
-
return {
|
|
465
|
-
state() {
|
|
466
|
-
return toTargetState(effectiveState());
|
|
467
|
-
},
|
|
468
|
-
async recordSuccess() {
|
|
469
|
-
try {
|
|
470
|
-
await current.policy.execute(async () => void 0);
|
|
471
|
-
} catch (err) {
|
|
472
|
-
if (err instanceof import_cockatiel2.BrokenCircuitError || err instanceof import_cockatiel2.IsolatedCircuitError) {
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
throw err;
|
|
476
|
-
}
|
|
477
|
-
},
|
|
478
|
-
async recordFailure() {
|
|
479
|
-
try {
|
|
480
|
-
await current.policy.execute(async () => {
|
|
481
|
-
throw new Error("recorded-failure");
|
|
482
|
-
});
|
|
483
|
-
} catch {
|
|
484
|
-
}
|
|
485
|
-
},
|
|
486
|
-
allowRequest() {
|
|
487
|
-
return effectiveState() !== import_cockatiel2.CircuitState.Open;
|
|
488
|
-
},
|
|
489
|
-
reset() {
|
|
490
|
-
current = createPolicy();
|
|
491
|
-
previousState = import_cockatiel2.CircuitState.Closed;
|
|
492
|
-
openedAtMs = 0;
|
|
493
|
-
subscribeEvents(current.policy);
|
|
494
|
-
}
|
|
495
|
-
};
|
|
496
|
-
};
|
|
497
|
-
|
|
498
|
-
// src/routing/concurrency-tracker.ts
|
|
499
|
-
var createConcurrencyTracker = (defaultLimit) => {
|
|
500
|
-
const state = /* @__PURE__ */ new Map();
|
|
501
|
-
const getOrCreate = (target_id) => {
|
|
502
|
-
const existing = state.get(target_id);
|
|
503
|
-
if (existing) return existing;
|
|
504
|
-
const entry = { active: 0, limit: defaultLimit };
|
|
505
|
-
state.set(target_id, entry);
|
|
506
|
-
return entry;
|
|
507
|
-
};
|
|
508
|
-
return {
|
|
509
|
-
acquire(target_id) {
|
|
510
|
-
const entry = getOrCreate(target_id);
|
|
511
|
-
if (entry.active >= entry.limit) return false;
|
|
512
|
-
entry.active += 1;
|
|
513
|
-
return true;
|
|
514
|
-
},
|
|
515
|
-
release(target_id) {
|
|
516
|
-
const entry = state.get(target_id);
|
|
517
|
-
if (!entry || entry.active <= 0) return;
|
|
518
|
-
entry.active -= 1;
|
|
519
|
-
},
|
|
520
|
-
headroom(target_id) {
|
|
521
|
-
const entry = getOrCreate(target_id);
|
|
522
|
-
return Math.max(0, entry.limit - entry.active);
|
|
523
|
-
},
|
|
524
|
-
active(target_id) {
|
|
525
|
-
const entry = state.get(target_id);
|
|
526
|
-
return entry ? entry.active : 0;
|
|
527
|
-
},
|
|
528
|
-
setLimit(target_id, limit) {
|
|
529
|
-
const entry = getOrCreate(target_id);
|
|
530
|
-
entry.limit = limit;
|
|
34
|
+
const flush = async () => {
|
|
35
|
+
if (timer) {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
timer = void 0;
|
|
531
38
|
}
|
|
39
|
+
doWrite();
|
|
40
|
+
await writePromise;
|
|
532
41
|
};
|
|
42
|
+
return { write, flush };
|
|
533
43
|
};
|
|
534
44
|
|
|
535
|
-
// src/routing/cooldown.ts
|
|
536
|
-
var createCooldownManager = (registry, eventBus) => ({
|
|
537
|
-
setCooldown(target_id, durationMs, errorClass) {
|
|
538
|
-
const untilMs = Date.now() + durationMs;
|
|
539
|
-
registry.setCooldown(target_id, untilMs);
|
|
540
|
-
registry.updateState(target_id, "CoolingDown");
|
|
541
|
-
eventBus?.emit("cooldown_set", {
|
|
542
|
-
target_id,
|
|
543
|
-
until_ms: untilMs,
|
|
544
|
-
error_class: errorClass
|
|
545
|
-
});
|
|
546
|
-
},
|
|
547
|
-
checkExpired(nowMs) {
|
|
548
|
-
const expired = [];
|
|
549
|
-
for (const target of registry.getAllTargets()) {
|
|
550
|
-
if (target.state === "CoolingDown" && target.cooldown_until !== null && target.cooldown_until <= nowMs) {
|
|
551
|
-
registry.setCooldown(target.target_id, 0);
|
|
552
|
-
registry.updateState(target.target_id, "Active");
|
|
553
|
-
eventBus?.emit("cooldown_expired", { target_id: target.target_id });
|
|
554
|
-
expired.push(target.target_id);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
return expired;
|
|
558
|
-
},
|
|
559
|
-
isInCooldown(target_id, nowMs) {
|
|
560
|
-
const target = registry.getTarget(target_id);
|
|
561
|
-
if (!target) {
|
|
562
|
-
return false;
|
|
563
|
-
}
|
|
564
|
-
return target.cooldown_until !== null && target.cooldown_until > nowMs;
|
|
565
|
-
}
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
// src/routing/admission.ts
|
|
569
|
-
var import_p_queue = __toESM(require("p-queue"), 1);
|
|
570
|
-
|
|
571
|
-
// src/routing/log-subscriber.ts
|
|
572
|
-
var createLogSubscriber = (eventBus, logger) => {
|
|
573
|
-
const log = logger.child({ component: "routing" });
|
|
574
|
-
const onCircuitStateChange = (payload) => {
|
|
575
|
-
log.info(
|
|
576
|
-
{
|
|
577
|
-
event: "circuit_state_change",
|
|
578
|
-
target_id: payload.target_id,
|
|
579
|
-
from: payload.from,
|
|
580
|
-
to: payload.to,
|
|
581
|
-
reason: payload.reason
|
|
582
|
-
},
|
|
583
|
-
"Circuit breaker transition"
|
|
584
|
-
);
|
|
585
|
-
};
|
|
586
|
-
const onCooldownSet = (payload) => {
|
|
587
|
-
log.info(
|
|
588
|
-
{
|
|
589
|
-
event: "cooldown_set",
|
|
590
|
-
target_id: payload.target_id,
|
|
591
|
-
duration_ms: payload.until_ms - Date.now(),
|
|
592
|
-
error_class: payload.error_class
|
|
593
|
-
},
|
|
594
|
-
"Cooldown set"
|
|
595
|
-
);
|
|
596
|
-
};
|
|
597
|
-
const onCooldownExpired = (payload) => {
|
|
598
|
-
log.info(
|
|
599
|
-
{
|
|
600
|
-
event: "cooldown_expired",
|
|
601
|
-
target_id: payload.target_id
|
|
602
|
-
},
|
|
603
|
-
"Cooldown expired"
|
|
604
|
-
);
|
|
605
|
-
};
|
|
606
|
-
const onAdmissionDecision = (payload) => {
|
|
607
|
-
const fields = {
|
|
608
|
-
event: "admission_decision",
|
|
609
|
-
request_id: payload.request_id,
|
|
610
|
-
result: payload.result,
|
|
611
|
-
reason: payload.reason
|
|
612
|
-
};
|
|
613
|
-
if (payload.result === "admitted" || payload.result === "queued") {
|
|
614
|
-
log.info(fields, "Admission decision");
|
|
615
|
-
} else {
|
|
616
|
-
log.warn(fields, "Admission decision");
|
|
617
|
-
}
|
|
618
|
-
};
|
|
619
|
-
const onHealthUpdated = (payload) => {
|
|
620
|
-
log.debug(
|
|
621
|
-
{
|
|
622
|
-
event: "health_updated",
|
|
623
|
-
target_id: payload.target_id,
|
|
624
|
-
old_score: payload.old_score,
|
|
625
|
-
new_score: payload.new_score
|
|
626
|
-
},
|
|
627
|
-
"Health score updated"
|
|
628
|
-
);
|
|
629
|
-
};
|
|
630
|
-
const onTargetExcluded = (payload) => {
|
|
631
|
-
log.debug(
|
|
632
|
-
{
|
|
633
|
-
event: "target_excluded",
|
|
634
|
-
target_id: payload.target_id,
|
|
635
|
-
reason: payload.reason
|
|
636
|
-
},
|
|
637
|
-
"Target excluded"
|
|
638
|
-
);
|
|
639
|
-
};
|
|
640
|
-
eventBus.on("circuit_state_change", onCircuitStateChange);
|
|
641
|
-
eventBus.on("cooldown_set", onCooldownSet);
|
|
642
|
-
eventBus.on("cooldown_expired", onCooldownExpired);
|
|
643
|
-
eventBus.on("admission_decision", onAdmissionDecision);
|
|
644
|
-
eventBus.on("health_updated", onHealthUpdated);
|
|
645
|
-
eventBus.on("target_excluded", onTargetExcluded);
|
|
646
|
-
return () => {
|
|
647
|
-
eventBus.off("circuit_state_change", onCircuitStateChange);
|
|
648
|
-
eventBus.off("cooldown_set", onCooldownSet);
|
|
649
|
-
eventBus.off("cooldown_expired", onCooldownExpired);
|
|
650
|
-
eventBus.off("admission_decision", onAdmissionDecision);
|
|
651
|
-
eventBus.off("health_updated", onHealthUpdated);
|
|
652
|
-
eventBus.off("target_excluded", onTargetExcluded);
|
|
653
|
-
};
|
|
654
|
-
};
|
|
655
|
-
|
|
656
|
-
// src/errors/taxonomy.ts
|
|
657
|
-
var import_zod2 = require("zod");
|
|
658
|
-
var RateLimitedSchema = import_zod2.z.object({
|
|
659
|
-
class: import_zod2.z.literal("RateLimited"),
|
|
660
|
-
retryable: import_zod2.z.literal(true),
|
|
661
|
-
retry_after_ms: import_zod2.z.number().optional(),
|
|
662
|
-
provider_reason: import_zod2.z.string().optional()
|
|
663
|
-
});
|
|
664
|
-
var QuotaExhaustedSchema = import_zod2.z.object({
|
|
665
|
-
class: import_zod2.z.literal("QuotaExhausted"),
|
|
666
|
-
retryable: import_zod2.z.literal(false),
|
|
667
|
-
provider_reason: import_zod2.z.string().optional()
|
|
668
|
-
});
|
|
669
|
-
var AuthFailureSchema = import_zod2.z.object({
|
|
670
|
-
class: import_zod2.z.literal("AuthFailure"),
|
|
671
|
-
retryable: import_zod2.z.literal(false),
|
|
672
|
-
recovery_attempted: import_zod2.z.boolean().default(false)
|
|
673
|
-
});
|
|
674
|
-
var PermissionFailureSchema = import_zod2.z.object({
|
|
675
|
-
class: import_zod2.z.literal("PermissionFailure"),
|
|
676
|
-
retryable: import_zod2.z.literal(false)
|
|
677
|
-
});
|
|
678
|
-
var PolicyFailureSchema = import_zod2.z.object({
|
|
679
|
-
class: import_zod2.z.literal("PolicyFailure"),
|
|
680
|
-
retryable: import_zod2.z.literal(false)
|
|
681
|
-
});
|
|
682
|
-
var RegionRestrictionSchema = import_zod2.z.object({
|
|
683
|
-
class: import_zod2.z.literal("RegionRestriction"),
|
|
684
|
-
retryable: import_zod2.z.literal(false)
|
|
685
|
-
});
|
|
686
|
-
var ModelUnavailableSchema = import_zod2.z.object({
|
|
687
|
-
class: import_zod2.z.literal("ModelUnavailable"),
|
|
688
|
-
retryable: import_zod2.z.literal(false),
|
|
689
|
-
failover_eligible: import_zod2.z.literal(true)
|
|
690
|
-
});
|
|
691
|
-
var TransientServerFailureSchema = import_zod2.z.object({
|
|
692
|
-
class: import_zod2.z.literal("TransientServerFailure"),
|
|
693
|
-
retryable: import_zod2.z.literal(true),
|
|
694
|
-
http_status: import_zod2.z.number().optional()
|
|
695
|
-
});
|
|
696
|
-
var TransportFailureSchema = import_zod2.z.object({
|
|
697
|
-
class: import_zod2.z.literal("TransportFailure"),
|
|
698
|
-
retryable: import_zod2.z.literal(true)
|
|
699
|
-
});
|
|
700
|
-
var InterruptedExecutionSchema = import_zod2.z.object({
|
|
701
|
-
class: import_zod2.z.literal("InterruptedExecution"),
|
|
702
|
-
retryable: import_zod2.z.literal(true),
|
|
703
|
-
partial_output_bytes: import_zod2.z.number().optional()
|
|
704
|
-
});
|
|
705
|
-
var ErrorClassSchema = import_zod2.z.discriminatedUnion("class", [
|
|
706
|
-
RateLimitedSchema,
|
|
707
|
-
QuotaExhaustedSchema,
|
|
708
|
-
AuthFailureSchema,
|
|
709
|
-
PermissionFailureSchema,
|
|
710
|
-
PolicyFailureSchema,
|
|
711
|
-
RegionRestrictionSchema,
|
|
712
|
-
ModelUnavailableSchema,
|
|
713
|
-
TransientServerFailureSchema,
|
|
714
|
-
TransportFailureSchema,
|
|
715
|
-
InterruptedExecutionSchema
|
|
716
|
-
]);
|
|
717
|
-
|
|
718
|
-
// src/operator/plugin-tools.ts
|
|
719
|
-
var import_tool = require("@opencode-ai/plugin/tool");
|
|
720
|
-
|
|
721
|
-
// src/operator/commands.ts
|
|
722
|
-
var createRequestTraceBuffer = (capacity) => {
|
|
723
|
-
const entries = /* @__PURE__ */ new Map();
|
|
724
|
-
const order = [];
|
|
725
|
-
return {
|
|
726
|
-
record(entry) {
|
|
727
|
-
if (entries.has(entry.request_id)) {
|
|
728
|
-
const idx = order.indexOf(entry.request_id);
|
|
729
|
-
if (idx !== -1) {
|
|
730
|
-
order.splice(idx, 1);
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
while (order.length >= capacity) {
|
|
734
|
-
const oldest = order.shift();
|
|
735
|
-
if (oldest !== void 0) {
|
|
736
|
-
entries.delete(oldest);
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
entries.set(entry.request_id, entry);
|
|
740
|
-
order.push(entry.request_id);
|
|
741
|
-
},
|
|
742
|
-
lookup(requestId) {
|
|
743
|
-
return entries.get(requestId);
|
|
744
|
-
},
|
|
745
|
-
size() {
|
|
746
|
-
return entries.size;
|
|
747
|
-
}
|
|
748
|
-
};
|
|
749
|
-
};
|
|
750
|
-
|
|
751
|
-
// src/operator/plugin-tools.ts
|
|
752
|
-
var { schema: z3 } = import_tool.tool;
|
|
753
|
-
|
|
754
|
-
// src/profiles/store.ts
|
|
755
|
-
var import_promises = require("fs/promises");
|
|
756
|
-
var import_node_os = require("os");
|
|
757
|
-
var import_node_path = require("path");
|
|
758
|
-
var PROFILES_DIR = (0, import_node_path.join)((0, import_node_os.homedir)(), ".local", "share", "o-switcher");
|
|
759
|
-
var PROFILES_PATH = (0, import_node_path.join)(PROFILES_DIR, "profiles.json");
|
|
760
|
-
var loadProfiles = async (path = PROFILES_PATH) => {
|
|
761
|
-
try {
|
|
762
|
-
const content = await (0, import_promises.readFile)(path, "utf-8");
|
|
763
|
-
return JSON.parse(content);
|
|
764
|
-
} catch (err) {
|
|
765
|
-
const code = err.code;
|
|
766
|
-
if (code === "ENOENT") {
|
|
767
|
-
return {};
|
|
768
|
-
}
|
|
769
|
-
throw err;
|
|
770
|
-
}
|
|
771
|
-
};
|
|
772
|
-
var saveProfiles = async (store, path = PROFILES_PATH, logger) => {
|
|
773
|
-
const dir = (0, import_node_path.dirname)(path);
|
|
774
|
-
await (0, import_promises.mkdir)(dir, { recursive: true });
|
|
775
|
-
const tmpPath = `${path}.tmp`;
|
|
776
|
-
await (0, import_promises.writeFile)(tmpPath, JSON.stringify(store, null, 2), "utf-8");
|
|
777
|
-
await (0, import_promises.rename)(tmpPath, path);
|
|
778
|
-
logger?.info({ path }, "Profiles saved to disk");
|
|
779
|
-
};
|
|
780
|
-
var credentialsMatch = (a, b) => {
|
|
781
|
-
if (a.type !== b.type) return false;
|
|
782
|
-
if (a.type === "api-key" && b.type === "api-key") {
|
|
783
|
-
return a.key === b.key;
|
|
784
|
-
}
|
|
785
|
-
if (a.type === "oauth" && b.type === "oauth") {
|
|
786
|
-
return a.refresh === b.refresh && a.access === b.access && a.expires === b.expires && a.accountId === b.accountId;
|
|
787
|
-
}
|
|
788
|
-
return false;
|
|
789
|
-
};
|
|
790
|
-
var addProfile = (store, provider, credentials) => {
|
|
791
|
-
const isDuplicate = Object.values(store).some(
|
|
792
|
-
(entry2) => entry2.provider === provider && credentialsMatch(entry2.credentials, credentials)
|
|
793
|
-
);
|
|
794
|
-
if (isDuplicate) {
|
|
795
|
-
return store;
|
|
796
|
-
}
|
|
797
|
-
const id = nextProfileId(store, provider);
|
|
798
|
-
const entry = {
|
|
799
|
-
id,
|
|
800
|
-
provider,
|
|
801
|
-
type: credentials.type,
|
|
802
|
-
credentials,
|
|
803
|
-
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
804
|
-
};
|
|
805
|
-
return { ...store, [id]: entry };
|
|
806
|
-
};
|
|
807
|
-
var removeProfile = (store, id) => {
|
|
808
|
-
if (store[id] === void 0) {
|
|
809
|
-
return { store, removed: false };
|
|
810
|
-
}
|
|
811
|
-
const { [id]: _removed, ...rest } = store;
|
|
812
|
-
return { store: rest, removed: true };
|
|
813
|
-
};
|
|
814
|
-
var listProfiles = (store) => {
|
|
815
|
-
return Object.values(store).sort(
|
|
816
|
-
(a, b) => new Date(a.created).getTime() - new Date(b.created).getTime()
|
|
817
|
-
);
|
|
818
|
-
};
|
|
819
|
-
var nextProfileId = (store, provider) => {
|
|
820
|
-
const prefix = `${provider}-`;
|
|
821
|
-
const maxN = Object.keys(store).filter((key) => key.startsWith(prefix)).map((key) => Number(key.slice(prefix.length))).filter((n) => !Number.isNaN(n)).reduce((max, n) => Math.max(max, n), 0);
|
|
822
|
-
return `${provider}-${maxN + 1}`;
|
|
823
|
-
};
|
|
824
|
-
|
|
825
|
-
// src/profiles/watcher.ts
|
|
826
|
-
var import_promises2 = require("fs/promises");
|
|
827
|
-
var import_node_os2 = require("os");
|
|
828
|
-
var import_node_path2 = require("path");
|
|
829
|
-
var AUTH_JSON_PATH = (0, import_node_path2.join)(
|
|
830
|
-
(0, import_node_os2.homedir)(),
|
|
831
|
-
".local",
|
|
832
|
-
"share",
|
|
833
|
-
"opencode",
|
|
834
|
-
"auth.json"
|
|
835
|
-
);
|
|
836
|
-
var DEBOUNCE_MS = 100;
|
|
837
|
-
var readAuthJson = async (path) => {
|
|
838
|
-
try {
|
|
839
|
-
const content = await (0, import_promises2.readFile)(path, "utf-8");
|
|
840
|
-
return JSON.parse(content);
|
|
841
|
-
} catch {
|
|
842
|
-
return {};
|
|
843
|
-
}
|
|
844
|
-
};
|
|
845
|
-
var toCredential = (entry) => {
|
|
846
|
-
if (entry.type === "oauth" || entry.refresh !== void 0 && entry.access !== void 0) {
|
|
847
|
-
return {
|
|
848
|
-
type: "oauth",
|
|
849
|
-
refresh: String(entry.refresh ?? ""),
|
|
850
|
-
access: String(entry.access ?? ""),
|
|
851
|
-
expires: Number(entry.expires ?? 0),
|
|
852
|
-
accountId: entry.accountId !== void 0 ? String(entry.accountId) : void 0
|
|
853
|
-
};
|
|
854
|
-
}
|
|
855
|
-
return {
|
|
856
|
-
type: "api-key",
|
|
857
|
-
key: String(entry.key ?? "")
|
|
858
|
-
};
|
|
859
|
-
};
|
|
860
|
-
var entriesEqual = (a, b) => {
|
|
861
|
-
if (a === void 0 && b === void 0) return true;
|
|
862
|
-
if (a === void 0 || b === void 0) return false;
|
|
863
|
-
return JSON.stringify(a) === JSON.stringify(b);
|
|
864
|
-
};
|
|
865
|
-
var createAuthWatcher = (options) => {
|
|
866
|
-
const authPath = options?.authJsonPath ?? AUTH_JSON_PATH;
|
|
867
|
-
const profPath = options?.profilesPath ?? PROFILES_PATH;
|
|
868
|
-
const log = options?.logger?.child({ component: "profile-watcher" });
|
|
869
|
-
let lastKnownAuth = {};
|
|
870
|
-
let abortController = null;
|
|
871
|
-
let debounceTimer = null;
|
|
872
|
-
let watchPromise = null;
|
|
873
|
-
const processChange = async () => {
|
|
874
|
-
const newAuth = await readAuthJson(authPath);
|
|
875
|
-
let store = await loadProfiles(profPath);
|
|
876
|
-
let changed = false;
|
|
877
|
-
for (const [provider, entry] of Object.entries(newAuth)) {
|
|
878
|
-
if (!entry) continue;
|
|
879
|
-
const previousEntry = lastKnownAuth[provider];
|
|
880
|
-
if (previousEntry !== void 0 && !entriesEqual(previousEntry, entry)) {
|
|
881
|
-
log?.info({ provider, action: "credential_changed" }, "Credential change detected \u2014 saving previous credential");
|
|
882
|
-
const prevCredential = toCredential(previousEntry);
|
|
883
|
-
const newStore = addProfile(store, provider, prevCredential);
|
|
884
|
-
if (newStore !== store) {
|
|
885
|
-
store = newStore;
|
|
886
|
-
changed = true;
|
|
887
|
-
}
|
|
888
|
-
} else if (previousEntry === void 0) {
|
|
889
|
-
log?.info({ provider, action: "new_provider" }, "New provider detected");
|
|
890
|
-
const credential = toCredential(entry);
|
|
891
|
-
const newStore = addProfile(store, provider, credential);
|
|
892
|
-
if (newStore !== store) {
|
|
893
|
-
store = newStore;
|
|
894
|
-
changed = true;
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
if (changed) {
|
|
899
|
-
await saveProfiles(store, profPath);
|
|
900
|
-
log?.info({ profiles_saved: true }, "Profiles saved to disk");
|
|
901
|
-
}
|
|
902
|
-
lastKnownAuth = newAuth;
|
|
903
|
-
};
|
|
904
|
-
const onFileChange = () => {
|
|
905
|
-
if (debounceTimer !== null) {
|
|
906
|
-
clearTimeout(debounceTimer);
|
|
907
|
-
}
|
|
908
|
-
debounceTimer = setTimeout(() => {
|
|
909
|
-
debounceTimer = null;
|
|
910
|
-
processChange().catch((err) => {
|
|
911
|
-
log?.warn({ err }, "Error processing auth change");
|
|
912
|
-
});
|
|
913
|
-
}, DEBOUNCE_MS);
|
|
914
|
-
};
|
|
915
|
-
const start = async () => {
|
|
916
|
-
const currentAuth = await readAuthJson(authPath);
|
|
917
|
-
const currentStore = await loadProfiles(profPath);
|
|
918
|
-
log?.info({ providers: Object.keys(currentAuth) }, "Auth watcher started");
|
|
919
|
-
if (Object.keys(currentStore).length === 0 && Object.keys(currentAuth).length > 0) {
|
|
920
|
-
let store = {};
|
|
921
|
-
for (const [provider, entry] of Object.entries(currentAuth)) {
|
|
922
|
-
if (!entry) continue;
|
|
923
|
-
const credential = toCredential(entry);
|
|
924
|
-
store = addProfile(store, provider, credential);
|
|
925
|
-
}
|
|
926
|
-
await saveProfiles(store, profPath);
|
|
927
|
-
log?.info({ profiles_initialized: Object.keys(store).length }, "Initialized profiles from auth.json");
|
|
928
|
-
}
|
|
929
|
-
lastKnownAuth = currentAuth;
|
|
930
|
-
abortController = new AbortController();
|
|
931
|
-
const parentDir = (0, import_node_path2.dirname)(authPath);
|
|
932
|
-
watchPromise = (async () => {
|
|
933
|
-
try {
|
|
934
|
-
const watcher = (0, import_promises2.watch)(parentDir, {
|
|
935
|
-
signal: abortController.signal
|
|
936
|
-
});
|
|
937
|
-
for await (const event of watcher) {
|
|
938
|
-
if (event.filename === "auth.json" || event.filename === null) {
|
|
939
|
-
onFileChange();
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
} catch (err) {
|
|
943
|
-
const name = err.name;
|
|
944
|
-
if (name !== "AbortError") {
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
})();
|
|
948
|
-
};
|
|
949
|
-
const stop = () => {
|
|
950
|
-
if (debounceTimer !== null) {
|
|
951
|
-
clearTimeout(debounceTimer);
|
|
952
|
-
debounceTimer = null;
|
|
953
|
-
}
|
|
954
|
-
if (abortController !== null) {
|
|
955
|
-
abortController.abort();
|
|
956
|
-
abortController = null;
|
|
957
|
-
}
|
|
958
|
-
watchPromise = null;
|
|
959
|
-
log?.info("Auth watcher stopped");
|
|
960
|
-
};
|
|
961
|
-
return { start, stop };
|
|
962
|
-
};
|
|
963
|
-
|
|
964
|
-
// src/profiles/tools.ts
|
|
965
|
-
var import_tool2 = require("@opencode-ai/plugin/tool");
|
|
966
|
-
var { schema: z4 } = import_tool2.tool;
|
|
967
|
-
var createProfileTools = (options) => ({
|
|
968
|
-
profilesList: (0, import_tool2.tool)({
|
|
969
|
-
description: "List all saved auth profiles with provider, type, and creation date.",
|
|
970
|
-
args: {},
|
|
971
|
-
async execute() {
|
|
972
|
-
const store = await loadProfiles(options?.profilesPath);
|
|
973
|
-
const profiles = listProfiles(store);
|
|
974
|
-
const result = profiles.map((entry) => ({
|
|
975
|
-
id: entry.id,
|
|
976
|
-
provider: entry.provider,
|
|
977
|
-
type: entry.type,
|
|
978
|
-
created: entry.created
|
|
979
|
-
}));
|
|
980
|
-
return JSON.stringify(result, null, 2);
|
|
981
|
-
}
|
|
982
|
-
}),
|
|
983
|
-
profilesRemove: (0, import_tool2.tool)({
|
|
984
|
-
description: "Remove a saved auth profile by ID.",
|
|
985
|
-
args: { id: z4.string().min(1) },
|
|
986
|
-
async execute(args) {
|
|
987
|
-
const store = await loadProfiles(options?.profilesPath);
|
|
988
|
-
const { store: newStore, removed } = removeProfile(store, args.id);
|
|
989
|
-
if (removed) {
|
|
990
|
-
await saveProfiles(newStore, options?.profilesPath);
|
|
991
|
-
return JSON.stringify({ success: true, id: args.id });
|
|
992
|
-
}
|
|
993
|
-
return JSON.stringify({
|
|
994
|
-
success: false,
|
|
995
|
-
id: args.id,
|
|
996
|
-
error: "Profile not found"
|
|
997
|
-
});
|
|
998
|
-
}
|
|
999
|
-
})
|
|
1000
|
-
});
|
|
1001
|
-
|
|
1002
45
|
// src/plugin.ts
|
|
1003
46
|
var initializeSwitcher = (rawConfig) => {
|
|
1004
|
-
const config = validateConfig(rawConfig);
|
|
1005
|
-
const registry = createRegistry(config);
|
|
1006
|
-
const logger = createAuditLogger();
|
|
1007
|
-
const eventBus = createRoutingEventBus();
|
|
47
|
+
const config = chunkTJ7ZGZHD_cjs.validateConfig(rawConfig);
|
|
48
|
+
const registry = chunkTJ7ZGZHD_cjs.createRegistry(config);
|
|
49
|
+
const logger = chunkTJ7ZGZHD_cjs.createAuditLogger();
|
|
50
|
+
const eventBus = chunkTJ7ZGZHD_cjs.createRoutingEventBus();
|
|
1008
51
|
const circuitBreakers = /* @__PURE__ */ new Map();
|
|
1009
52
|
for (const target of registry.getAllTargets()) {
|
|
1010
53
|
circuitBreakers.set(
|
|
1011
54
|
target.target_id,
|
|
1012
|
-
createCircuitBreaker(target.target_id, config.circuit_breaker, eventBus)
|
|
55
|
+
chunkTJ7ZGZHD_cjs.createCircuitBreaker(target.target_id, config.circuit_breaker, eventBus)
|
|
1013
56
|
);
|
|
1014
57
|
}
|
|
1015
|
-
const concurrency = createConcurrencyTracker(config.concurrency_limit);
|
|
1016
|
-
const cooldownManager = createCooldownManager(registry, eventBus);
|
|
1017
|
-
const traceBuffer = createRequestTraceBuffer(100);
|
|
1018
|
-
createLogSubscriber(eventBus, logger);
|
|
58
|
+
const concurrency = chunkTJ7ZGZHD_cjs.createConcurrencyTracker(config.concurrency_limit);
|
|
59
|
+
const cooldownManager = chunkTJ7ZGZHD_cjs.createCooldownManager(registry, eventBus);
|
|
60
|
+
const traceBuffer = chunkTJ7ZGZHD_cjs.createRequestTraceBuffer(100);
|
|
61
|
+
chunkTJ7ZGZHD_cjs.createLogSubscriber(eventBus, logger);
|
|
1019
62
|
const configRef = {
|
|
1020
63
|
current: () => config,
|
|
1021
64
|
swap: () => {
|
|
@@ -1030,10 +73,52 @@ var initializeSwitcher = (rawConfig) => {
|
|
|
1030
73
|
};
|
|
1031
74
|
return { config, registry, logger, circuitBreakers, concurrency, cooldownManager, operatorDeps };
|
|
1032
75
|
};
|
|
76
|
+
var snapshotState = (state) => {
|
|
77
|
+
if (!state.registry) return void 0;
|
|
78
|
+
const allTargets = state.registry.getAllTargets();
|
|
79
|
+
const activeTarget = allTargets.find((t) => t.state === "Active" && t.enabled);
|
|
80
|
+
const targets = allTargets.map((t) => ({
|
|
81
|
+
target_id: t.target_id,
|
|
82
|
+
provider_id: t.provider_id,
|
|
83
|
+
profile: t.profile,
|
|
84
|
+
state: t.state,
|
|
85
|
+
health_score: t.health_score,
|
|
86
|
+
latency_ema_ms: t.latency_ema_ms,
|
|
87
|
+
enabled: t.enabled
|
|
88
|
+
}));
|
|
89
|
+
return {
|
|
90
|
+
version: 1,
|
|
91
|
+
updated_at: Date.now(),
|
|
92
|
+
active_target_id: activeTarget?.target_id,
|
|
93
|
+
targets
|
|
94
|
+
};
|
|
95
|
+
};
|
|
1033
96
|
var server = async (_input) => {
|
|
1034
|
-
const logger = createAuditLogger({ level: "info" });
|
|
97
|
+
const logger = chunkTJ7ZGZHD_cjs.createAuditLogger({ level: "info" });
|
|
1035
98
|
logger.info("O-Switcher plugin initializing");
|
|
1036
99
|
const state = {};
|
|
100
|
+
const stateWriter = createStateWriter();
|
|
101
|
+
const publishTuiState = () => {
|
|
102
|
+
const snapshot = snapshotState(state);
|
|
103
|
+
if (snapshot) stateWriter.write(snapshot);
|
|
104
|
+
};
|
|
105
|
+
const lazyOperatorDeps = {
|
|
106
|
+
get registry() {
|
|
107
|
+
return state.operatorDeps.registry;
|
|
108
|
+
},
|
|
109
|
+
get circuitBreakers() {
|
|
110
|
+
return state.operatorDeps.circuitBreakers;
|
|
111
|
+
},
|
|
112
|
+
get configRef() {
|
|
113
|
+
return state.operatorDeps.configRef;
|
|
114
|
+
},
|
|
115
|
+
get logger() {
|
|
116
|
+
return state.operatorDeps.logger;
|
|
117
|
+
},
|
|
118
|
+
get traceBuffer() {
|
|
119
|
+
return state.operatorDeps.traceBuffer;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
1037
122
|
const hooks = {
|
|
1038
123
|
/**
|
|
1039
124
|
* Config hook: initialize O-Switcher when OpenCode config is loaded.
|
|
@@ -1052,17 +137,17 @@ var server = async (_input) => {
|
|
|
1052
137
|
rawConfig["targets"] && Array.isArray(rawConfig["targets"]) && rawConfig["targets"].length > 0
|
|
1053
138
|
);
|
|
1054
139
|
if (!hasExplicitTargets) {
|
|
1055
|
-
const profileStore = await loadProfiles().catch(() => ({}));
|
|
140
|
+
const profileStore = await chunkTJ7ZGZHD_cjs.loadProfiles().catch(() => ({}));
|
|
1056
141
|
const profileKeys = Object.keys(profileStore);
|
|
1057
142
|
if (profileKeys.length > 0) {
|
|
1058
|
-
const profileTargets = discoverTargetsFromProfiles(profileStore);
|
|
143
|
+
const profileTargets = chunkTJ7ZGZHD_cjs.discoverTargetsFromProfiles(profileStore);
|
|
1059
144
|
rawConfig["targets"] = profileTargets;
|
|
1060
145
|
logger.info(
|
|
1061
146
|
{ discovered: profileTargets.length, profiles: profileKeys },
|
|
1062
147
|
"Auto-discovered targets from O-Switcher profiles"
|
|
1063
148
|
);
|
|
1064
149
|
} else if (providerConfig && typeof providerConfig === "object") {
|
|
1065
|
-
const discoveredTargets = discoverTargets(providerConfig);
|
|
150
|
+
const discoveredTargets = chunkTJ7ZGZHD_cjs.discoverTargets(providerConfig);
|
|
1066
151
|
if (discoveredTargets.length === 0) {
|
|
1067
152
|
logger.warn("No providers found \u2014 running in passthrough mode");
|
|
1068
153
|
return;
|
|
@@ -1080,11 +165,9 @@ var server = async (_input) => {
|
|
|
1080
165
|
try {
|
|
1081
166
|
const initialized = initializeSwitcher(rawConfig);
|
|
1082
167
|
Object.assign(state, initialized);
|
|
1083
|
-
logger.info(
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
);
|
|
1087
|
-
state.authWatcher = createAuthWatcher({ logger });
|
|
168
|
+
logger.info({ targets: state.registry?.getAllTargets().length }, "O-Switcher initialized");
|
|
169
|
+
publishTuiState();
|
|
170
|
+
state.authWatcher = chunkTJ7ZGZHD_cjs.createAuthWatcher({ logger });
|
|
1088
171
|
await state.authWatcher.start();
|
|
1089
172
|
logger.info("Auth watcher started");
|
|
1090
173
|
} catch (err) {
|
|
@@ -1099,13 +182,11 @@ var server = async (_input) => {
|
|
|
1099
182
|
*/
|
|
1100
183
|
async "chat.params"(input, output) {
|
|
1101
184
|
if (!state.registry) return;
|
|
1102
|
-
const requestId = generateCorrelationId();
|
|
185
|
+
const requestId = chunkTJ7ZGZHD_cjs.generateCorrelationId();
|
|
1103
186
|
const targets = state.registry.getAllTargets();
|
|
1104
187
|
const providerId = input.provider?.info?.id ?? input.provider?.info?.name ?? input.model?.providerID ?? void 0;
|
|
1105
188
|
if (!providerId) return;
|
|
1106
|
-
const matchingTargets = targets.filter(
|
|
1107
|
-
(t) => t.provider_id === providerId && t.enabled
|
|
1108
|
-
);
|
|
189
|
+
const matchingTargets = targets.filter((t) => t.provider_id === providerId && t.enabled);
|
|
1109
190
|
if (matchingTargets.length === 0) return;
|
|
1110
191
|
const activeTarget = matchingTargets.reduce(
|
|
1111
192
|
(best, t) => t.health_score > best.health_score ? t : best
|
|
@@ -1118,7 +199,11 @@ var server = async (_input) => {
|
|
|
1118
199
|
output.maxOutputTokens = Math.min(output.maxOutputTokens, 4096);
|
|
1119
200
|
}
|
|
1120
201
|
state.logger?.info(
|
|
1121
|
-
{
|
|
202
|
+
{
|
|
203
|
+
request_id: requestId,
|
|
204
|
+
target_id: activeTarget.target_id,
|
|
205
|
+
health: activeTarget.health_score
|
|
206
|
+
},
|
|
1122
207
|
"Adjusted params for degraded target"
|
|
1123
208
|
);
|
|
1124
209
|
}
|
|
@@ -1136,21 +221,24 @@ var server = async (_input) => {
|
|
|
1136
221
|
if (error && "providerID" in error) {
|
|
1137
222
|
const providerId = error.providerID;
|
|
1138
223
|
if (providerId) {
|
|
1139
|
-
const matchingTargets = state.registry.getAllTargets().filter(
|
|
1140
|
-
(t) => t.provider_id === providerId
|
|
1141
|
-
);
|
|
224
|
+
const matchingTargets = state.registry.getAllTargets().filter((t) => t.provider_id === providerId);
|
|
1142
225
|
if (matchingTargets.length === 0) return;
|
|
1143
226
|
const target = matchingTargets.length === 1 ? matchingTargets[0] : matchingTargets.reduce(
|
|
1144
227
|
(worst, t) => t.health_score < worst.health_score ? t : worst
|
|
1145
228
|
);
|
|
1146
229
|
if (matchingTargets.length > 1) {
|
|
1147
230
|
state.logger?.warn(
|
|
1148
|
-
{
|
|
231
|
+
{
|
|
232
|
+
provider_id: providerId,
|
|
233
|
+
attributed_target: target.target_id,
|
|
234
|
+
profile_count: matchingTargets.length
|
|
235
|
+
},
|
|
1149
236
|
"Multiple profiles for provider \u2014 error attributed to worst-health target (plugin-only mode limitation)"
|
|
1150
237
|
);
|
|
1151
238
|
}
|
|
1152
239
|
state.registry.recordObservation(target.target_id, 0);
|
|
1153
240
|
state.circuitBreakers?.get(target.target_id)?.recordFailure();
|
|
241
|
+
publishTuiState();
|
|
1154
242
|
state.logger?.info(
|
|
1155
243
|
{ target_id: target.target_id, event_type: event.type },
|
|
1156
244
|
"Recorded failure from session event"
|
|
@@ -1160,7 +248,8 @@ var server = async (_input) => {
|
|
|
1160
248
|
}
|
|
1161
249
|
},
|
|
1162
250
|
tool: {
|
|
1163
|
-
...createProfileTools()
|
|
251
|
+
...chunkTJ7ZGZHD_cjs.createProfileTools(),
|
|
252
|
+
...chunkTJ7ZGZHD_cjs.createOperatorTools(lazyOperatorDeps)
|
|
1164
253
|
}
|
|
1165
254
|
};
|
|
1166
255
|
return hooks;
|
|
@@ -1170,8 +259,6 @@ var pluginModule = {
|
|
|
1170
259
|
server
|
|
1171
260
|
};
|
|
1172
261
|
var plugin_default = pluginModule;
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
});
|
|
1177
|
-
//# sourceMappingURL=plugin.cjs.map
|
|
262
|
+
|
|
263
|
+
exports.default = plugin_default;
|
|
264
|
+
exports.server = server;
|