@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.
- package/CHANGELOG.md +18 -0
- package/LICENSE +199 -21
- package/README.md +88 -288
- package/dist/{chunk-BTDKGS7P.js → chunk-IKNWSNAS.js} +120 -307
- package/dist/chunk-VABBGKSR.cjs +1663 -0
- package/dist/index.cjs +583 -1927
- package/dist/index.js +348 -224
- package/dist/plugin.cjs +35 -1031
- package/dist/plugin.js +16 -34
- package/package.json +47 -11
- 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
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import pino from 'pino';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { EventEmitter } from 'eventemitter3';
|
|
5
|
+
import { CircuitState, ConsecutiveBreaker, CountBreaker, BrokenCircuitError, IsolatedCircuitError, circuitBreaker, handleAll } from 'cockatiel';
|
|
6
|
+
import PQueue from 'p-queue';
|
|
7
|
+
import { readFile, mkdir, writeFile, rename, watch } from 'fs/promises';
|
|
8
|
+
import { homedir } from 'os';
|
|
9
|
+
import { join, dirname } from 'path';
|
|
10
|
+
import { tool } from '@opencode-ai/plugin/tool';
|
|
11
|
+
|
|
1
12
|
// src/config/defaults.ts
|
|
2
13
|
var DEFAULT_RETRY_BUDGET = 3;
|
|
3
14
|
var DEFAULT_FAILOVER_BUDGET = 2;
|
|
@@ -21,9 +32,6 @@ var DEFAULT_CB_HALF_OPEN_MAX_PROBES = 1;
|
|
|
21
32
|
var DEFAULT_CB_SUCCESS_THRESHOLD = 2;
|
|
22
33
|
var DEFAULT_RETRY = 3;
|
|
23
34
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
24
|
-
|
|
25
|
-
// src/config/schema.ts
|
|
26
|
-
import { z } from "zod";
|
|
27
35
|
var BackoffConfigSchema = z.object({
|
|
28
36
|
base_ms: z.number().positive().default(DEFAULT_BACKOFF_BASE_MS),
|
|
29
37
|
multiplier: z.number().positive().default(DEFAULT_BACKOFF_MULTIPLIER),
|
|
@@ -133,13 +141,11 @@ var validateConfig = (raw) => {
|
|
|
133
141
|
}
|
|
134
142
|
return Object.freeze(data);
|
|
135
143
|
}
|
|
136
|
-
const diagnostics = result.error.issues.map(
|
|
137
|
-
(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
})
|
|
142
|
-
);
|
|
144
|
+
const diagnostics = result.error.issues.map((issue) => ({
|
|
145
|
+
path: issue.path.join("."),
|
|
146
|
+
message: issue.message,
|
|
147
|
+
received: "expected" in issue ? issue.expected : void 0
|
|
148
|
+
}));
|
|
143
149
|
throw new ConfigValidationError(diagnostics);
|
|
144
150
|
};
|
|
145
151
|
|
|
@@ -240,10 +246,7 @@ var TargetRegistry = class {
|
|
|
240
246
|
if (!target) {
|
|
241
247
|
return;
|
|
242
248
|
}
|
|
243
|
-
const newHealthScore = updateHealthScore(
|
|
244
|
-
target.health_score,
|
|
245
|
-
observation
|
|
246
|
-
);
|
|
249
|
+
const newHealthScore = updateHealthScore(target.health_score, observation);
|
|
247
250
|
this.targets.set(id, { ...target, health_score: newHealthScore });
|
|
248
251
|
}
|
|
249
252
|
/** Sets the cooldown_until timestamp for a target. */
|
|
@@ -302,10 +305,6 @@ var TARGET_STATES = [
|
|
|
302
305
|
"Draining",
|
|
303
306
|
"Disabled"
|
|
304
307
|
];
|
|
305
|
-
|
|
306
|
-
// src/audit/logger.ts
|
|
307
|
-
import pino from "pino";
|
|
308
|
-
import crypto from "crypto";
|
|
309
308
|
var REDACT_PATHS = [
|
|
310
309
|
"api_key",
|
|
311
310
|
"token",
|
|
@@ -337,57 +336,54 @@ var createAuditLogger = (options) => {
|
|
|
337
336
|
};
|
|
338
337
|
var createRequestLogger = (baseLogger, requestId) => baseLogger.child({ request_id: requestId });
|
|
339
338
|
var generateCorrelationId = () => crypto.randomUUID();
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
retryable: z2.literal(true),
|
|
346
|
-
retry_after_ms: z2.number().optional(),
|
|
347
|
-
provider_reason: z2.string().optional()
|
|
339
|
+
var RateLimitedSchema = z.object({
|
|
340
|
+
class: z.literal("RateLimited"),
|
|
341
|
+
retryable: z.literal(true),
|
|
342
|
+
retry_after_ms: z.number().optional(),
|
|
343
|
+
provider_reason: z.string().optional()
|
|
348
344
|
});
|
|
349
|
-
var QuotaExhaustedSchema =
|
|
350
|
-
class:
|
|
351
|
-
retryable:
|
|
352
|
-
provider_reason:
|
|
345
|
+
var QuotaExhaustedSchema = z.object({
|
|
346
|
+
class: z.literal("QuotaExhausted"),
|
|
347
|
+
retryable: z.literal(false),
|
|
348
|
+
provider_reason: z.string().optional()
|
|
353
349
|
});
|
|
354
|
-
var AuthFailureSchema =
|
|
355
|
-
class:
|
|
356
|
-
retryable:
|
|
357
|
-
recovery_attempted:
|
|
350
|
+
var AuthFailureSchema = z.object({
|
|
351
|
+
class: z.literal("AuthFailure"),
|
|
352
|
+
retryable: z.literal(false),
|
|
353
|
+
recovery_attempted: z.boolean().default(false)
|
|
358
354
|
});
|
|
359
|
-
var PermissionFailureSchema =
|
|
360
|
-
class:
|
|
361
|
-
retryable:
|
|
355
|
+
var PermissionFailureSchema = z.object({
|
|
356
|
+
class: z.literal("PermissionFailure"),
|
|
357
|
+
retryable: z.literal(false)
|
|
362
358
|
});
|
|
363
|
-
var PolicyFailureSchema =
|
|
364
|
-
class:
|
|
365
|
-
retryable:
|
|
359
|
+
var PolicyFailureSchema = z.object({
|
|
360
|
+
class: z.literal("PolicyFailure"),
|
|
361
|
+
retryable: z.literal(false)
|
|
366
362
|
});
|
|
367
|
-
var RegionRestrictionSchema =
|
|
368
|
-
class:
|
|
369
|
-
retryable:
|
|
363
|
+
var RegionRestrictionSchema = z.object({
|
|
364
|
+
class: z.literal("RegionRestriction"),
|
|
365
|
+
retryable: z.literal(false)
|
|
370
366
|
});
|
|
371
|
-
var ModelUnavailableSchema =
|
|
372
|
-
class:
|
|
373
|
-
retryable:
|
|
374
|
-
failover_eligible:
|
|
367
|
+
var ModelUnavailableSchema = z.object({
|
|
368
|
+
class: z.literal("ModelUnavailable"),
|
|
369
|
+
retryable: z.literal(false),
|
|
370
|
+
failover_eligible: z.literal(true)
|
|
375
371
|
});
|
|
376
|
-
var TransientServerFailureSchema =
|
|
377
|
-
class:
|
|
378
|
-
retryable:
|
|
379
|
-
http_status:
|
|
372
|
+
var TransientServerFailureSchema = z.object({
|
|
373
|
+
class: z.literal("TransientServerFailure"),
|
|
374
|
+
retryable: z.literal(true),
|
|
375
|
+
http_status: z.number().optional()
|
|
380
376
|
});
|
|
381
|
-
var TransportFailureSchema =
|
|
382
|
-
class:
|
|
383
|
-
retryable:
|
|
377
|
+
var TransportFailureSchema = z.object({
|
|
378
|
+
class: z.literal("TransportFailure"),
|
|
379
|
+
retryable: z.literal(true)
|
|
384
380
|
});
|
|
385
|
-
var InterruptedExecutionSchema =
|
|
386
|
-
class:
|
|
387
|
-
retryable:
|
|
388
|
-
partial_output_bytes:
|
|
381
|
+
var InterruptedExecutionSchema = z.object({
|
|
382
|
+
class: z.literal("InterruptedExecution"),
|
|
383
|
+
retryable: z.literal(true),
|
|
384
|
+
partial_output_bytes: z.number().optional()
|
|
389
385
|
});
|
|
390
|
-
var ErrorClassSchema =
|
|
386
|
+
var ErrorClassSchema = z.discriminatedUnion("class", [
|
|
391
387
|
RateLimitedSchema,
|
|
392
388
|
QuotaExhaustedSchema,
|
|
393
389
|
AuthFailureSchema,
|
|
@@ -433,10 +429,7 @@ var DEFAULT_BACKOFF_PARAMS = {
|
|
|
433
429
|
jitter: "full"
|
|
434
430
|
};
|
|
435
431
|
var computeBackoffMs = (attempt, params, retryAfterMs) => {
|
|
436
|
-
const rawDelay = Math.min(
|
|
437
|
-
params.base_ms * Math.pow(params.multiplier, attempt),
|
|
438
|
-
params.max_ms
|
|
439
|
-
);
|
|
432
|
+
const rawDelay = Math.min(params.base_ms * Math.pow(params.multiplier, attempt), params.max_ms);
|
|
440
433
|
const jitteredDelay = params.jitter === "full" ? Math.random() * rawDelay : params.jitter === "equal" ? rawDelay / 2 + Math.random() * (rawDelay / 2) : rawDelay;
|
|
441
434
|
return Math.max(jitteredDelay, retryAfterMs ?? 0);
|
|
442
435
|
};
|
|
@@ -480,22 +473,8 @@ var EXCLUSION_REASONS = [
|
|
|
480
473
|
"capability_mismatch",
|
|
481
474
|
"draining"
|
|
482
475
|
];
|
|
483
|
-
var ADMISSION_RESULTS = [
|
|
484
|
-
"admitted",
|
|
485
|
-
"queued",
|
|
486
|
-
"degraded",
|
|
487
|
-
"rejected"
|
|
488
|
-
];
|
|
489
|
-
|
|
490
|
-
// src/routing/events.ts
|
|
491
|
-
import { EventEmitter } from "eventemitter3";
|
|
476
|
+
var ADMISSION_RESULTS = ["admitted", "queued", "degraded", "rejected"];
|
|
492
477
|
var createRoutingEventBus = () => new EventEmitter();
|
|
493
|
-
|
|
494
|
-
// src/routing/dual-breaker.ts
|
|
495
|
-
import {
|
|
496
|
-
ConsecutiveBreaker,
|
|
497
|
-
CountBreaker
|
|
498
|
-
} from "cockatiel";
|
|
499
478
|
var DualBreaker = class {
|
|
500
479
|
consecutive;
|
|
501
480
|
count;
|
|
@@ -537,21 +516,11 @@ var DualBreaker = class {
|
|
|
537
516
|
return consecutiveTripped || countTripped;
|
|
538
517
|
}
|
|
539
518
|
};
|
|
540
|
-
|
|
541
|
-
// src/routing/circuit-breaker.ts
|
|
542
|
-
import {
|
|
543
|
-
CircuitState as CircuitState2,
|
|
544
|
-
handleAll,
|
|
545
|
-
circuitBreaker,
|
|
546
|
-
BrokenCircuitError,
|
|
547
|
-
IsolatedCircuitError
|
|
548
|
-
} from "cockatiel";
|
|
549
|
-
import "eventemitter3";
|
|
550
519
|
var COCKATIEL_TO_TARGET = /* @__PURE__ */ new Map([
|
|
551
|
-
[
|
|
552
|
-
[
|
|
553
|
-
[
|
|
554
|
-
[
|
|
520
|
+
[CircuitState.Closed, "Active"],
|
|
521
|
+
[CircuitState.Open, "CircuitOpen"],
|
|
522
|
+
[CircuitState.HalfOpen, "CircuitHalfOpen"],
|
|
523
|
+
[CircuitState.Isolated, "Disabled"]
|
|
555
524
|
]);
|
|
556
525
|
var toTargetState = (state) => COCKATIEL_TO_TARGET.get(state) ?? "Disabled";
|
|
557
526
|
var createCircuitBreaker = (targetId, config, eventBus) => {
|
|
@@ -567,11 +536,11 @@ var createCircuitBreaker = (targetId, config, eventBus) => {
|
|
|
567
536
|
return { policy, breaker };
|
|
568
537
|
};
|
|
569
538
|
let current = createPolicy();
|
|
570
|
-
let previousState =
|
|
539
|
+
let previousState = CircuitState.Closed;
|
|
571
540
|
let openedAtMs = 0;
|
|
572
541
|
const subscribeEvents = (policy) => {
|
|
573
542
|
policy.onStateChange((newState) => {
|
|
574
|
-
if (newState ===
|
|
543
|
+
if (newState === CircuitState.Open) {
|
|
575
544
|
openedAtMs = Date.now();
|
|
576
545
|
}
|
|
577
546
|
const from = toTargetState(previousState);
|
|
@@ -590,8 +559,8 @@ var createCircuitBreaker = (targetId, config, eventBus) => {
|
|
|
590
559
|
subscribeEvents(current.policy);
|
|
591
560
|
const effectiveState = () => {
|
|
592
561
|
const raw = current.policy.state;
|
|
593
|
-
if (raw ===
|
|
594
|
-
return
|
|
562
|
+
if (raw === CircuitState.Open && openedAtMs > 0 && Date.now() - openedAtMs >= config.half_open_after_ms) {
|
|
563
|
+
return CircuitState.HalfOpen;
|
|
595
564
|
}
|
|
596
565
|
return raw;
|
|
597
566
|
};
|
|
@@ -618,11 +587,11 @@ var createCircuitBreaker = (targetId, config, eventBus) => {
|
|
|
618
587
|
}
|
|
619
588
|
},
|
|
620
589
|
allowRequest() {
|
|
621
|
-
return effectiveState() !==
|
|
590
|
+
return effectiveState() !== CircuitState.Open;
|
|
622
591
|
},
|
|
623
592
|
reset() {
|
|
624
593
|
current = createPolicy();
|
|
625
|
-
previousState =
|
|
594
|
+
previousState = CircuitState.Closed;
|
|
626
595
|
openedAtMs = 0;
|
|
627
596
|
subscribeEvents(current.policy);
|
|
628
597
|
}
|
|
@@ -824,9 +793,6 @@ var createCooldownManager = (registry, eventBus) => ({
|
|
|
824
793
|
return target.cooldown_until !== null && target.cooldown_until > nowMs;
|
|
825
794
|
}
|
|
826
795
|
});
|
|
827
|
-
|
|
828
|
-
// src/routing/admission.ts
|
|
829
|
-
import PQueue from "p-queue";
|
|
830
796
|
var checkHardRejects = (ctx) => {
|
|
831
797
|
if (!ctx.hasEligibleTargets) {
|
|
832
798
|
return { result: "rejected", reason: "no eligible targets available" };
|
|
@@ -997,7 +963,11 @@ var createFailoverOrchestrator = (deps) => ({
|
|
|
997
963
|
let totalFailovers = 0;
|
|
998
964
|
let lastErrorClass;
|
|
999
965
|
log?.info(
|
|
1000
|
-
{
|
|
966
|
+
{
|
|
967
|
+
event: "failover_start",
|
|
968
|
+
retry_budget: deps.retryBudget,
|
|
969
|
+
failover_budget: deps.failoverBudget
|
|
970
|
+
},
|
|
1001
971
|
"Failover loop started"
|
|
1002
972
|
);
|
|
1003
973
|
for (let failoverNo = 0; failoverNo <= deps.failoverBudget; failoverNo++) {
|
|
@@ -1028,10 +998,7 @@ var createFailoverOrchestrator = (deps) => ({
|
|
|
1028
998
|
};
|
|
1029
999
|
}
|
|
1030
1000
|
const targetId = selection.selected.target_id;
|
|
1031
|
-
const retryPolicy = createRetryPolicy(
|
|
1032
|
-
deps.retryBudget,
|
|
1033
|
-
deps.backoffParams
|
|
1034
|
-
);
|
|
1001
|
+
const retryPolicy = createRetryPolicy(deps.retryBudget, deps.backoffParams);
|
|
1035
1002
|
for (let retry = 0; retry <= deps.retryBudget; retry++) {
|
|
1036
1003
|
const attemptNo = retry + 1;
|
|
1037
1004
|
const acquired = deps.concurrency.acquire(targetId);
|
|
@@ -1057,7 +1024,13 @@ var createFailoverOrchestrator = (deps) => ({
|
|
|
1057
1024
|
deps.registry.recordObservation(targetId, 1);
|
|
1058
1025
|
deps.registry.updateLatency(targetId, attemptResult.latency_ms);
|
|
1059
1026
|
log?.info(
|
|
1060
|
-
{
|
|
1027
|
+
{
|
|
1028
|
+
event: "attempt_success",
|
|
1029
|
+
target_id: targetId,
|
|
1030
|
+
attempt_no: attemptNo,
|
|
1031
|
+
failover_no: failoverNo,
|
|
1032
|
+
latency_ms: attemptResult.latency_ms
|
|
1033
|
+
},
|
|
1061
1034
|
"Attempt succeeded"
|
|
1062
1035
|
);
|
|
1063
1036
|
attempts.push({
|
|
@@ -1095,15 +1068,16 @@ var createFailoverOrchestrator = (deps) => ({
|
|
|
1095
1068
|
deps.backoffParams,
|
|
1096
1069
|
deps.backoffParams.max_ms
|
|
1097
1070
|
);
|
|
1098
|
-
deps.cooldownManager.setCooldown(
|
|
1099
|
-
targetId,
|
|
1100
|
-
cooldownMs,
|
|
1101
|
-
errorClass.class
|
|
1102
|
-
);
|
|
1071
|
+
deps.cooldownManager.setCooldown(targetId, cooldownMs, errorClass.class);
|
|
1103
1072
|
}
|
|
1104
1073
|
if (errorClass.class === "ModelUnavailable") {
|
|
1105
1074
|
log?.info(
|
|
1106
|
-
{
|
|
1075
|
+
{
|
|
1076
|
+
event: "early_failover",
|
|
1077
|
+
target_id: targetId,
|
|
1078
|
+
error_class: errorClass.class,
|
|
1079
|
+
failover_no: failoverNo
|
|
1080
|
+
},
|
|
1107
1081
|
"Early failover \u2014 ModelUnavailable"
|
|
1108
1082
|
);
|
|
1109
1083
|
attempts.push({
|
|
@@ -1181,17 +1155,25 @@ var createFailoverOrchestrator = (deps) => ({
|
|
|
1181
1155
|
totalRetries++;
|
|
1182
1156
|
if (decision.action === "retry") {
|
|
1183
1157
|
log?.info(
|
|
1184
|
-
{
|
|
1158
|
+
{
|
|
1159
|
+
event: "retry",
|
|
1160
|
+
target_id: targetId,
|
|
1161
|
+
attempt_no: attemptNo,
|
|
1162
|
+
error_class: errorClass.class,
|
|
1163
|
+
delay_ms: decision.delay_ms
|
|
1164
|
+
},
|
|
1185
1165
|
"Retrying after delay"
|
|
1186
1166
|
);
|
|
1187
|
-
await new Promise(
|
|
1188
|
-
(resolve) => setTimeout(resolve, decision.delay_ms)
|
|
1189
|
-
);
|
|
1167
|
+
await new Promise((resolve) => setTimeout(resolve, decision.delay_ms));
|
|
1190
1168
|
}
|
|
1191
1169
|
}
|
|
1192
1170
|
}
|
|
1193
1171
|
log?.warn(
|
|
1194
|
-
{
|
|
1172
|
+
{
|
|
1173
|
+
event: "failover_budget_exhausted",
|
|
1174
|
+
total_retries: totalRetries,
|
|
1175
|
+
total_failovers: totalFailovers
|
|
1176
|
+
},
|
|
1195
1177
|
"Failover budget exhausted"
|
|
1196
1178
|
);
|
|
1197
1179
|
return {
|
|
@@ -1231,7 +1213,8 @@ var hasConfigChanged = (oldTarget, newTarget) => {
|
|
|
1231
1213
|
if (oldTarget.enabled !== newTarget.enabled) return true;
|
|
1232
1214
|
if (oldTarget.operator_priority !== newTarget.operator_priority) return true;
|
|
1233
1215
|
if (JSON.stringify(oldTarget.policy_tags) !== JSON.stringify(newTarget.policy_tags)) return true;
|
|
1234
|
-
if (JSON.stringify(oldTarget.capabilities) !== JSON.stringify(newTarget.capabilities))
|
|
1216
|
+
if (JSON.stringify(oldTarget.capabilities) !== JSON.stringify(newTarget.capabilities))
|
|
1217
|
+
return true;
|
|
1235
1218
|
if (oldTarget.retry_budget !== newTarget.retry_budget) return true;
|
|
1236
1219
|
if (oldTarget.failover_budget !== newTarget.failover_budget) return true;
|
|
1237
1220
|
if (oldTarget.concurrency_limit !== newTarget.concurrency_limit) return true;
|
|
@@ -1372,95 +1355,6 @@ var reloadConfig = (deps, rawConfig) => {
|
|
|
1372
1355
|
throw err;
|
|
1373
1356
|
}
|
|
1374
1357
|
};
|
|
1375
|
-
|
|
1376
|
-
// src/operator/plugin-tools.ts
|
|
1377
|
-
import { tool } from "@opencode-ai/plugin/tool";
|
|
1378
|
-
var { schema: z3 } = tool;
|
|
1379
|
-
var createOperatorTools = (deps) => ({
|
|
1380
|
-
listTargets: tool({
|
|
1381
|
-
description: "List all routing targets with health scores, states, and circuit breaker status.",
|
|
1382
|
-
args: {},
|
|
1383
|
-
async execute() {
|
|
1384
|
-
deps.logger.info({ op: "listTargets" }, "operator: listTargets");
|
|
1385
|
-
const result = listTargets(deps);
|
|
1386
|
-
return JSON.stringify(result, null, 2);
|
|
1387
|
-
}
|
|
1388
|
-
}),
|
|
1389
|
-
pauseTarget: tool({
|
|
1390
|
-
description: "Pause a target, preventing new requests from being routed to it.",
|
|
1391
|
-
args: { target_id: z3.string().min(1) },
|
|
1392
|
-
async execute(args) {
|
|
1393
|
-
deps.logger.info(
|
|
1394
|
-
{ op: "pauseTarget", target_id: args.target_id },
|
|
1395
|
-
"operator: pauseTarget"
|
|
1396
|
-
);
|
|
1397
|
-
const result = pauseTarget(deps, args.target_id);
|
|
1398
|
-
return JSON.stringify(result, null, 2);
|
|
1399
|
-
}
|
|
1400
|
-
}),
|
|
1401
|
-
resumeTarget: tool({
|
|
1402
|
-
description: "Resume a previously paused or disabled target, allowing new requests.",
|
|
1403
|
-
args: { target_id: z3.string().min(1) },
|
|
1404
|
-
async execute(args) {
|
|
1405
|
-
deps.logger.info(
|
|
1406
|
-
{ op: "resumeTarget", target_id: args.target_id },
|
|
1407
|
-
"operator: resumeTarget"
|
|
1408
|
-
);
|
|
1409
|
-
const result = resumeTarget(deps, args.target_id);
|
|
1410
|
-
return JSON.stringify(result, null, 2);
|
|
1411
|
-
}
|
|
1412
|
-
}),
|
|
1413
|
-
drainTarget: tool({
|
|
1414
|
-
description: "Drain a target, allowing in-flight requests to complete but preventing new ones.",
|
|
1415
|
-
args: { target_id: z3.string().min(1) },
|
|
1416
|
-
async execute(args) {
|
|
1417
|
-
deps.logger.info(
|
|
1418
|
-
{ op: "drainTarget", target_id: args.target_id },
|
|
1419
|
-
"operator: drainTarget"
|
|
1420
|
-
);
|
|
1421
|
-
const result = drainTarget(deps, args.target_id);
|
|
1422
|
-
return JSON.stringify(result, null, 2);
|
|
1423
|
-
}
|
|
1424
|
-
}),
|
|
1425
|
-
disableTarget: tool({
|
|
1426
|
-
description: "Disable a target entirely, removing it from routing.",
|
|
1427
|
-
args: { target_id: z3.string().min(1) },
|
|
1428
|
-
async execute(args) {
|
|
1429
|
-
deps.logger.info(
|
|
1430
|
-
{ op: "disableTarget", target_id: args.target_id },
|
|
1431
|
-
"operator: disableTarget"
|
|
1432
|
-
);
|
|
1433
|
-
const result = disableTarget(deps, args.target_id);
|
|
1434
|
-
return JSON.stringify(result, null, 2);
|
|
1435
|
-
}
|
|
1436
|
-
}),
|
|
1437
|
-
inspectRequest: tool({
|
|
1438
|
-
description: "Inspect a request trace by ID, showing attempts, segments, and outcome.",
|
|
1439
|
-
args: { request_id: z3.string().min(1) },
|
|
1440
|
-
async execute(args) {
|
|
1441
|
-
deps.logger.info(
|
|
1442
|
-
{ op: "inspectRequest", request_id: args.request_id },
|
|
1443
|
-
"operator: inspectRequest"
|
|
1444
|
-
);
|
|
1445
|
-
const result = inspectRequest(deps, args.request_id);
|
|
1446
|
-
return JSON.stringify(result, null, 2);
|
|
1447
|
-
}
|
|
1448
|
-
}),
|
|
1449
|
-
reloadConfig: tool({
|
|
1450
|
-
description: "Reload routing configuration with diff-apply. Validates new config before applying.",
|
|
1451
|
-
args: { config: z3.record(z3.string(), z3.unknown()) },
|
|
1452
|
-
async execute(args) {
|
|
1453
|
-
deps.logger.info({ op: "reloadConfig" }, "operator: reloadConfig");
|
|
1454
|
-
const result = reloadConfig(deps, args.config);
|
|
1455
|
-
return JSON.stringify(result, null, 2);
|
|
1456
|
-
}
|
|
1457
|
-
})
|
|
1458
|
-
});
|
|
1459
|
-
|
|
1460
|
-
// src/profiles/store.ts
|
|
1461
|
-
import { readFile, writeFile, rename, mkdir } from "fs/promises";
|
|
1462
|
-
import { homedir } from "os";
|
|
1463
|
-
import { join, dirname } from "path";
|
|
1464
1358
|
var PROFILES_DIR = join(homedir(), ".local", "share", "o-switcher");
|
|
1465
1359
|
var PROFILES_PATH = join(PROFILES_DIR, "profiles.json");
|
|
1466
1360
|
var loadProfiles = async (path = PROFILES_PATH) => {
|
|
@@ -1527,22 +1421,11 @@ var nextProfileId = (store, provider) => {
|
|
|
1527
1421
|
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);
|
|
1528
1422
|
return `${provider}-${maxN + 1}`;
|
|
1529
1423
|
};
|
|
1530
|
-
|
|
1531
|
-
// src/profiles/watcher.ts
|
|
1532
|
-
import { readFile as readFile2, watch } from "fs/promises";
|
|
1533
|
-
import { homedir as homedir2 } from "os";
|
|
1534
|
-
import { join as join2, dirname as dirname2 } from "path";
|
|
1535
|
-
var AUTH_JSON_PATH = join2(
|
|
1536
|
-
homedir2(),
|
|
1537
|
-
".local",
|
|
1538
|
-
"share",
|
|
1539
|
-
"opencode",
|
|
1540
|
-
"auth.json"
|
|
1541
|
-
);
|
|
1424
|
+
var AUTH_JSON_PATH = join(homedir(), ".local", "share", "opencode", "auth.json");
|
|
1542
1425
|
var DEBOUNCE_MS = 100;
|
|
1543
1426
|
var readAuthJson = async (path) => {
|
|
1544
1427
|
try {
|
|
1545
|
-
const content = await
|
|
1428
|
+
const content = await readFile(path, "utf-8");
|
|
1546
1429
|
return JSON.parse(content);
|
|
1547
1430
|
} catch {
|
|
1548
1431
|
return {};
|
|
@@ -1575,7 +1458,6 @@ var createAuthWatcher = (options) => {
|
|
|
1575
1458
|
let lastKnownAuth = {};
|
|
1576
1459
|
let abortController = null;
|
|
1577
1460
|
let debounceTimer = null;
|
|
1578
|
-
let watchPromise = null;
|
|
1579
1461
|
const processChange = async () => {
|
|
1580
1462
|
const newAuth = await readAuthJson(authPath);
|
|
1581
1463
|
let store = await loadProfiles(profPath);
|
|
@@ -1584,7 +1466,10 @@ var createAuthWatcher = (options) => {
|
|
|
1584
1466
|
if (!entry) continue;
|
|
1585
1467
|
const previousEntry = lastKnownAuth[provider];
|
|
1586
1468
|
if (previousEntry !== void 0 && !entriesEqual(previousEntry, entry)) {
|
|
1587
|
-
log?.info(
|
|
1469
|
+
log?.info(
|
|
1470
|
+
{ provider, action: "credential_changed" },
|
|
1471
|
+
"Credential change detected \u2014 saving previous credential"
|
|
1472
|
+
);
|
|
1588
1473
|
const prevCredential = toCredential(previousEntry);
|
|
1589
1474
|
const newStore = addProfile(store, provider, prevCredential);
|
|
1590
1475
|
if (newStore !== store) {
|
|
@@ -1630,12 +1515,15 @@ var createAuthWatcher = (options) => {
|
|
|
1630
1515
|
store = addProfile(store, provider, credential);
|
|
1631
1516
|
}
|
|
1632
1517
|
await saveProfiles(store, profPath);
|
|
1633
|
-
log?.info(
|
|
1518
|
+
log?.info(
|
|
1519
|
+
{ profiles_initialized: Object.keys(store).length },
|
|
1520
|
+
"Initialized profiles from auth.json"
|
|
1521
|
+
);
|
|
1634
1522
|
}
|
|
1635
1523
|
lastKnownAuth = currentAuth;
|
|
1636
1524
|
abortController = new AbortController();
|
|
1637
|
-
const parentDir =
|
|
1638
|
-
|
|
1525
|
+
const parentDir = dirname(authPath);
|
|
1526
|
+
(async () => {
|
|
1639
1527
|
try {
|
|
1640
1528
|
const watcher = watch(parentDir, {
|
|
1641
1529
|
signal: abortController.signal
|
|
@@ -1646,9 +1534,7 @@ var createAuthWatcher = (options) => {
|
|
|
1646
1534
|
}
|
|
1647
1535
|
}
|
|
1648
1536
|
} catch (err) {
|
|
1649
|
-
|
|
1650
|
-
if (name !== "AbortError") {
|
|
1651
|
-
}
|
|
1537
|
+
err.name;
|
|
1652
1538
|
}
|
|
1653
1539
|
})();
|
|
1654
1540
|
};
|
|
@@ -1661,17 +1547,13 @@ var createAuthWatcher = (options) => {
|
|
|
1661
1547
|
abortController.abort();
|
|
1662
1548
|
abortController = null;
|
|
1663
1549
|
}
|
|
1664
|
-
watchPromise = null;
|
|
1665
1550
|
log?.info("Auth watcher stopped");
|
|
1666
1551
|
};
|
|
1667
1552
|
return { start, stop };
|
|
1668
1553
|
};
|
|
1669
|
-
|
|
1670
|
-
// src/profiles/tools.ts
|
|
1671
|
-
import { tool as tool2 } from "@opencode-ai/plugin/tool";
|
|
1672
|
-
var { schema: z4 } = tool2;
|
|
1554
|
+
var { schema: z3 } = tool;
|
|
1673
1555
|
var createProfileTools = (options) => ({
|
|
1674
|
-
profilesList:
|
|
1556
|
+
profilesList: tool({
|
|
1675
1557
|
description: "List all saved auth profiles with provider, type, and creation date.",
|
|
1676
1558
|
args: {},
|
|
1677
1559
|
async execute() {
|
|
@@ -1686,9 +1568,9 @@ var createProfileTools = (options) => ({
|
|
|
1686
1568
|
return JSON.stringify(result, null, 2);
|
|
1687
1569
|
}
|
|
1688
1570
|
}),
|
|
1689
|
-
profilesRemove:
|
|
1571
|
+
profilesRemove: tool({
|
|
1690
1572
|
description: "Remove a saved auth profile by ID.",
|
|
1691
|
-
args: { id:
|
|
1573
|
+
args: { id: z3.string().min(1) },
|
|
1692
1574
|
async execute(args) {
|
|
1693
1575
|
const store = await loadProfiles(options?.profilesPath);
|
|
1694
1576
|
const { store: newStore, removed } = removeProfile(store, args.id);
|
|
@@ -1705,73 +1587,4 @@ var createProfileTools = (options) => ({
|
|
|
1705
1587
|
})
|
|
1706
1588
|
});
|
|
1707
1589
|
|
|
1708
|
-
export {
|
|
1709
|
-
DEFAULT_RETRY_BUDGET,
|
|
1710
|
-
DEFAULT_FAILOVER_BUDGET,
|
|
1711
|
-
DEFAULT_BACKOFF_BASE_MS,
|
|
1712
|
-
DEFAULT_BACKOFF_MULTIPLIER,
|
|
1713
|
-
DEFAULT_BACKOFF_MAX_MS,
|
|
1714
|
-
DEFAULT_BACKOFF_JITTER,
|
|
1715
|
-
DEFAULT_RETRY,
|
|
1716
|
-
DEFAULT_TIMEOUT_MS,
|
|
1717
|
-
BackoffConfigSchema,
|
|
1718
|
-
TargetConfigSchema,
|
|
1719
|
-
SwitcherConfigSchema,
|
|
1720
|
-
ConfigValidationError,
|
|
1721
|
-
validateConfig,
|
|
1722
|
-
discoverTargets,
|
|
1723
|
-
discoverTargetsFromProfiles,
|
|
1724
|
-
INITIAL_HEALTH_SCORE,
|
|
1725
|
-
DEFAULT_ALPHA,
|
|
1726
|
-
updateHealthScore,
|
|
1727
|
-
updateLatencyEma,
|
|
1728
|
-
TargetRegistry,
|
|
1729
|
-
createRegistry,
|
|
1730
|
-
TARGET_STATES,
|
|
1731
|
-
REDACT_PATHS,
|
|
1732
|
-
createAuditLogger,
|
|
1733
|
-
createRequestLogger,
|
|
1734
|
-
generateCorrelationId,
|
|
1735
|
-
ErrorClassSchema,
|
|
1736
|
-
isRetryable,
|
|
1737
|
-
getTargetStateTransition,
|
|
1738
|
-
DEFAULT_BACKOFF_PARAMS,
|
|
1739
|
-
computeBackoffMs,
|
|
1740
|
-
createRetryPolicy,
|
|
1741
|
-
EXCLUSION_REASONS,
|
|
1742
|
-
ADMISSION_RESULTS,
|
|
1743
|
-
createRoutingEventBus,
|
|
1744
|
-
DualBreaker,
|
|
1745
|
-
createCircuitBreaker,
|
|
1746
|
-
createConcurrencyTracker,
|
|
1747
|
-
normalizeLatency,
|
|
1748
|
-
computeScore,
|
|
1749
|
-
getExclusionReason,
|
|
1750
|
-
selectTarget,
|
|
1751
|
-
computeCooldownMs,
|
|
1752
|
-
createCooldownManager,
|
|
1753
|
-
checkHardRejects,
|
|
1754
|
-
createAdmissionController,
|
|
1755
|
-
createLogSubscriber,
|
|
1756
|
-
createFailoverOrchestrator,
|
|
1757
|
-
computeConfigDiff,
|
|
1758
|
-
applyConfigDiff,
|
|
1759
|
-
createRequestTraceBuffer,
|
|
1760
|
-
listTargets,
|
|
1761
|
-
pauseTarget,
|
|
1762
|
-
resumeTarget,
|
|
1763
|
-
drainTarget,
|
|
1764
|
-
disableTarget,
|
|
1765
|
-
inspectRequest,
|
|
1766
|
-
reloadConfig,
|
|
1767
|
-
createOperatorTools,
|
|
1768
|
-
loadProfiles,
|
|
1769
|
-
saveProfiles,
|
|
1770
|
-
addProfile,
|
|
1771
|
-
removeProfile,
|
|
1772
|
-
listProfiles,
|
|
1773
|
-
nextProfileId,
|
|
1774
|
-
createAuthWatcher,
|
|
1775
|
-
createProfileTools
|
|
1776
|
-
};
|
|
1777
|
-
//# sourceMappingURL=chunk-BTDKGS7P.js.map
|
|
1590
|
+
export { ADMISSION_RESULTS, BackoffConfigSchema, ConfigValidationError, DEFAULT_ALPHA, DEFAULT_BACKOFF_BASE_MS, DEFAULT_BACKOFF_JITTER, DEFAULT_BACKOFF_MAX_MS, DEFAULT_BACKOFF_MULTIPLIER, DEFAULT_BACKOFF_PARAMS, DEFAULT_FAILOVER_BUDGET, DEFAULT_RETRY, DEFAULT_RETRY_BUDGET, DEFAULT_TIMEOUT_MS, DualBreaker, EXCLUSION_REASONS, ErrorClassSchema, INITIAL_HEALTH_SCORE, REDACT_PATHS, SwitcherConfigSchema, TARGET_STATES, TargetConfigSchema, TargetRegistry, addProfile, applyConfigDiff, checkHardRejects, computeBackoffMs, computeConfigDiff, computeCooldownMs, computeScore, createAdmissionController, createAuditLogger, createAuthWatcher, createCircuitBreaker, createConcurrencyTracker, createCooldownManager, createFailoverOrchestrator, createLogSubscriber, createProfileTools, createRegistry, createRequestLogger, createRequestTraceBuffer, createRetryPolicy, createRoutingEventBus, disableTarget, discoverTargets, discoverTargetsFromProfiles, drainTarget, generateCorrelationId, getExclusionReason, getTargetStateTransition, inspectRequest, isRetryable, listProfiles, listTargets, loadProfiles, nextProfileId, normalizeLatency, pauseTarget, reloadConfig, removeProfile, resumeTarget, saveProfiles, selectTarget, updateHealthScore, updateLatencyEma, validateConfig };
|