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
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2582 @@
|
|
|
1
|
+
"use strict";
|
|
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/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
ADMISSION_RESULTS: () => ADMISSION_RESULTS,
|
|
34
|
+
BackoffConfigSchema: () => BackoffConfigSchema,
|
|
35
|
+
ConfigValidationError: () => ConfigValidationError,
|
|
36
|
+
DEFAULT_ALPHA: () => DEFAULT_ALPHA,
|
|
37
|
+
DEFAULT_BACKOFF_BASE_MS: () => DEFAULT_BACKOFF_BASE_MS,
|
|
38
|
+
DEFAULT_BACKOFF_JITTER: () => DEFAULT_BACKOFF_JITTER,
|
|
39
|
+
DEFAULT_BACKOFF_MAX_MS: () => DEFAULT_BACKOFF_MAX_MS,
|
|
40
|
+
DEFAULT_BACKOFF_MULTIPLIER: () => DEFAULT_BACKOFF_MULTIPLIER,
|
|
41
|
+
DEFAULT_BACKOFF_PARAMS: () => DEFAULT_BACKOFF_PARAMS,
|
|
42
|
+
DEFAULT_FAILOVER_BUDGET: () => DEFAULT_FAILOVER_BUDGET,
|
|
43
|
+
DEFAULT_RETRY: () => DEFAULT_RETRY,
|
|
44
|
+
DEFAULT_RETRY_BUDGET: () => DEFAULT_RETRY_BUDGET,
|
|
45
|
+
DEFAULT_TIMEOUT_MS: () => DEFAULT_TIMEOUT_MS,
|
|
46
|
+
DualBreaker: () => DualBreaker,
|
|
47
|
+
EXCLUSION_REASONS: () => EXCLUSION_REASONS,
|
|
48
|
+
ErrorClassSchema: () => ErrorClassSchema,
|
|
49
|
+
HEURISTIC_PATTERNS: () => HEURISTIC_PATTERNS,
|
|
50
|
+
INITIAL_HEALTH_SCORE: () => INITIAL_HEALTH_SCORE,
|
|
51
|
+
PROVIDER_PATTERNS: () => PROVIDER_PATTERNS,
|
|
52
|
+
REDACT_PATHS: () => REDACT_PATHS,
|
|
53
|
+
SwitcherConfigSchema: () => SwitcherConfigSchema,
|
|
54
|
+
TARGET_STATES: () => TARGET_STATES,
|
|
55
|
+
TEMPORAL_QUOTA_PATTERN: () => TEMPORAL_QUOTA_PATTERN,
|
|
56
|
+
TargetConfigSchema: () => TargetConfigSchema,
|
|
57
|
+
TargetRegistry: () => TargetRegistry,
|
|
58
|
+
addProfile: () => addProfile,
|
|
59
|
+
applyConfigDiff: () => applyConfigDiff,
|
|
60
|
+
checkHardRejects: () => checkHardRejects,
|
|
61
|
+
classify: () => classify,
|
|
62
|
+
computeBackoffMs: () => computeBackoffMs,
|
|
63
|
+
computeConfigDiff: () => computeConfigDiff,
|
|
64
|
+
computeCooldownMs: () => computeCooldownMs,
|
|
65
|
+
computeScore: () => computeScore,
|
|
66
|
+
createAdmissionController: () => createAdmissionController,
|
|
67
|
+
createAuditCollector: () => createAuditCollector,
|
|
68
|
+
createAuditLogger: () => createAuditLogger,
|
|
69
|
+
createAuthWatcher: () => createAuthWatcher,
|
|
70
|
+
createCircuitBreaker: () => createCircuitBreaker,
|
|
71
|
+
createConcurrencyTracker: () => createConcurrencyTracker,
|
|
72
|
+
createCooldownManager: () => createCooldownManager,
|
|
73
|
+
createExecutionOrchestrator: () => createExecutionOrchestrator,
|
|
74
|
+
createFailoverOrchestrator: () => createFailoverOrchestrator,
|
|
75
|
+
createLogSubscriber: () => createLogSubscriber,
|
|
76
|
+
createModeAdapter: () => createModeAdapter,
|
|
77
|
+
createOperatorTools: () => createOperatorTools,
|
|
78
|
+
createProfileTools: () => createProfileTools,
|
|
79
|
+
createRegistry: () => createRegistry,
|
|
80
|
+
createRequestLogger: () => createRequestLogger,
|
|
81
|
+
createRequestTraceBuffer: () => createRequestTraceBuffer,
|
|
82
|
+
createRetryPolicy: () => createRetryPolicy,
|
|
83
|
+
createRoutingEventBus: () => createRoutingEventBus,
|
|
84
|
+
createStreamBuffer: () => createStreamBuffer,
|
|
85
|
+
createStreamStitcher: () => createStreamStitcher,
|
|
86
|
+
detectDeploymentMode: () => detectDeploymentMode,
|
|
87
|
+
determineContinuationMode: () => determineContinuationMode,
|
|
88
|
+
directSignalFromResponse: () => directSignalFromResponse,
|
|
89
|
+
disableTarget: () => disableTarget,
|
|
90
|
+
discoverTargets: () => discoverTargets,
|
|
91
|
+
discoverTargetsFromProfiles: () => discoverTargetsFromProfiles,
|
|
92
|
+
drainTarget: () => drainTarget,
|
|
93
|
+
extractRetryAfterMs: () => extractRetryAfterMs,
|
|
94
|
+
generateCorrelationId: () => generateCorrelationId,
|
|
95
|
+
getExclusionReason: () => getExclusionReason,
|
|
96
|
+
getModeCapabilities: () => getModeCapabilities,
|
|
97
|
+
getSignalFidelity: () => getSignalFidelity,
|
|
98
|
+
getTargetStateTransition: () => getTargetStateTransition,
|
|
99
|
+
heuristicSignalFromEvent: () => heuristicSignalFromEvent,
|
|
100
|
+
inspectRequest: () => inspectRequest,
|
|
101
|
+
isRetryable: () => isRetryable,
|
|
102
|
+
listProfiles: () => listProfiles,
|
|
103
|
+
listTargets: () => listTargets,
|
|
104
|
+
loadProfiles: () => loadProfiles,
|
|
105
|
+
nextProfileId: () => nextProfileId,
|
|
106
|
+
normalizeLatency: () => normalizeLatency,
|
|
107
|
+
pauseTarget: () => pauseTarget,
|
|
108
|
+
reloadConfig: () => reloadConfig,
|
|
109
|
+
removeProfile: () => removeProfile,
|
|
110
|
+
resumeTarget: () => resumeTarget,
|
|
111
|
+
saveProfiles: () => saveProfiles,
|
|
112
|
+
selectTarget: () => selectTarget,
|
|
113
|
+
updateHealthScore: () => updateHealthScore,
|
|
114
|
+
updateLatencyEma: () => updateLatencyEma,
|
|
115
|
+
validateBearerToken: () => validateBearerToken,
|
|
116
|
+
validateConfig: () => validateConfig
|
|
117
|
+
});
|
|
118
|
+
module.exports = __toCommonJS(index_exports);
|
|
119
|
+
|
|
120
|
+
// src/config/schema.ts
|
|
121
|
+
var import_zod = require("zod");
|
|
122
|
+
|
|
123
|
+
// src/config/defaults.ts
|
|
124
|
+
var DEFAULT_RETRY_BUDGET = 3;
|
|
125
|
+
var DEFAULT_FAILOVER_BUDGET = 2;
|
|
126
|
+
var DEFAULT_BACKOFF_BASE_MS = 1e3;
|
|
127
|
+
var DEFAULT_BACKOFF_MULTIPLIER = 2;
|
|
128
|
+
var DEFAULT_BACKOFF_MAX_MS = 3e4;
|
|
129
|
+
var DEFAULT_BACKOFF_JITTER = "full";
|
|
130
|
+
var DEFAULT_ROUTING_WEIGHT_HEALTH = 1;
|
|
131
|
+
var DEFAULT_ROUTING_WEIGHT_LATENCY = 0.5;
|
|
132
|
+
var DEFAULT_ROUTING_WEIGHT_FAILURE = 0.8;
|
|
133
|
+
var DEFAULT_ROUTING_WEIGHT_PRIORITY = 0.3;
|
|
134
|
+
var DEFAULT_MAX_EXPECTED_LATENCY_MS = 3e4;
|
|
135
|
+
var DEFAULT_QUEUE_LIMIT = 100;
|
|
136
|
+
var DEFAULT_GLOBAL_CONCURRENCY_LIMIT = 10;
|
|
137
|
+
var DEFAULT_BACKPRESSURE_THRESHOLD = 50;
|
|
138
|
+
var DEFAULT_CB_FAILURE_THRESHOLD = 5;
|
|
139
|
+
var DEFAULT_CB_FAILURE_RATE_THRESHOLD = 0.5;
|
|
140
|
+
var DEFAULT_CB_SLIDING_WINDOW_SIZE = 10;
|
|
141
|
+
var DEFAULT_CB_HALF_OPEN_AFTER_MS = 3e4;
|
|
142
|
+
var DEFAULT_CB_HALF_OPEN_MAX_PROBES = 1;
|
|
143
|
+
var DEFAULT_CB_SUCCESS_THRESHOLD = 2;
|
|
144
|
+
var DEFAULT_RETRY = 3;
|
|
145
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
146
|
+
|
|
147
|
+
// src/config/schema.ts
|
|
148
|
+
var BackoffConfigSchema = import_zod.z.object({
|
|
149
|
+
base_ms: import_zod.z.number().positive().default(DEFAULT_BACKOFF_BASE_MS),
|
|
150
|
+
multiplier: import_zod.z.number().positive().default(DEFAULT_BACKOFF_MULTIPLIER),
|
|
151
|
+
max_ms: import_zod.z.number().positive().default(DEFAULT_BACKOFF_MAX_MS),
|
|
152
|
+
jitter: import_zod.z.enum(["full", "equal", "none"]).default(DEFAULT_BACKOFF_JITTER)
|
|
153
|
+
});
|
|
154
|
+
var RoutingWeightsSchema = import_zod.z.object({
|
|
155
|
+
health: import_zod.z.number().default(DEFAULT_ROUTING_WEIGHT_HEALTH),
|
|
156
|
+
latency: import_zod.z.number().default(DEFAULT_ROUTING_WEIGHT_LATENCY),
|
|
157
|
+
failure: import_zod.z.number().default(DEFAULT_ROUTING_WEIGHT_FAILURE),
|
|
158
|
+
priority: import_zod.z.number().default(DEFAULT_ROUTING_WEIGHT_PRIORITY)
|
|
159
|
+
});
|
|
160
|
+
var CircuitBreakerConfigSchema = import_zod.z.object({
|
|
161
|
+
failure_threshold: import_zod.z.number().int().positive().default(DEFAULT_CB_FAILURE_THRESHOLD),
|
|
162
|
+
failure_rate_threshold: import_zod.z.number().gt(0).lt(1).default(DEFAULT_CB_FAILURE_RATE_THRESHOLD),
|
|
163
|
+
sliding_window_size: import_zod.z.number().int().positive().default(DEFAULT_CB_SLIDING_WINDOW_SIZE),
|
|
164
|
+
half_open_after_ms: import_zod.z.number().positive().default(DEFAULT_CB_HALF_OPEN_AFTER_MS),
|
|
165
|
+
half_open_max_probes: import_zod.z.number().int().positive().default(DEFAULT_CB_HALF_OPEN_MAX_PROBES),
|
|
166
|
+
success_threshold: import_zod.z.number().int().positive().default(DEFAULT_CB_SUCCESS_THRESHOLD)
|
|
167
|
+
});
|
|
168
|
+
var TargetConfigSchema = import_zod.z.object({
|
|
169
|
+
target_id: import_zod.z.string().min(1),
|
|
170
|
+
provider_id: import_zod.z.string().min(1),
|
|
171
|
+
profile: import_zod.z.string().optional(),
|
|
172
|
+
endpoint_id: import_zod.z.string().optional(),
|
|
173
|
+
capabilities: import_zod.z.array(import_zod.z.string()).default([]),
|
|
174
|
+
enabled: import_zod.z.boolean().default(true),
|
|
175
|
+
operator_priority: import_zod.z.number().int().default(0),
|
|
176
|
+
policy_tags: import_zod.z.array(import_zod.z.string()).default([]),
|
|
177
|
+
retry_budget: import_zod.z.number().int().positive().optional(),
|
|
178
|
+
failover_budget: import_zod.z.number().int().positive().optional(),
|
|
179
|
+
backoff: BackoffConfigSchema.optional(),
|
|
180
|
+
concurrency_limit: import_zod.z.number().int().positive().optional(),
|
|
181
|
+
circuit_breaker: CircuitBreakerConfigSchema.optional()
|
|
182
|
+
});
|
|
183
|
+
var SwitcherConfigSchema = import_zod.z.object({
|
|
184
|
+
targets: import_zod.z.array(TargetConfigSchema).min(1).optional(),
|
|
185
|
+
retry: import_zod.z.number().int().positive().optional(),
|
|
186
|
+
timeout: import_zod.z.number().positive().optional(),
|
|
187
|
+
retry_budget: import_zod.z.number().int().positive().default(DEFAULT_RETRY_BUDGET),
|
|
188
|
+
failover_budget: import_zod.z.number().int().positive().default(DEFAULT_FAILOVER_BUDGET),
|
|
189
|
+
backoff: BackoffConfigSchema.default({
|
|
190
|
+
base_ms: DEFAULT_BACKOFF_BASE_MS,
|
|
191
|
+
multiplier: DEFAULT_BACKOFF_MULTIPLIER,
|
|
192
|
+
max_ms: DEFAULT_BACKOFF_MAX_MS,
|
|
193
|
+
jitter: DEFAULT_BACKOFF_JITTER
|
|
194
|
+
}),
|
|
195
|
+
deployment_mode_hint: import_zod.z.enum(["plugin-only", "server-companion", "sdk-control", "auto"]).default("auto"),
|
|
196
|
+
routing_weights: RoutingWeightsSchema.default({
|
|
197
|
+
health: DEFAULT_ROUTING_WEIGHT_HEALTH,
|
|
198
|
+
latency: DEFAULT_ROUTING_WEIGHT_LATENCY,
|
|
199
|
+
failure: DEFAULT_ROUTING_WEIGHT_FAILURE,
|
|
200
|
+
priority: DEFAULT_ROUTING_WEIGHT_PRIORITY
|
|
201
|
+
}),
|
|
202
|
+
queue_limit: import_zod.z.number().int().positive().default(DEFAULT_QUEUE_LIMIT),
|
|
203
|
+
concurrency_limit: import_zod.z.number().int().positive().default(DEFAULT_GLOBAL_CONCURRENCY_LIMIT),
|
|
204
|
+
backpressure_threshold: import_zod.z.number().int().nonnegative().default(DEFAULT_BACKPRESSURE_THRESHOLD),
|
|
205
|
+
circuit_breaker: CircuitBreakerConfigSchema.default({
|
|
206
|
+
failure_threshold: DEFAULT_CB_FAILURE_THRESHOLD,
|
|
207
|
+
failure_rate_threshold: DEFAULT_CB_FAILURE_RATE_THRESHOLD,
|
|
208
|
+
sliding_window_size: DEFAULT_CB_SLIDING_WINDOW_SIZE,
|
|
209
|
+
half_open_after_ms: DEFAULT_CB_HALF_OPEN_AFTER_MS,
|
|
210
|
+
half_open_max_probes: DEFAULT_CB_HALF_OPEN_MAX_PROBES,
|
|
211
|
+
success_threshold: DEFAULT_CB_SUCCESS_THRESHOLD
|
|
212
|
+
}),
|
|
213
|
+
max_expected_latency_ms: import_zod.z.number().positive().default(DEFAULT_MAX_EXPECTED_LATENCY_MS)
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// src/config/loader.ts
|
|
217
|
+
var ConfigValidationError = class extends Error {
|
|
218
|
+
diagnostics;
|
|
219
|
+
constructor(diagnostics) {
|
|
220
|
+
const summary = diagnostics.map((d) => ` ${d.path}: ${d.message}`).join("\n");
|
|
221
|
+
super(`Invalid O-Switcher configuration:
|
|
222
|
+
${summary}`);
|
|
223
|
+
this.name = "ConfigValidationError";
|
|
224
|
+
this.diagnostics = diagnostics;
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
var validateConfig = (raw) => {
|
|
228
|
+
const result = SwitcherConfigSchema.safeParse(raw);
|
|
229
|
+
if (result.success) {
|
|
230
|
+
const data = { ...result.data };
|
|
231
|
+
if (data.retry !== void 0 && data.retry_budget === DEFAULT_RETRY_BUDGET) {
|
|
232
|
+
data.retry_budget = data.retry;
|
|
233
|
+
}
|
|
234
|
+
if (data.timeout !== void 0 && data.max_expected_latency_ms === DEFAULT_MAX_EXPECTED_LATENCY_MS) {
|
|
235
|
+
data.max_expected_latency_ms = data.timeout;
|
|
236
|
+
}
|
|
237
|
+
if (data.targets !== void 0) {
|
|
238
|
+
const seen = /* @__PURE__ */ new Set();
|
|
239
|
+
const duplicateDiagnostics = [];
|
|
240
|
+
for (const target of data.targets) {
|
|
241
|
+
const key = `${target.provider_id}::${target.profile ?? "__default__"}`;
|
|
242
|
+
if (seen.has(key)) {
|
|
243
|
+
duplicateDiagnostics.push({
|
|
244
|
+
path: "targets",
|
|
245
|
+
message: `Duplicate provider_id + profile combination: ${key}`,
|
|
246
|
+
received: key
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
seen.add(key);
|
|
250
|
+
}
|
|
251
|
+
if (duplicateDiagnostics.length > 0) {
|
|
252
|
+
throw new ConfigValidationError(duplicateDiagnostics);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return Object.freeze(data);
|
|
256
|
+
}
|
|
257
|
+
const diagnostics = result.error.issues.map(
|
|
258
|
+
(issue) => ({
|
|
259
|
+
path: issue.path.join("."),
|
|
260
|
+
message: issue.message,
|
|
261
|
+
received: "expected" in issue ? issue.expected : void 0
|
|
262
|
+
})
|
|
263
|
+
);
|
|
264
|
+
throw new ConfigValidationError(diagnostics);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// src/config/discovery.ts
|
|
268
|
+
var discoverTargets = (providerConfig) => {
|
|
269
|
+
const targets = [];
|
|
270
|
+
for (const [key, value] of Object.entries(providerConfig)) {
|
|
271
|
+
if (!value) continue;
|
|
272
|
+
const raw = {
|
|
273
|
+
target_id: key,
|
|
274
|
+
provider_id: value.id ?? key,
|
|
275
|
+
capabilities: ["chat"],
|
|
276
|
+
enabled: true,
|
|
277
|
+
operator_priority: 0,
|
|
278
|
+
policy_tags: []
|
|
279
|
+
};
|
|
280
|
+
const parsed = TargetConfigSchema.parse(raw);
|
|
281
|
+
targets.push(parsed);
|
|
282
|
+
}
|
|
283
|
+
return targets;
|
|
284
|
+
};
|
|
285
|
+
var discoverTargetsFromProfiles = (store) => {
|
|
286
|
+
const targets = [];
|
|
287
|
+
for (const entry of Object.values(store)) {
|
|
288
|
+
if (!entry) continue;
|
|
289
|
+
const raw = {
|
|
290
|
+
target_id: entry.id,
|
|
291
|
+
provider_id: entry.provider,
|
|
292
|
+
profile: entry.id,
|
|
293
|
+
capabilities: ["chat"],
|
|
294
|
+
enabled: true,
|
|
295
|
+
operator_priority: 0,
|
|
296
|
+
policy_tags: []
|
|
297
|
+
};
|
|
298
|
+
const parsed = TargetConfigSchema.parse(raw);
|
|
299
|
+
targets.push(parsed);
|
|
300
|
+
}
|
|
301
|
+
return targets;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// src/registry/health.ts
|
|
305
|
+
var INITIAL_HEALTH_SCORE = 1;
|
|
306
|
+
var DEFAULT_ALPHA = 0.1;
|
|
307
|
+
var updateHealthScore = (currentScore, observation, alpha = DEFAULT_ALPHA) => alpha * observation + (1 - alpha) * currentScore;
|
|
308
|
+
var updateLatencyEma = (currentEma, observedMs, alpha = DEFAULT_ALPHA) => alpha * observedMs + (1 - alpha) * currentEma;
|
|
309
|
+
|
|
310
|
+
// src/registry/registry.ts
|
|
311
|
+
var TargetRegistry = class {
|
|
312
|
+
targets;
|
|
313
|
+
constructor(config) {
|
|
314
|
+
this.targets = new Map(
|
|
315
|
+
(config.targets ?? []).map((t) => [
|
|
316
|
+
t.target_id,
|
|
317
|
+
{
|
|
318
|
+
target_id: t.target_id,
|
|
319
|
+
provider_id: t.provider_id,
|
|
320
|
+
profile: t.profile,
|
|
321
|
+
endpoint_id: t.endpoint_id,
|
|
322
|
+
capabilities: [...t.capabilities],
|
|
323
|
+
enabled: t.enabled,
|
|
324
|
+
state: "Active",
|
|
325
|
+
health_score: INITIAL_HEALTH_SCORE,
|
|
326
|
+
cooldown_until: null,
|
|
327
|
+
latency_ema_ms: 0,
|
|
328
|
+
failure_score: 0,
|
|
329
|
+
operator_priority: t.operator_priority,
|
|
330
|
+
policy_tags: [...t.policy_tags]
|
|
331
|
+
}
|
|
332
|
+
])
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
/** Returns the target entry for the given id, or undefined if not found. */
|
|
336
|
+
getTarget(id) {
|
|
337
|
+
return this.targets.get(id);
|
|
338
|
+
}
|
|
339
|
+
/** Returns a readonly array of all target entries. */
|
|
340
|
+
getAllTargets() {
|
|
341
|
+
return [...this.targets.values()];
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Updates the state of a target.
|
|
345
|
+
* @returns true if the target was found and updated, false otherwise.
|
|
346
|
+
*/
|
|
347
|
+
updateState(id, newState) {
|
|
348
|
+
const target = this.targets.get(id);
|
|
349
|
+
if (!target) {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
this.targets.set(id, { ...target, state: newState });
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Records a success (1) or failure (0) observation for a target.
|
|
357
|
+
* Updates health_score via EMA.
|
|
358
|
+
*/
|
|
359
|
+
recordObservation(id, observation) {
|
|
360
|
+
const target = this.targets.get(id);
|
|
361
|
+
if (!target) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const newHealthScore = updateHealthScore(
|
|
365
|
+
target.health_score,
|
|
366
|
+
observation
|
|
367
|
+
);
|
|
368
|
+
this.targets.set(id, { ...target, health_score: newHealthScore });
|
|
369
|
+
}
|
|
370
|
+
/** Sets the cooldown_until timestamp for a target. */
|
|
371
|
+
setCooldown(id, untilMs) {
|
|
372
|
+
const target = this.targets.get(id);
|
|
373
|
+
if (!target) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
this.targets.set(id, { ...target, cooldown_until: untilMs });
|
|
377
|
+
}
|
|
378
|
+
/** Updates the latency EMA for a target. */
|
|
379
|
+
updateLatency(id, ms) {
|
|
380
|
+
const target = this.targets.get(id);
|
|
381
|
+
if (!target) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const newLatency = updateLatencyEma(target.latency_ema_ms, ms);
|
|
385
|
+
this.targets.set(id, { ...target, latency_ema_ms: newLatency });
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Adds a new target entry to the registry.
|
|
389
|
+
* @returns true if added, false if target_id already exists.
|
|
390
|
+
*/
|
|
391
|
+
addTarget(entry) {
|
|
392
|
+
if (this.targets.has(entry.target_id)) {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
this.targets.set(entry.target_id, entry);
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Removes a target entry from the registry.
|
|
400
|
+
* @returns true if found and removed, false if not found.
|
|
401
|
+
*/
|
|
402
|
+
removeTarget(id) {
|
|
403
|
+
return this.targets.delete(id);
|
|
404
|
+
}
|
|
405
|
+
/** Returns a deep-frozen snapshot of all targets. */
|
|
406
|
+
getSnapshot() {
|
|
407
|
+
const snapshot = {
|
|
408
|
+
targets: [...this.targets.values()].map((t) => ({ ...t }))
|
|
409
|
+
};
|
|
410
|
+
return Object.freeze(snapshot);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
var createRegistry = (config) => new TargetRegistry(config);
|
|
414
|
+
|
|
415
|
+
// src/registry/types.ts
|
|
416
|
+
var TARGET_STATES = [
|
|
417
|
+
"Active",
|
|
418
|
+
"CoolingDown",
|
|
419
|
+
"ReauthRequired",
|
|
420
|
+
"PolicyBlocked",
|
|
421
|
+
"CircuitOpen",
|
|
422
|
+
"CircuitHalfOpen",
|
|
423
|
+
"Draining",
|
|
424
|
+
"Disabled"
|
|
425
|
+
];
|
|
426
|
+
|
|
427
|
+
// src/mode/detection.ts
|
|
428
|
+
var detectDeploymentMode = (hint) => {
|
|
429
|
+
if (hint !== "auto") {
|
|
430
|
+
return hint;
|
|
431
|
+
}
|
|
432
|
+
return "plugin-only";
|
|
433
|
+
};
|
|
434
|
+
var getSignalFidelity = (mode) => {
|
|
435
|
+
if (mode === "plugin-only") {
|
|
436
|
+
return "heuristic";
|
|
437
|
+
}
|
|
438
|
+
return "direct";
|
|
439
|
+
};
|
|
440
|
+
var getModeCapabilities = (mode) => {
|
|
441
|
+
if (mode === "plugin-only") {
|
|
442
|
+
return {
|
|
443
|
+
mode,
|
|
444
|
+
signalFidelity: "heuristic",
|
|
445
|
+
hasHttpStatus: false,
|
|
446
|
+
hasRetryAfterHeader: false,
|
|
447
|
+
hasOperatorApi: false
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
mode,
|
|
452
|
+
signalFidelity: "direct",
|
|
453
|
+
hasHttpStatus: true,
|
|
454
|
+
hasRetryAfterHeader: true,
|
|
455
|
+
hasOperatorApi: true
|
|
456
|
+
};
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
// src/audit/logger.ts
|
|
460
|
+
var import_pino = __toESM(require("pino"), 1);
|
|
461
|
+
var import_node_crypto = __toESM(require("crypto"), 1);
|
|
462
|
+
var REDACT_PATHS = [
|
|
463
|
+
"api_key",
|
|
464
|
+
"token",
|
|
465
|
+
"secret",
|
|
466
|
+
"password",
|
|
467
|
+
"authorization",
|
|
468
|
+
"credential",
|
|
469
|
+
"credentials",
|
|
470
|
+
"*.api_key",
|
|
471
|
+
"*.token",
|
|
472
|
+
"*.secret",
|
|
473
|
+
"*.password",
|
|
474
|
+
"*.authorization",
|
|
475
|
+
"*.credential",
|
|
476
|
+
"*.credentials"
|
|
477
|
+
];
|
|
478
|
+
var createAuditLogger = (options) => {
|
|
479
|
+
const opts = {
|
|
480
|
+
level: options?.level ?? "info",
|
|
481
|
+
redact: {
|
|
482
|
+
paths: [...REDACT_PATHS],
|
|
483
|
+
censor: "[Redacted]"
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
if (options?.destination) {
|
|
487
|
+
return (0, import_pino.default)(opts, options.destination);
|
|
488
|
+
}
|
|
489
|
+
return (0, import_pino.default)(opts);
|
|
490
|
+
};
|
|
491
|
+
var createRequestLogger = (baseLogger, requestId) => baseLogger.child({ request_id: requestId });
|
|
492
|
+
var generateCorrelationId = () => import_node_crypto.default.randomUUID();
|
|
493
|
+
|
|
494
|
+
// src/errors/taxonomy.ts
|
|
495
|
+
var import_zod2 = require("zod");
|
|
496
|
+
var RateLimitedSchema = import_zod2.z.object({
|
|
497
|
+
class: import_zod2.z.literal("RateLimited"),
|
|
498
|
+
retryable: import_zod2.z.literal(true),
|
|
499
|
+
retry_after_ms: import_zod2.z.number().optional(),
|
|
500
|
+
provider_reason: import_zod2.z.string().optional()
|
|
501
|
+
});
|
|
502
|
+
var QuotaExhaustedSchema = import_zod2.z.object({
|
|
503
|
+
class: import_zod2.z.literal("QuotaExhausted"),
|
|
504
|
+
retryable: import_zod2.z.literal(false),
|
|
505
|
+
provider_reason: import_zod2.z.string().optional()
|
|
506
|
+
});
|
|
507
|
+
var AuthFailureSchema = import_zod2.z.object({
|
|
508
|
+
class: import_zod2.z.literal("AuthFailure"),
|
|
509
|
+
retryable: import_zod2.z.literal(false),
|
|
510
|
+
recovery_attempted: import_zod2.z.boolean().default(false)
|
|
511
|
+
});
|
|
512
|
+
var PermissionFailureSchema = import_zod2.z.object({
|
|
513
|
+
class: import_zod2.z.literal("PermissionFailure"),
|
|
514
|
+
retryable: import_zod2.z.literal(false)
|
|
515
|
+
});
|
|
516
|
+
var PolicyFailureSchema = import_zod2.z.object({
|
|
517
|
+
class: import_zod2.z.literal("PolicyFailure"),
|
|
518
|
+
retryable: import_zod2.z.literal(false)
|
|
519
|
+
});
|
|
520
|
+
var RegionRestrictionSchema = import_zod2.z.object({
|
|
521
|
+
class: import_zod2.z.literal("RegionRestriction"),
|
|
522
|
+
retryable: import_zod2.z.literal(false)
|
|
523
|
+
});
|
|
524
|
+
var ModelUnavailableSchema = import_zod2.z.object({
|
|
525
|
+
class: import_zod2.z.literal("ModelUnavailable"),
|
|
526
|
+
retryable: import_zod2.z.literal(false),
|
|
527
|
+
failover_eligible: import_zod2.z.literal(true)
|
|
528
|
+
});
|
|
529
|
+
var TransientServerFailureSchema = import_zod2.z.object({
|
|
530
|
+
class: import_zod2.z.literal("TransientServerFailure"),
|
|
531
|
+
retryable: import_zod2.z.literal(true),
|
|
532
|
+
http_status: import_zod2.z.number().optional()
|
|
533
|
+
});
|
|
534
|
+
var TransportFailureSchema = import_zod2.z.object({
|
|
535
|
+
class: import_zod2.z.literal("TransportFailure"),
|
|
536
|
+
retryable: import_zod2.z.literal(true)
|
|
537
|
+
});
|
|
538
|
+
var InterruptedExecutionSchema = import_zod2.z.object({
|
|
539
|
+
class: import_zod2.z.literal("InterruptedExecution"),
|
|
540
|
+
retryable: import_zod2.z.literal(true),
|
|
541
|
+
partial_output_bytes: import_zod2.z.number().optional()
|
|
542
|
+
});
|
|
543
|
+
var ErrorClassSchema = import_zod2.z.discriminatedUnion("class", [
|
|
544
|
+
RateLimitedSchema,
|
|
545
|
+
QuotaExhaustedSchema,
|
|
546
|
+
AuthFailureSchema,
|
|
547
|
+
PermissionFailureSchema,
|
|
548
|
+
PolicyFailureSchema,
|
|
549
|
+
RegionRestrictionSchema,
|
|
550
|
+
ModelUnavailableSchema,
|
|
551
|
+
TransientServerFailureSchema,
|
|
552
|
+
TransportFailureSchema,
|
|
553
|
+
InterruptedExecutionSchema
|
|
554
|
+
]);
|
|
555
|
+
var isRetryable = (errorClass) => errorClass.retryable;
|
|
556
|
+
var getTargetStateTransition = (errorClass) => {
|
|
557
|
+
switch (errorClass.class) {
|
|
558
|
+
case "RateLimited":
|
|
559
|
+
return "CoolingDown";
|
|
560
|
+
case "QuotaExhausted":
|
|
561
|
+
return "Disabled";
|
|
562
|
+
case "AuthFailure":
|
|
563
|
+
return "ReauthRequired";
|
|
564
|
+
case "PermissionFailure":
|
|
565
|
+
return "PolicyBlocked";
|
|
566
|
+
case "PolicyFailure":
|
|
567
|
+
return "PolicyBlocked";
|
|
568
|
+
case "RegionRestriction":
|
|
569
|
+
return "Disabled";
|
|
570
|
+
case "ModelUnavailable":
|
|
571
|
+
return null;
|
|
572
|
+
case "TransientServerFailure":
|
|
573
|
+
return null;
|
|
574
|
+
case "TransportFailure":
|
|
575
|
+
return null;
|
|
576
|
+
case "InterruptedExecution":
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
// src/errors/corpus.ts
|
|
582
|
+
var PROVIDER_PATTERNS = [
|
|
583
|
+
// Anthropic
|
|
584
|
+
{ provider: "anthropic", http_status: 400, error_type_field: "error.type", error_type_value: "invalid_request_error", error_class: "PolicyFailure" },
|
|
585
|
+
{ provider: "anthropic", http_status: 401, error_type_field: "error.type", error_type_value: "authentication_error", error_class: "AuthFailure" },
|
|
586
|
+
{ provider: "anthropic", http_status: 402, error_type_field: "error.type", error_type_value: "billing_error", error_class: "QuotaExhausted" },
|
|
587
|
+
{ provider: "anthropic", http_status: 403, error_type_field: "error.type", error_type_value: "permission_error", error_class: "PermissionFailure" },
|
|
588
|
+
{ provider: "anthropic", http_status: 404, error_type_field: "error.type", error_type_value: "not_found_error", error_class: "ModelUnavailable" },
|
|
589
|
+
{ provider: "anthropic", http_status: 429, error_type_field: "error.type", error_type_value: "rate_limit_error", error_class: "RateLimited" },
|
|
590
|
+
{ provider: "anthropic", http_status: 500, error_type_field: "error.type", error_type_value: "api_error", error_class: "TransientServerFailure" },
|
|
591
|
+
{ provider: "anthropic", http_status: 504, error_type_field: "error.type", error_type_value: "timeout_error", error_class: "TransportFailure" },
|
|
592
|
+
{ provider: "anthropic", http_status: 529, error_type_field: "error.type", error_type_value: "overloaded_error", error_class: "RateLimited", notes: "Server capacity exhaustion; backoff on same provider, do NOT failover" },
|
|
593
|
+
// OpenAI
|
|
594
|
+
{ provider: "openai", http_status: 400, error_type_field: "error.code", error_type_value: "", error_class: "PolicyFailure" },
|
|
595
|
+
{ provider: "openai", http_status: 401, error_type_field: "error.code", error_type_value: "invalid_api_key", error_class: "AuthFailure" },
|
|
596
|
+
{ provider: "openai", http_status: 403, error_type_field: "error.code", error_type_value: "", error_class: "PermissionFailure" },
|
|
597
|
+
{ provider: "openai", http_status: 429, error_type_field: "error.code", error_type_value: "rate_limit_exceeded", error_class: "RateLimited" },
|
|
598
|
+
{ provider: "openai", http_status: 429, error_type_field: "error.code", error_type_value: "insufficient_quota", error_class: "QuotaExhausted", notes: "Billing exhausted -- NOT retryable" },
|
|
599
|
+
{ provider: "openai", http_status: 500, error_type_field: "error.code", error_type_value: "", error_class: "TransientServerFailure" },
|
|
600
|
+
{ provider: "openai", http_status: 503, error_type_field: "error.code", error_type_value: "", error_class: "TransientServerFailure" },
|
|
601
|
+
// Google Vertex AI / Gemini
|
|
602
|
+
{ provider: "google", http_status: 400, error_type_field: "error.status", error_type_value: "INVALID_ARGUMENT", error_class: "PolicyFailure" },
|
|
603
|
+
{ provider: "google", http_status: 403, error_type_field: "error.status", error_type_value: "PERMISSION_DENIED", error_class: "PermissionFailure" },
|
|
604
|
+
{ provider: "google", http_status: 429, error_type_field: "error.status", error_type_value: "RESOURCE_EXHAUSTED", error_class: "RateLimited" },
|
|
605
|
+
{ provider: "google", http_status: 500, error_type_field: "error.status", error_type_value: "INTERNAL", error_class: "TransientServerFailure" },
|
|
606
|
+
{ provider: "google", http_status: 503, error_type_field: "error.status", error_type_value: "UNAVAILABLE", error_class: "TransientServerFailure" },
|
|
607
|
+
// AWS Bedrock
|
|
608
|
+
{ provider: "bedrock", http_status: 429, error_type_field: "__type", error_type_value: "ThrottlingException", error_class: "RateLimited" },
|
|
609
|
+
{ provider: "bedrock", http_status: 408, error_type_field: "__type", error_type_value: "ModelTimeoutException", error_class: "TransportFailure" },
|
|
610
|
+
{ provider: "bedrock", http_status: 424, error_type_field: "__type", error_type_value: "ModelNotReadyException", error_class: "ModelUnavailable" },
|
|
611
|
+
{ provider: "bedrock", http_status: 403, error_type_field: "__type", error_type_value: "AccessDeniedException", error_class: "PermissionFailure" },
|
|
612
|
+
{ provider: "bedrock", http_status: 400, error_type_field: "__type", error_type_value: "ValidationException", error_class: "PolicyFailure" },
|
|
613
|
+
{ provider: "bedrock", http_status: 500, error_type_field: "__type", error_type_value: "InternalServerException", error_class: "TransientServerFailure" }
|
|
614
|
+
];
|
|
615
|
+
var HEURISTIC_PATTERNS = [
|
|
616
|
+
// Transport errors (high confidence -- these are unambiguous)
|
|
617
|
+
{ pattern: /ECONNREFUSED|ECONNRESET|ETIMEDOUT/, error_class: "TransportFailure", confidence: "high" },
|
|
618
|
+
// Quota patterns (before rate limit to avoid misclassification -- Pitfall 2/3)
|
|
619
|
+
{ pattern: /quota\s*(?:exceeded|exhausted)/i, error_class: "QuotaExhausted", confidence: "medium" },
|
|
620
|
+
{ pattern: /insufficient\s*quota/i, error_class: "QuotaExhausted", confidence: "medium", provider: "openai" },
|
|
621
|
+
{ pattern: /billing/i, error_class: "QuotaExhausted", confidence: "low", provider: "openai" },
|
|
622
|
+
// Rate limit patterns
|
|
623
|
+
{ pattern: /rate\s*limit/i, error_class: "RateLimited", confidence: "medium" },
|
|
624
|
+
{ pattern: /too many requests/i, error_class: "RateLimited", confidence: "medium" },
|
|
625
|
+
{ pattern: /retry\s*after/i, error_class: "RateLimited", confidence: "medium" },
|
|
626
|
+
{ pattern: /overloaded/i, error_class: "RateLimited", confidence: "medium", provider: "anthropic" },
|
|
627
|
+
{ pattern: /resource\s*exhausted/i, error_class: "RateLimited", confidence: "medium", provider: "google" },
|
|
628
|
+
// Auth patterns
|
|
629
|
+
{ pattern: /authentication/i, error_class: "AuthFailure", confidence: "medium" },
|
|
630
|
+
{ pattern: /invalid\s*api\s*key/i, error_class: "AuthFailure", confidence: "medium" },
|
|
631
|
+
{ pattern: /unauthorized/i, error_class: "AuthFailure", confidence: "medium" },
|
|
632
|
+
// Permission patterns
|
|
633
|
+
{ pattern: /permission\s*denied/i, error_class: "PermissionFailure", confidence: "medium" },
|
|
634
|
+
{ pattern: /forbidden/i, error_class: "PermissionFailure", confidence: "low" },
|
|
635
|
+
// Model availability patterns
|
|
636
|
+
{ pattern: /model\s*not\s*(?:available|ready|found)/i, error_class: "ModelUnavailable", confidence: "medium" },
|
|
637
|
+
{ pattern: /not\s*found/i, error_class: "ModelUnavailable", confidence: "low" },
|
|
638
|
+
// Transport patterns (lower confidence than ECONNREFUSED)
|
|
639
|
+
{ pattern: /timeout/i, error_class: "TransportFailure", confidence: "low" },
|
|
640
|
+
// Region restriction (requires both keywords)
|
|
641
|
+
{ pattern: /region.*restrict|restrict.*region/i, error_class: "RegionRestriction", confidence: "low" }
|
|
642
|
+
];
|
|
643
|
+
var TEMPORAL_QUOTA_PATTERN = /too many tokens per (?:day|month|hour|week)/i;
|
|
644
|
+
|
|
645
|
+
// src/errors/classifier.ts
|
|
646
|
+
var buildErrorClass = (className, extras) => {
|
|
647
|
+
switch (className) {
|
|
648
|
+
case "RateLimited":
|
|
649
|
+
return {
|
|
650
|
+
class: "RateLimited",
|
|
651
|
+
retryable: true,
|
|
652
|
+
retry_after_ms: extras?.retry_after_ms,
|
|
653
|
+
provider_reason: extras?.provider_reason
|
|
654
|
+
};
|
|
655
|
+
case "QuotaExhausted":
|
|
656
|
+
return {
|
|
657
|
+
class: "QuotaExhausted",
|
|
658
|
+
retryable: false,
|
|
659
|
+
provider_reason: extras?.provider_reason
|
|
660
|
+
};
|
|
661
|
+
case "AuthFailure":
|
|
662
|
+
return { class: "AuthFailure", retryable: false, recovery_attempted: false };
|
|
663
|
+
case "PermissionFailure":
|
|
664
|
+
return { class: "PermissionFailure", retryable: false };
|
|
665
|
+
case "PolicyFailure":
|
|
666
|
+
return { class: "PolicyFailure", retryable: false };
|
|
667
|
+
case "RegionRestriction":
|
|
668
|
+
return { class: "RegionRestriction", retryable: false };
|
|
669
|
+
case "ModelUnavailable":
|
|
670
|
+
return { class: "ModelUnavailable", retryable: false, failover_eligible: true };
|
|
671
|
+
case "TransientServerFailure":
|
|
672
|
+
return {
|
|
673
|
+
class: "TransientServerFailure",
|
|
674
|
+
retryable: true,
|
|
675
|
+
http_status: extras?.http_status
|
|
676
|
+
};
|
|
677
|
+
case "TransportFailure":
|
|
678
|
+
return { class: "TransportFailure", retryable: true };
|
|
679
|
+
case "InterruptedExecution":
|
|
680
|
+
return { class: "InterruptedExecution", retryable: true };
|
|
681
|
+
default:
|
|
682
|
+
return { class: "TransientServerFailure", retryable: true, http_status: extras?.http_status };
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
var extractErrorTypeFromBody = (body) => {
|
|
686
|
+
if (typeof body !== "object" || body === null) return void 0;
|
|
687
|
+
const b = body;
|
|
688
|
+
const errorObj = b.error;
|
|
689
|
+
if (typeof errorObj === "object" && errorObj !== null) {
|
|
690
|
+
if (typeof errorObj.type === "string") return errorObj.type;
|
|
691
|
+
if (typeof errorObj.code === "string") return errorObj.code;
|
|
692
|
+
if (typeof errorObj.status === "string") return errorObj.status;
|
|
693
|
+
}
|
|
694
|
+
if (typeof b.__type === "string") return b.__type;
|
|
695
|
+
return void 0;
|
|
696
|
+
};
|
|
697
|
+
var classifyDirect = (signal) => {
|
|
698
|
+
const status = signal.http_status;
|
|
699
|
+
const errorType = signal.error_type ?? extractErrorTypeFromBody(signal.response_body);
|
|
700
|
+
const errorMessage = signal.error_message ?? "";
|
|
701
|
+
if (status === 429 && TEMPORAL_QUOTA_PATTERN.test(errorMessage)) {
|
|
702
|
+
return {
|
|
703
|
+
error_class: buildErrorClass("QuotaExhausted"),
|
|
704
|
+
detection_mode: "direct",
|
|
705
|
+
confidence: "high",
|
|
706
|
+
raw_signal: signal
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
if (status === 429 && errorType === "insufficient_quota") {
|
|
710
|
+
return {
|
|
711
|
+
error_class: buildErrorClass("QuotaExhausted"),
|
|
712
|
+
detection_mode: "direct",
|
|
713
|
+
confidence: "high",
|
|
714
|
+
raw_signal: signal
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
if (status === 529 && errorType === "overloaded_error") {
|
|
718
|
+
return {
|
|
719
|
+
error_class: buildErrorClass("RateLimited", {
|
|
720
|
+
retry_after_ms: signal.retry_after_ms,
|
|
721
|
+
provider_reason: "server_capacity_exhaustion"
|
|
722
|
+
}),
|
|
723
|
+
detection_mode: "direct",
|
|
724
|
+
confidence: "high",
|
|
725
|
+
raw_signal: signal
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
if (errorType) {
|
|
729
|
+
const match = PROVIDER_PATTERNS.find(
|
|
730
|
+
(p) => p.http_status === status && p.error_type_value === errorType
|
|
731
|
+
);
|
|
732
|
+
if (match) {
|
|
733
|
+
return {
|
|
734
|
+
error_class: buildErrorClass(match.error_class, {
|
|
735
|
+
retry_after_ms: signal.retry_after_ms,
|
|
736
|
+
http_status: status
|
|
737
|
+
}),
|
|
738
|
+
detection_mode: "direct",
|
|
739
|
+
confidence: "high",
|
|
740
|
+
raw_signal: signal
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (errorType === "ThrottlingException" && TEMPORAL_QUOTA_PATTERN.test(errorMessage)) {
|
|
745
|
+
return {
|
|
746
|
+
error_class: buildErrorClass("QuotaExhausted"),
|
|
747
|
+
detection_mode: "direct",
|
|
748
|
+
confidence: "high",
|
|
749
|
+
raw_signal: signal
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
const fallbackClass = getClassFromStatus(status);
|
|
753
|
+
return {
|
|
754
|
+
error_class: buildErrorClass(fallbackClass, {
|
|
755
|
+
retry_after_ms: signal.retry_after_ms,
|
|
756
|
+
http_status: status
|
|
757
|
+
}),
|
|
758
|
+
detection_mode: "direct",
|
|
759
|
+
confidence: "high",
|
|
760
|
+
raw_signal: signal
|
|
761
|
+
};
|
|
762
|
+
};
|
|
763
|
+
var getClassFromStatus = (status) => {
|
|
764
|
+
if (status === 400) return "PolicyFailure";
|
|
765
|
+
if (status === 401) return "AuthFailure";
|
|
766
|
+
if (status === 402) return "QuotaExhausted";
|
|
767
|
+
if (status === 403) return "PermissionFailure";
|
|
768
|
+
if (status === 404) return "ModelUnavailable";
|
|
769
|
+
if (status === 408) return "TransportFailure";
|
|
770
|
+
if (status === 429) return "RateLimited";
|
|
771
|
+
if (status >= 500) return "TransientServerFailure";
|
|
772
|
+
return "TransientServerFailure";
|
|
773
|
+
};
|
|
774
|
+
var classifyHeuristic = (signal) => {
|
|
775
|
+
const message = signal.error_message ?? "";
|
|
776
|
+
if (TEMPORAL_QUOTA_PATTERN.test(message)) {
|
|
777
|
+
return {
|
|
778
|
+
error_class: buildErrorClass("QuotaExhausted"),
|
|
779
|
+
detection_mode: "heuristic",
|
|
780
|
+
confidence: "medium",
|
|
781
|
+
raw_signal: signal
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
for (const hp of HEURISTIC_PATTERNS) {
|
|
785
|
+
if (hp.pattern.test(message)) {
|
|
786
|
+
return {
|
|
787
|
+
error_class: buildErrorClass(hp.error_class),
|
|
788
|
+
detection_mode: "heuristic",
|
|
789
|
+
confidence: hp.confidence,
|
|
790
|
+
raw_signal: signal
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return {
|
|
795
|
+
error_class: buildErrorClass("TransientServerFailure"),
|
|
796
|
+
detection_mode: "heuristic",
|
|
797
|
+
confidence: "low",
|
|
798
|
+
raw_signal: signal
|
|
799
|
+
};
|
|
800
|
+
};
|
|
801
|
+
var classify = (signal) => {
|
|
802
|
+
if (signal.http_status !== void 0) {
|
|
803
|
+
return classifyDirect(signal);
|
|
804
|
+
}
|
|
805
|
+
return classifyHeuristic(signal);
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
// src/errors/direct-adapter.ts
|
|
809
|
+
var extractErrorType = (body) => {
|
|
810
|
+
if (typeof body !== "object" || body === null) {
|
|
811
|
+
return void 0;
|
|
812
|
+
}
|
|
813
|
+
const b = body;
|
|
814
|
+
const errorObj = b.error;
|
|
815
|
+
if (typeof errorObj === "object" && errorObj !== null) {
|
|
816
|
+
if (typeof errorObj.type === "string") return errorObj.type;
|
|
817
|
+
if (typeof errorObj.code === "string") return errorObj.code;
|
|
818
|
+
if (typeof errorObj.status === "string") return errorObj.status;
|
|
819
|
+
}
|
|
820
|
+
if (typeof b.__type === "string") return b.__type;
|
|
821
|
+
return void 0;
|
|
822
|
+
};
|
|
823
|
+
var extractErrorMessage = (body) => {
|
|
824
|
+
if (typeof body !== "object" || body === null) {
|
|
825
|
+
return void 0;
|
|
826
|
+
}
|
|
827
|
+
const b = body;
|
|
828
|
+
const errorObj = b.error;
|
|
829
|
+
if (typeof errorObj === "object" && errorObj !== null) {
|
|
830
|
+
if (typeof errorObj.message === "string") return errorObj.message;
|
|
831
|
+
}
|
|
832
|
+
if (typeof b.message === "string") return b.message;
|
|
833
|
+
return void 0;
|
|
834
|
+
};
|
|
835
|
+
var extractRetryAfterMs = (headers) => {
|
|
836
|
+
if (!headers) return void 0;
|
|
837
|
+
const value = headers["retry-after"] ?? headers["Retry-After"];
|
|
838
|
+
if (!value) return void 0;
|
|
839
|
+
const seconds = Number(value);
|
|
840
|
+
if (Number.isNaN(seconds) || seconds < 0) return void 0;
|
|
841
|
+
return seconds * 1e3;
|
|
842
|
+
};
|
|
843
|
+
var directSignalFromResponse = (status, body, headers) => ({
|
|
844
|
+
http_status: status,
|
|
845
|
+
error_type: extractErrorType(body),
|
|
846
|
+
error_message: extractErrorMessage(body),
|
|
847
|
+
response_body: body,
|
|
848
|
+
detection_mode: "direct",
|
|
849
|
+
retry_after_ms: extractRetryAfterMs(headers)
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
// src/errors/heuristic-adapter.ts
|
|
853
|
+
var heuristicSignalFromEvent = (eventType, message, providerId) => ({
|
|
854
|
+
error_type: eventType,
|
|
855
|
+
error_message: message,
|
|
856
|
+
detection_mode: "heuristic",
|
|
857
|
+
provider_id: providerId
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
// src/retry/backoff.ts
|
|
861
|
+
var DEFAULT_BACKOFF_PARAMS = {
|
|
862
|
+
base_ms: 1e3,
|
|
863
|
+
multiplier: 2,
|
|
864
|
+
max_ms: 3e4,
|
|
865
|
+
jitter: "full"
|
|
866
|
+
};
|
|
867
|
+
var computeBackoffMs = (attempt, params, retryAfterMs) => {
|
|
868
|
+
const rawDelay = Math.min(
|
|
869
|
+
params.base_ms * Math.pow(params.multiplier, attempt),
|
|
870
|
+
params.max_ms
|
|
871
|
+
);
|
|
872
|
+
const jitteredDelay = params.jitter === "full" ? Math.random() * rawDelay : params.jitter === "equal" ? rawDelay / 2 + Math.random() * (rawDelay / 2) : rawDelay;
|
|
873
|
+
return Math.max(jitteredDelay, retryAfterMs ?? 0);
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
// src/retry/retry-policy.ts
|
|
877
|
+
var createRetryPolicy = (budget, backoffParams = DEFAULT_BACKOFF_PARAMS) => ({
|
|
878
|
+
budget,
|
|
879
|
+
backoffParams,
|
|
880
|
+
decide: (classification, attemptsSoFar) => {
|
|
881
|
+
const errorClass = classification.error_class;
|
|
882
|
+
if (!isRetryable(errorClass)) {
|
|
883
|
+
return {
|
|
884
|
+
action: "abort",
|
|
885
|
+
reason: `Non-retryable: ${errorClass.class}`,
|
|
886
|
+
target_state: getTargetStateTransition(errorClass)
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
if (attemptsSoFar >= budget) {
|
|
890
|
+
return {
|
|
891
|
+
action: "exhausted",
|
|
892
|
+
attempts_used: attemptsSoFar
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
const retryAfterMs = errorClass.class === "RateLimited" ? errorClass.retry_after_ms : void 0;
|
|
896
|
+
const delayMs = computeBackoffMs(attemptsSoFar, backoffParams, retryAfterMs);
|
|
897
|
+
return {
|
|
898
|
+
action: "retry",
|
|
899
|
+
delay_ms: delayMs,
|
|
900
|
+
attempt: attemptsSoFar + 1
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
// src/routing/types.ts
|
|
906
|
+
var EXCLUSION_REASONS = [
|
|
907
|
+
"disabled",
|
|
908
|
+
"policy_blocked",
|
|
909
|
+
"reauth_required",
|
|
910
|
+
"cooldown_active",
|
|
911
|
+
"circuit_open",
|
|
912
|
+
"capability_mismatch",
|
|
913
|
+
"draining"
|
|
914
|
+
];
|
|
915
|
+
var ADMISSION_RESULTS = [
|
|
916
|
+
"admitted",
|
|
917
|
+
"queued",
|
|
918
|
+
"degraded",
|
|
919
|
+
"rejected"
|
|
920
|
+
];
|
|
921
|
+
|
|
922
|
+
// src/routing/events.ts
|
|
923
|
+
var import_eventemitter3 = require("eventemitter3");
|
|
924
|
+
var createRoutingEventBus = () => new import_eventemitter3.EventEmitter();
|
|
925
|
+
|
|
926
|
+
// src/routing/dual-breaker.ts
|
|
927
|
+
var import_cockatiel = require("cockatiel");
|
|
928
|
+
var DualBreaker = class {
|
|
929
|
+
consecutive;
|
|
930
|
+
count;
|
|
931
|
+
constructor(consecutiveThreshold, countOpts) {
|
|
932
|
+
this.consecutive = new import_cockatiel.ConsecutiveBreaker(consecutiveThreshold);
|
|
933
|
+
this.count = new import_cockatiel.CountBreaker(countOpts);
|
|
934
|
+
}
|
|
935
|
+
/** Serializable state for both internal breakers. */
|
|
936
|
+
get state() {
|
|
937
|
+
return {
|
|
938
|
+
consecutive: this.consecutive.state,
|
|
939
|
+
count: this.count.state
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
/** Restore state for both internal breakers. */
|
|
943
|
+
set state(value) {
|
|
944
|
+
const v = value;
|
|
945
|
+
this.consecutive.state = v.consecutive;
|
|
946
|
+
this.count.state = v.count;
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Record success on both breakers.
|
|
950
|
+
* ConsecutiveBreaker.success() takes no args (resets counter).
|
|
951
|
+
* CountBreaker.success() takes CircuitState.
|
|
952
|
+
*/
|
|
953
|
+
success(state) {
|
|
954
|
+
this.consecutive.success();
|
|
955
|
+
this.count.success(state);
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Record failure on both breakers.
|
|
959
|
+
* Returns true if EITHER breaker trips (OR logic).
|
|
960
|
+
* ConsecutiveBreaker.failure() takes no args.
|
|
961
|
+
* CountBreaker.failure() takes CircuitState.
|
|
962
|
+
*/
|
|
963
|
+
failure(state) {
|
|
964
|
+
const consecutiveTripped = this.consecutive.failure();
|
|
965
|
+
const countTripped = this.count.failure(state);
|
|
966
|
+
return consecutiveTripped || countTripped;
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
// src/routing/circuit-breaker.ts
|
|
971
|
+
var import_cockatiel2 = require("cockatiel");
|
|
972
|
+
var import_eventemitter32 = require("eventemitter3");
|
|
973
|
+
var COCKATIEL_TO_TARGET = /* @__PURE__ */ new Map([
|
|
974
|
+
[import_cockatiel2.CircuitState.Closed, "Active"],
|
|
975
|
+
[import_cockatiel2.CircuitState.Open, "CircuitOpen"],
|
|
976
|
+
[import_cockatiel2.CircuitState.HalfOpen, "CircuitHalfOpen"],
|
|
977
|
+
[import_cockatiel2.CircuitState.Isolated, "Disabled"]
|
|
978
|
+
]);
|
|
979
|
+
var toTargetState = (state) => COCKATIEL_TO_TARGET.get(state) ?? "Disabled";
|
|
980
|
+
var createCircuitBreaker = (targetId, config, eventBus) => {
|
|
981
|
+
const createPolicy = () => {
|
|
982
|
+
const breaker = new DualBreaker(config.failure_threshold, {
|
|
983
|
+
threshold: config.failure_rate_threshold,
|
|
984
|
+
size: config.sliding_window_size
|
|
985
|
+
});
|
|
986
|
+
const policy = (0, import_cockatiel2.circuitBreaker)(import_cockatiel2.handleAll, {
|
|
987
|
+
halfOpenAfter: config.half_open_after_ms,
|
|
988
|
+
breaker
|
|
989
|
+
});
|
|
990
|
+
return { policy, breaker };
|
|
991
|
+
};
|
|
992
|
+
let current = createPolicy();
|
|
993
|
+
let previousState = import_cockatiel2.CircuitState.Closed;
|
|
994
|
+
let openedAtMs = 0;
|
|
995
|
+
const subscribeEvents = (policy) => {
|
|
996
|
+
policy.onStateChange((newState) => {
|
|
997
|
+
if (newState === import_cockatiel2.CircuitState.Open) {
|
|
998
|
+
openedAtMs = Date.now();
|
|
999
|
+
}
|
|
1000
|
+
const from = toTargetState(previousState);
|
|
1001
|
+
const to = toTargetState(newState);
|
|
1002
|
+
previousState = newState;
|
|
1003
|
+
if (from !== to && eventBus) {
|
|
1004
|
+
eventBus.emit("circuit_state_change", {
|
|
1005
|
+
target_id: targetId,
|
|
1006
|
+
from,
|
|
1007
|
+
to,
|
|
1008
|
+
reason: `state_transition`
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
};
|
|
1013
|
+
subscribeEvents(current.policy);
|
|
1014
|
+
const effectiveState = () => {
|
|
1015
|
+
const raw = current.policy.state;
|
|
1016
|
+
if (raw === import_cockatiel2.CircuitState.Open && openedAtMs > 0 && Date.now() - openedAtMs >= config.half_open_after_ms) {
|
|
1017
|
+
return import_cockatiel2.CircuitState.HalfOpen;
|
|
1018
|
+
}
|
|
1019
|
+
return raw;
|
|
1020
|
+
};
|
|
1021
|
+
return {
|
|
1022
|
+
state() {
|
|
1023
|
+
return toTargetState(effectiveState());
|
|
1024
|
+
},
|
|
1025
|
+
async recordSuccess() {
|
|
1026
|
+
try {
|
|
1027
|
+
await current.policy.execute(async () => void 0);
|
|
1028
|
+
} catch (err) {
|
|
1029
|
+
if (err instanceof import_cockatiel2.BrokenCircuitError || err instanceof import_cockatiel2.IsolatedCircuitError) {
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
throw err;
|
|
1033
|
+
}
|
|
1034
|
+
},
|
|
1035
|
+
async recordFailure() {
|
|
1036
|
+
try {
|
|
1037
|
+
await current.policy.execute(async () => {
|
|
1038
|
+
throw new Error("recorded-failure");
|
|
1039
|
+
});
|
|
1040
|
+
} catch {
|
|
1041
|
+
}
|
|
1042
|
+
},
|
|
1043
|
+
allowRequest() {
|
|
1044
|
+
return effectiveState() !== import_cockatiel2.CircuitState.Open;
|
|
1045
|
+
},
|
|
1046
|
+
reset() {
|
|
1047
|
+
current = createPolicy();
|
|
1048
|
+
previousState = import_cockatiel2.CircuitState.Closed;
|
|
1049
|
+
openedAtMs = 0;
|
|
1050
|
+
subscribeEvents(current.policy);
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1055
|
+
// src/routing/concurrency-tracker.ts
|
|
1056
|
+
var createConcurrencyTracker = (defaultLimit) => {
|
|
1057
|
+
const state = /* @__PURE__ */ new Map();
|
|
1058
|
+
const getOrCreate = (target_id) => {
|
|
1059
|
+
const existing = state.get(target_id);
|
|
1060
|
+
if (existing) return existing;
|
|
1061
|
+
const entry = { active: 0, limit: defaultLimit };
|
|
1062
|
+
state.set(target_id, entry);
|
|
1063
|
+
return entry;
|
|
1064
|
+
};
|
|
1065
|
+
return {
|
|
1066
|
+
acquire(target_id) {
|
|
1067
|
+
const entry = getOrCreate(target_id);
|
|
1068
|
+
if (entry.active >= entry.limit) return false;
|
|
1069
|
+
entry.active += 1;
|
|
1070
|
+
return true;
|
|
1071
|
+
},
|
|
1072
|
+
release(target_id) {
|
|
1073
|
+
const entry = state.get(target_id);
|
|
1074
|
+
if (!entry || entry.active <= 0) return;
|
|
1075
|
+
entry.active -= 1;
|
|
1076
|
+
},
|
|
1077
|
+
headroom(target_id) {
|
|
1078
|
+
const entry = getOrCreate(target_id);
|
|
1079
|
+
return Math.max(0, entry.limit - entry.active);
|
|
1080
|
+
},
|
|
1081
|
+
active(target_id) {
|
|
1082
|
+
const entry = state.get(target_id);
|
|
1083
|
+
return entry ? entry.active : 0;
|
|
1084
|
+
},
|
|
1085
|
+
setLimit(target_id, limit) {
|
|
1086
|
+
const entry = getOrCreate(target_id);
|
|
1087
|
+
entry.limit = limit;
|
|
1088
|
+
}
|
|
1089
|
+
};
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
// src/routing/policy-engine.ts
|
|
1093
|
+
var normalizeLatency = (latencyEmaMs, maxExpectedMs) => Math.min(latencyEmaMs / maxExpectedMs, 1);
|
|
1094
|
+
var computeScore = (target, weights, maxLatencyMs) => weights.health * target.health_score - weights.latency * normalizeLatency(target.latency_ema_ms, maxLatencyMs) - weights.failure * target.failure_score + weights.priority * target.operator_priority;
|
|
1095
|
+
var getExclusionReason = (target, circuitBreakers, requiredCapabilities, excludeTargets, nowMs) => {
|
|
1096
|
+
if (!target.enabled) {
|
|
1097
|
+
return "disabled";
|
|
1098
|
+
}
|
|
1099
|
+
if (target.state === "PolicyBlocked") {
|
|
1100
|
+
return "policy_blocked";
|
|
1101
|
+
}
|
|
1102
|
+
if (target.state === "ReauthRequired") {
|
|
1103
|
+
return "reauth_required";
|
|
1104
|
+
}
|
|
1105
|
+
if (target.state === "Draining") {
|
|
1106
|
+
return "draining";
|
|
1107
|
+
}
|
|
1108
|
+
if (target.state === "Disabled") {
|
|
1109
|
+
return "disabled";
|
|
1110
|
+
}
|
|
1111
|
+
const breaker = circuitBreakers.get(target.target_id);
|
|
1112
|
+
if (breaker !== void 0 && !breaker.allowRequest()) {
|
|
1113
|
+
return "circuit_open";
|
|
1114
|
+
}
|
|
1115
|
+
if (target.cooldown_until !== null && target.cooldown_until > nowMs) {
|
|
1116
|
+
return "cooldown_active";
|
|
1117
|
+
}
|
|
1118
|
+
if (requiredCapabilities.length > 0 && !requiredCapabilities.every((cap) => target.capabilities.includes(cap))) {
|
|
1119
|
+
return "capability_mismatch";
|
|
1120
|
+
}
|
|
1121
|
+
return null;
|
|
1122
|
+
};
|
|
1123
|
+
var selectTarget = (snapshot, circuitBreakers, requiredCapabilities, weights, maxLatencyMs, nowMs, request_id, excludeTargets, logger) => {
|
|
1124
|
+
const excluded = [];
|
|
1125
|
+
const eligible = [];
|
|
1126
|
+
for (const target of snapshot.targets) {
|
|
1127
|
+
if (excludeTargets !== void 0 && excludeTargets.has(target.target_id)) {
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
const reason = getExclusionReason(
|
|
1131
|
+
target,
|
|
1132
|
+
circuitBreakers,
|
|
1133
|
+
requiredCapabilities,
|
|
1134
|
+
excludeTargets ?? /* @__PURE__ */ new Set(),
|
|
1135
|
+
nowMs
|
|
1136
|
+
);
|
|
1137
|
+
if (reason !== null) {
|
|
1138
|
+
excluded.push({ target_id: target.target_id, reason });
|
|
1139
|
+
} else {
|
|
1140
|
+
eligible.push(target);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
const scored = eligible.map((target) => ({
|
|
1144
|
+
target,
|
|
1145
|
+
score: computeScore(target, weights, maxLatencyMs)
|
|
1146
|
+
}));
|
|
1147
|
+
scored.sort((a, b) => {
|
|
1148
|
+
const scoreDiff = b.score - a.score;
|
|
1149
|
+
if (scoreDiff !== 0) {
|
|
1150
|
+
return scoreDiff;
|
|
1151
|
+
}
|
|
1152
|
+
return a.target.target_id.localeCompare(b.target.target_id);
|
|
1153
|
+
});
|
|
1154
|
+
const candidates = scored.map((s) => ({
|
|
1155
|
+
target_id: s.target.target_id,
|
|
1156
|
+
score: s.score,
|
|
1157
|
+
health_score: s.target.health_score,
|
|
1158
|
+
latency_ema_ms: s.target.latency_ema_ms
|
|
1159
|
+
}));
|
|
1160
|
+
const firstCandidate = candidates.length > 0 ? candidates[0] : void 0;
|
|
1161
|
+
const selected = firstCandidate !== void 0 ? {
|
|
1162
|
+
target_id: firstCandidate.target_id,
|
|
1163
|
+
score: firstCandidate.score,
|
|
1164
|
+
rank: 0
|
|
1165
|
+
} : null;
|
|
1166
|
+
const record = {
|
|
1167
|
+
request_id,
|
|
1168
|
+
timestamp_ms: nowMs,
|
|
1169
|
+
candidates,
|
|
1170
|
+
excluded,
|
|
1171
|
+
selected
|
|
1172
|
+
};
|
|
1173
|
+
if (logger !== void 0) {
|
|
1174
|
+
const excludedSummary = {};
|
|
1175
|
+
for (const e of excluded) {
|
|
1176
|
+
excludedSummary[e.reason] = (excludedSummary[e.reason] ?? 0) + 1;
|
|
1177
|
+
}
|
|
1178
|
+
const logFields = {
|
|
1179
|
+
event: "target_selected",
|
|
1180
|
+
component: "policy-engine",
|
|
1181
|
+
request_id,
|
|
1182
|
+
selected: selected?.target_id ?? null,
|
|
1183
|
+
score: selected?.score ?? null,
|
|
1184
|
+
candidates_count: candidates.length,
|
|
1185
|
+
excluded_count: excluded.length,
|
|
1186
|
+
excluded_summary: excludedSummary
|
|
1187
|
+
};
|
|
1188
|
+
if (selected !== null) {
|
|
1189
|
+
logger.info(logFields, "Target selected");
|
|
1190
|
+
} else {
|
|
1191
|
+
logger.warn(logFields, "No eligible target");
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
return record;
|
|
1195
|
+
};
|
|
1196
|
+
|
|
1197
|
+
// src/routing/cooldown.ts
|
|
1198
|
+
var DEFAULT_COOLDOWN_BASE_MS = 5e3;
|
|
1199
|
+
var computeCooldownMs = (errorClass, consecutiveFailures, backoffParams, maxMs) => {
|
|
1200
|
+
const noJitterParams = {
|
|
1201
|
+
...backoffParams,
|
|
1202
|
+
jitter: "none"
|
|
1203
|
+
};
|
|
1204
|
+
let base;
|
|
1205
|
+
if (errorClass.class === "RateLimited" && "retry_after_ms" in errorClass && errorClass.retry_after_ms !== void 0) {
|
|
1206
|
+
base = Math.min(errorClass.retry_after_ms, maxMs);
|
|
1207
|
+
} else if (errorClass.class === "RateLimited") {
|
|
1208
|
+
base = computeBackoffMs(consecutiveFailures, noJitterParams);
|
|
1209
|
+
} else if (errorClass.class === "TransientServerFailure") {
|
|
1210
|
+
base = computeBackoffMs(consecutiveFailures, noJitterParams) / 2;
|
|
1211
|
+
} else {
|
|
1212
|
+
base = DEFAULT_COOLDOWN_BASE_MS;
|
|
1213
|
+
}
|
|
1214
|
+
base = Math.min(base, maxMs);
|
|
1215
|
+
const jitterFraction = 0.1 + Math.random() * 0.15;
|
|
1216
|
+
const jitter = jitterFraction * base;
|
|
1217
|
+
return Math.min(base + jitter, maxMs);
|
|
1218
|
+
};
|
|
1219
|
+
var createCooldownManager = (registry, eventBus) => ({
|
|
1220
|
+
setCooldown(target_id, durationMs, errorClass) {
|
|
1221
|
+
const untilMs = Date.now() + durationMs;
|
|
1222
|
+
registry.setCooldown(target_id, untilMs);
|
|
1223
|
+
registry.updateState(target_id, "CoolingDown");
|
|
1224
|
+
eventBus?.emit("cooldown_set", {
|
|
1225
|
+
target_id,
|
|
1226
|
+
until_ms: untilMs,
|
|
1227
|
+
error_class: errorClass
|
|
1228
|
+
});
|
|
1229
|
+
},
|
|
1230
|
+
checkExpired(nowMs) {
|
|
1231
|
+
const expired = [];
|
|
1232
|
+
for (const target of registry.getAllTargets()) {
|
|
1233
|
+
if (target.state === "CoolingDown" && target.cooldown_until !== null && target.cooldown_until <= nowMs) {
|
|
1234
|
+
registry.setCooldown(target.target_id, 0);
|
|
1235
|
+
registry.updateState(target.target_id, "Active");
|
|
1236
|
+
eventBus?.emit("cooldown_expired", { target_id: target.target_id });
|
|
1237
|
+
expired.push(target.target_id);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
return expired;
|
|
1241
|
+
},
|
|
1242
|
+
isInCooldown(target_id, nowMs) {
|
|
1243
|
+
const target = registry.getTarget(target_id);
|
|
1244
|
+
if (!target) {
|
|
1245
|
+
return false;
|
|
1246
|
+
}
|
|
1247
|
+
return target.cooldown_until !== null && target.cooldown_until > nowMs;
|
|
1248
|
+
}
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
// src/routing/admission.ts
|
|
1252
|
+
var import_p_queue = __toESM(require("p-queue"), 1);
|
|
1253
|
+
var checkHardRejects = (ctx) => {
|
|
1254
|
+
if (!ctx.hasEligibleTargets) {
|
|
1255
|
+
return { result: "rejected", reason: "no eligible targets available" };
|
|
1256
|
+
}
|
|
1257
|
+
if (ctx.operatorPaused) {
|
|
1258
|
+
return { result: "rejected", reason: "operator pause active" };
|
|
1259
|
+
}
|
|
1260
|
+
if (ctx.retryBudgetRemaining <= 0) {
|
|
1261
|
+
return { result: "rejected", reason: "retry budget exhausted" };
|
|
1262
|
+
}
|
|
1263
|
+
if (ctx.failoverBudgetRemaining <= 0) {
|
|
1264
|
+
return { result: "rejected", reason: "failover budget exhausted" };
|
|
1265
|
+
}
|
|
1266
|
+
return null;
|
|
1267
|
+
};
|
|
1268
|
+
var createAdmissionController = (config, eventBus) => {
|
|
1269
|
+
const queue = new import_p_queue.default({
|
|
1270
|
+
concurrency: config.concurrencyLimit
|
|
1271
|
+
});
|
|
1272
|
+
const emitDecision = (request_id, decision) => {
|
|
1273
|
+
eventBus?.emit("admission_decision", {
|
|
1274
|
+
request_id,
|
|
1275
|
+
result: decision.result,
|
|
1276
|
+
reason: decision.reason
|
|
1277
|
+
});
|
|
1278
|
+
};
|
|
1279
|
+
const controller = {
|
|
1280
|
+
async admit(request_id, ctx) {
|
|
1281
|
+
const hardReject = checkHardRejects(ctx);
|
|
1282
|
+
if (hardReject) {
|
|
1283
|
+
emitDecision(request_id, hardReject);
|
|
1284
|
+
return hardReject;
|
|
1285
|
+
}
|
|
1286
|
+
if (queue.size > config.backpressureThreshold) {
|
|
1287
|
+
const decision2 = {
|
|
1288
|
+
result: "degraded",
|
|
1289
|
+
reason: `backpressure: ${queue.size} queued > ${config.backpressureThreshold} threshold`
|
|
1290
|
+
};
|
|
1291
|
+
emitDecision(request_id, decision2);
|
|
1292
|
+
return decision2;
|
|
1293
|
+
}
|
|
1294
|
+
const total = queue.size + queue.pending;
|
|
1295
|
+
if (total >= config.queueLimit + config.concurrencyLimit) {
|
|
1296
|
+
const decision2 = {
|
|
1297
|
+
result: "rejected",
|
|
1298
|
+
reason: `queue full: ${total} >= ${config.queueLimit + config.concurrencyLimit}`
|
|
1299
|
+
};
|
|
1300
|
+
emitDecision(request_id, decision2);
|
|
1301
|
+
return decision2;
|
|
1302
|
+
}
|
|
1303
|
+
const decision = {
|
|
1304
|
+
result: "admitted",
|
|
1305
|
+
reason: "capacity available"
|
|
1306
|
+
};
|
|
1307
|
+
emitDecision(request_id, decision);
|
|
1308
|
+
return decision;
|
|
1309
|
+
},
|
|
1310
|
+
async execute(fn, request_id, ctx) {
|
|
1311
|
+
const decision = await controller.admit(request_id, ctx);
|
|
1312
|
+
if (decision.result === "rejected") {
|
|
1313
|
+
return { decision };
|
|
1314
|
+
}
|
|
1315
|
+
const result = await queue.add(fn);
|
|
1316
|
+
return { decision, result };
|
|
1317
|
+
},
|
|
1318
|
+
get pending() {
|
|
1319
|
+
return queue.pending;
|
|
1320
|
+
},
|
|
1321
|
+
get size() {
|
|
1322
|
+
return queue.size;
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
return controller;
|
|
1326
|
+
};
|
|
1327
|
+
|
|
1328
|
+
// src/routing/log-subscriber.ts
|
|
1329
|
+
var createLogSubscriber = (eventBus, logger) => {
|
|
1330
|
+
const log = logger.child({ component: "routing" });
|
|
1331
|
+
const onCircuitStateChange = (payload) => {
|
|
1332
|
+
log.info(
|
|
1333
|
+
{
|
|
1334
|
+
event: "circuit_state_change",
|
|
1335
|
+
target_id: payload.target_id,
|
|
1336
|
+
from: payload.from,
|
|
1337
|
+
to: payload.to,
|
|
1338
|
+
reason: payload.reason
|
|
1339
|
+
},
|
|
1340
|
+
"Circuit breaker transition"
|
|
1341
|
+
);
|
|
1342
|
+
};
|
|
1343
|
+
const onCooldownSet = (payload) => {
|
|
1344
|
+
log.info(
|
|
1345
|
+
{
|
|
1346
|
+
event: "cooldown_set",
|
|
1347
|
+
target_id: payload.target_id,
|
|
1348
|
+
duration_ms: payload.until_ms - Date.now(),
|
|
1349
|
+
error_class: payload.error_class
|
|
1350
|
+
},
|
|
1351
|
+
"Cooldown set"
|
|
1352
|
+
);
|
|
1353
|
+
};
|
|
1354
|
+
const onCooldownExpired = (payload) => {
|
|
1355
|
+
log.info(
|
|
1356
|
+
{
|
|
1357
|
+
event: "cooldown_expired",
|
|
1358
|
+
target_id: payload.target_id
|
|
1359
|
+
},
|
|
1360
|
+
"Cooldown expired"
|
|
1361
|
+
);
|
|
1362
|
+
};
|
|
1363
|
+
const onAdmissionDecision = (payload) => {
|
|
1364
|
+
const fields = {
|
|
1365
|
+
event: "admission_decision",
|
|
1366
|
+
request_id: payload.request_id,
|
|
1367
|
+
result: payload.result,
|
|
1368
|
+
reason: payload.reason
|
|
1369
|
+
};
|
|
1370
|
+
if (payload.result === "admitted" || payload.result === "queued") {
|
|
1371
|
+
log.info(fields, "Admission decision");
|
|
1372
|
+
} else {
|
|
1373
|
+
log.warn(fields, "Admission decision");
|
|
1374
|
+
}
|
|
1375
|
+
};
|
|
1376
|
+
const onHealthUpdated = (payload) => {
|
|
1377
|
+
log.debug(
|
|
1378
|
+
{
|
|
1379
|
+
event: "health_updated",
|
|
1380
|
+
target_id: payload.target_id,
|
|
1381
|
+
old_score: payload.old_score,
|
|
1382
|
+
new_score: payload.new_score
|
|
1383
|
+
},
|
|
1384
|
+
"Health score updated"
|
|
1385
|
+
);
|
|
1386
|
+
};
|
|
1387
|
+
const onTargetExcluded = (payload) => {
|
|
1388
|
+
log.debug(
|
|
1389
|
+
{
|
|
1390
|
+
event: "target_excluded",
|
|
1391
|
+
target_id: payload.target_id,
|
|
1392
|
+
reason: payload.reason
|
|
1393
|
+
},
|
|
1394
|
+
"Target excluded"
|
|
1395
|
+
);
|
|
1396
|
+
};
|
|
1397
|
+
eventBus.on("circuit_state_change", onCircuitStateChange);
|
|
1398
|
+
eventBus.on("cooldown_set", onCooldownSet);
|
|
1399
|
+
eventBus.on("cooldown_expired", onCooldownExpired);
|
|
1400
|
+
eventBus.on("admission_decision", onAdmissionDecision);
|
|
1401
|
+
eventBus.on("health_updated", onHealthUpdated);
|
|
1402
|
+
eventBus.on("target_excluded", onTargetExcluded);
|
|
1403
|
+
return () => {
|
|
1404
|
+
eventBus.off("circuit_state_change", onCircuitStateChange);
|
|
1405
|
+
eventBus.off("cooldown_set", onCooldownSet);
|
|
1406
|
+
eventBus.off("cooldown_expired", onCooldownExpired);
|
|
1407
|
+
eventBus.off("admission_decision", onAdmissionDecision);
|
|
1408
|
+
eventBus.off("health_updated", onHealthUpdated);
|
|
1409
|
+
eventBus.off("target_excluded", onTargetExcluded);
|
|
1410
|
+
};
|
|
1411
|
+
};
|
|
1412
|
+
|
|
1413
|
+
// src/routing/failover.ts
|
|
1414
|
+
var createFailoverOrchestrator = (deps) => ({
|
|
1415
|
+
async execute(request_id, attemptFn, requiredCapabilities) {
|
|
1416
|
+
const log = deps.logger?.child({ component: "failover", request_id });
|
|
1417
|
+
const attempts = [];
|
|
1418
|
+
const failedTargets = /* @__PURE__ */ new Set();
|
|
1419
|
+
let totalRetries = 0;
|
|
1420
|
+
let totalFailovers = 0;
|
|
1421
|
+
let lastErrorClass;
|
|
1422
|
+
log?.info(
|
|
1423
|
+
{ event: "failover_start", retry_budget: deps.retryBudget, failover_budget: deps.failoverBudget },
|
|
1424
|
+
"Failover loop started"
|
|
1425
|
+
);
|
|
1426
|
+
for (let failoverNo = 0; failoverNo <= deps.failoverBudget; failoverNo++) {
|
|
1427
|
+
deps.cooldownManager.checkExpired(Date.now());
|
|
1428
|
+
const snapshot = deps.registry.getSnapshot();
|
|
1429
|
+
const selection = selectTarget(
|
|
1430
|
+
snapshot,
|
|
1431
|
+
deps.circuitBreakers,
|
|
1432
|
+
requiredCapabilities,
|
|
1433
|
+
deps.weights,
|
|
1434
|
+
deps.maxLatencyMs,
|
|
1435
|
+
Date.now(),
|
|
1436
|
+
request_id,
|
|
1437
|
+
failedTargets
|
|
1438
|
+
);
|
|
1439
|
+
if (!selection.selected) {
|
|
1440
|
+
log?.warn(
|
|
1441
|
+
{ event: "no_eligible_target", failover_no: failoverNo },
|
|
1442
|
+
"No eligible target available"
|
|
1443
|
+
);
|
|
1444
|
+
return {
|
|
1445
|
+
outcome: "failure",
|
|
1446
|
+
reason: "no eligible target available",
|
|
1447
|
+
attempts,
|
|
1448
|
+
total_retries: totalRetries,
|
|
1449
|
+
total_failovers: totalFailovers,
|
|
1450
|
+
last_error_class: lastErrorClass
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
const targetId = selection.selected.target_id;
|
|
1454
|
+
const retryPolicy = createRetryPolicy(
|
|
1455
|
+
deps.retryBudget,
|
|
1456
|
+
deps.backoffParams
|
|
1457
|
+
);
|
|
1458
|
+
for (let retry = 0; retry <= deps.retryBudget; retry++) {
|
|
1459
|
+
const attemptNo = retry + 1;
|
|
1460
|
+
const acquired = deps.concurrency.acquire(targetId);
|
|
1461
|
+
if (!acquired) {
|
|
1462
|
+
log?.warn(
|
|
1463
|
+
{ event: "concurrency_full", target_id: targetId, failover_no: failoverNo },
|
|
1464
|
+
"No concurrency headroom \u2014 failing over"
|
|
1465
|
+
);
|
|
1466
|
+
failedTargets.add(targetId);
|
|
1467
|
+
break;
|
|
1468
|
+
}
|
|
1469
|
+
let attemptResult;
|
|
1470
|
+
try {
|
|
1471
|
+
attemptResult = await attemptFn(targetId);
|
|
1472
|
+
} finally {
|
|
1473
|
+
deps.concurrency.release(targetId);
|
|
1474
|
+
}
|
|
1475
|
+
if (attemptResult.success) {
|
|
1476
|
+
const cb2 = deps.circuitBreakers.get(targetId);
|
|
1477
|
+
if (cb2) {
|
|
1478
|
+
await cb2.recordSuccess();
|
|
1479
|
+
}
|
|
1480
|
+
deps.registry.recordObservation(targetId, 1);
|
|
1481
|
+
deps.registry.updateLatency(targetId, attemptResult.latency_ms);
|
|
1482
|
+
log?.info(
|
|
1483
|
+
{ event: "attempt_success", target_id: targetId, attempt_no: attemptNo, failover_no: failoverNo, latency_ms: attemptResult.latency_ms },
|
|
1484
|
+
"Attempt succeeded"
|
|
1485
|
+
);
|
|
1486
|
+
attempts.push({
|
|
1487
|
+
target_id: targetId,
|
|
1488
|
+
attempt_no: attemptNo,
|
|
1489
|
+
failover_no: failoverNo,
|
|
1490
|
+
outcome: "success",
|
|
1491
|
+
latency_ms: attemptResult.latency_ms,
|
|
1492
|
+
selection
|
|
1493
|
+
});
|
|
1494
|
+
return {
|
|
1495
|
+
outcome: "success",
|
|
1496
|
+
target_id: targetId,
|
|
1497
|
+
attempts,
|
|
1498
|
+
total_retries: totalRetries,
|
|
1499
|
+
total_failovers: totalFailovers,
|
|
1500
|
+
value: attemptResult.value
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
const errorClass = attemptResult.error_class;
|
|
1504
|
+
lastErrorClass = errorClass.class;
|
|
1505
|
+
const cb = deps.circuitBreakers.get(targetId);
|
|
1506
|
+
if (cb) {
|
|
1507
|
+
await cb.recordFailure();
|
|
1508
|
+
}
|
|
1509
|
+
deps.registry.recordObservation(targetId, 0);
|
|
1510
|
+
const stateTransition = getTargetStateTransition(errorClass);
|
|
1511
|
+
if (stateTransition) {
|
|
1512
|
+
deps.registry.updateState(targetId, stateTransition);
|
|
1513
|
+
}
|
|
1514
|
+
if (errorClass.class === "RateLimited") {
|
|
1515
|
+
const cooldownMs = computeCooldownMs(
|
|
1516
|
+
errorClass,
|
|
1517
|
+
retry,
|
|
1518
|
+
deps.backoffParams,
|
|
1519
|
+
deps.backoffParams.max_ms
|
|
1520
|
+
);
|
|
1521
|
+
deps.cooldownManager.setCooldown(
|
|
1522
|
+
targetId,
|
|
1523
|
+
cooldownMs,
|
|
1524
|
+
errorClass.class
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1527
|
+
if (errorClass.class === "ModelUnavailable") {
|
|
1528
|
+
log?.info(
|
|
1529
|
+
{ event: "early_failover", target_id: targetId, error_class: errorClass.class, failover_no: failoverNo },
|
|
1530
|
+
"Early failover \u2014 ModelUnavailable"
|
|
1531
|
+
);
|
|
1532
|
+
attempts.push({
|
|
1533
|
+
target_id: targetId,
|
|
1534
|
+
attempt_no: attemptNo,
|
|
1535
|
+
failover_no: failoverNo,
|
|
1536
|
+
outcome: "failover",
|
|
1537
|
+
error_class: errorClass.class,
|
|
1538
|
+
latency_ms: attemptResult.latency_ms,
|
|
1539
|
+
selection
|
|
1540
|
+
});
|
|
1541
|
+
failedTargets.add(targetId);
|
|
1542
|
+
totalFailovers++;
|
|
1543
|
+
break;
|
|
1544
|
+
}
|
|
1545
|
+
if (!isRetryable(errorClass)) {
|
|
1546
|
+
log?.warn(
|
|
1547
|
+
{ event: "abort", target_id: targetId, error_class: errorClass.class },
|
|
1548
|
+
"Non-retryable error \u2014 aborting"
|
|
1549
|
+
);
|
|
1550
|
+
attempts.push({
|
|
1551
|
+
target_id: targetId,
|
|
1552
|
+
attempt_no: attemptNo,
|
|
1553
|
+
failover_no: failoverNo,
|
|
1554
|
+
outcome: "abort",
|
|
1555
|
+
error_class: errorClass.class,
|
|
1556
|
+
latency_ms: attemptResult.latency_ms,
|
|
1557
|
+
selection
|
|
1558
|
+
});
|
|
1559
|
+
return {
|
|
1560
|
+
outcome: "failure",
|
|
1561
|
+
reason: `non-retryable: ${errorClass.class}`,
|
|
1562
|
+
attempts,
|
|
1563
|
+
total_retries: totalRetries,
|
|
1564
|
+
total_failovers: totalFailovers,
|
|
1565
|
+
last_error_class: errorClass.class
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
const decision = retryPolicy.decide(
|
|
1569
|
+
{
|
|
1570
|
+
error_class: errorClass,
|
|
1571
|
+
detection_mode: "direct",
|
|
1572
|
+
confidence: "high",
|
|
1573
|
+
raw_signal: { detection_mode: "direct" }
|
|
1574
|
+
},
|
|
1575
|
+
retry
|
|
1576
|
+
);
|
|
1577
|
+
if (decision.action === "exhausted") {
|
|
1578
|
+
log?.info(
|
|
1579
|
+
{ event: "retry_budget_exhausted", target_id: targetId, failover_no: failoverNo },
|
|
1580
|
+
"Retry budget exhausted \u2014 failing over"
|
|
1581
|
+
);
|
|
1582
|
+
attempts.push({
|
|
1583
|
+
target_id: targetId,
|
|
1584
|
+
attempt_no: attemptNo,
|
|
1585
|
+
failover_no: failoverNo,
|
|
1586
|
+
outcome: "failover",
|
|
1587
|
+
error_class: errorClass.class,
|
|
1588
|
+
latency_ms: attemptResult.latency_ms,
|
|
1589
|
+
selection
|
|
1590
|
+
});
|
|
1591
|
+
failedTargets.add(targetId);
|
|
1592
|
+
totalFailovers++;
|
|
1593
|
+
break;
|
|
1594
|
+
}
|
|
1595
|
+
attempts.push({
|
|
1596
|
+
target_id: targetId,
|
|
1597
|
+
attempt_no: attemptNo,
|
|
1598
|
+
failover_no: failoverNo,
|
|
1599
|
+
outcome: "retry",
|
|
1600
|
+
error_class: errorClass.class,
|
|
1601
|
+
latency_ms: attemptResult.latency_ms,
|
|
1602
|
+
selection
|
|
1603
|
+
});
|
|
1604
|
+
totalRetries++;
|
|
1605
|
+
if (decision.action === "retry") {
|
|
1606
|
+
log?.info(
|
|
1607
|
+
{ event: "retry", target_id: targetId, attempt_no: attemptNo, error_class: errorClass.class, delay_ms: decision.delay_ms },
|
|
1608
|
+
"Retrying after delay"
|
|
1609
|
+
);
|
|
1610
|
+
await new Promise(
|
|
1611
|
+
(resolve) => setTimeout(resolve, decision.delay_ms)
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
log?.warn(
|
|
1617
|
+
{ event: "failover_budget_exhausted", total_retries: totalRetries, total_failovers: totalFailovers },
|
|
1618
|
+
"Failover budget exhausted"
|
|
1619
|
+
);
|
|
1620
|
+
return {
|
|
1621
|
+
outcome: "failure",
|
|
1622
|
+
reason: "failover budget exhausted",
|
|
1623
|
+
attempts,
|
|
1624
|
+
total_retries: totalRetries,
|
|
1625
|
+
total_failovers: totalFailovers,
|
|
1626
|
+
last_error_class: lastErrorClass
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
// src/execution/stream-buffer.ts
|
|
1632
|
+
var createStreamBuffer = () => {
|
|
1633
|
+
const chunks = [];
|
|
1634
|
+
return {
|
|
1635
|
+
/**
|
|
1636
|
+
* Appends a chunk to the buffer.
|
|
1637
|
+
*
|
|
1638
|
+
* @param chunk - The stream chunk to append.
|
|
1639
|
+
* @returns The index of the appended chunk (0-based).
|
|
1640
|
+
*/
|
|
1641
|
+
append(chunk) {
|
|
1642
|
+
chunks.push(chunk);
|
|
1643
|
+
return chunks.length - 1;
|
|
1644
|
+
},
|
|
1645
|
+
/**
|
|
1646
|
+
* Returns a readonly copy of all accumulated chunks.
|
|
1647
|
+
*
|
|
1648
|
+
* The returned array is a shallow copy; mutations to it
|
|
1649
|
+
* do not affect the internal buffer.
|
|
1650
|
+
*
|
|
1651
|
+
* @returns Readonly array of confirmed chunks.
|
|
1652
|
+
*/
|
|
1653
|
+
confirmed() {
|
|
1654
|
+
return [...chunks];
|
|
1655
|
+
},
|
|
1656
|
+
/**
|
|
1657
|
+
* Returns the total character count across all confirmed chunks.
|
|
1658
|
+
*
|
|
1659
|
+
* Used to compute visible_offset for segment provenance.
|
|
1660
|
+
*
|
|
1661
|
+
* @returns Total character count.
|
|
1662
|
+
*/
|
|
1663
|
+
confirmedCharCount() {
|
|
1664
|
+
return chunks.reduce((sum, c) => sum + c.text.length, 0);
|
|
1665
|
+
},
|
|
1666
|
+
/**
|
|
1667
|
+
* Returns the concatenated text of all confirmed chunks.
|
|
1668
|
+
*
|
|
1669
|
+
* This is the confirmed output that can be preserved on
|
|
1670
|
+
* interruption and used as prefix for stream stitching.
|
|
1671
|
+
*
|
|
1672
|
+
* @returns Concatenated confirmed text.
|
|
1673
|
+
*/
|
|
1674
|
+
snapshot() {
|
|
1675
|
+
return chunks.map((c) => c.text).join("");
|
|
1676
|
+
}
|
|
1677
|
+
};
|
|
1678
|
+
};
|
|
1679
|
+
|
|
1680
|
+
// src/execution/stream-stitcher.ts
|
|
1681
|
+
var determineContinuationMode = (oldTargetId, newTargetId, sameModel) => {
|
|
1682
|
+
if (oldTargetId === newTargetId) {
|
|
1683
|
+
return "same_target_resume";
|
|
1684
|
+
}
|
|
1685
|
+
return sameModel ? "same_model_alternate_target" : "cross_model_semantic";
|
|
1686
|
+
};
|
|
1687
|
+
var createStreamStitcher = (_requestId) => {
|
|
1688
|
+
const segments = [];
|
|
1689
|
+
return {
|
|
1690
|
+
/**
|
|
1691
|
+
* Adds a segment to the stitcher.
|
|
1692
|
+
*
|
|
1693
|
+
* Computes visible_offset as the cumulative character count
|
|
1694
|
+
* of all previously added segments.
|
|
1695
|
+
*
|
|
1696
|
+
* @param buffer - The stream buffer containing the segment's chunks.
|
|
1697
|
+
* @param provenance - Segment provenance without visible_offset (computed here).
|
|
1698
|
+
*/
|
|
1699
|
+
addSegment(buffer, provenance) {
|
|
1700
|
+
const visibleOffset = segments.reduce(
|
|
1701
|
+
(sum, entry) => sum + entry.text.length,
|
|
1702
|
+
0
|
|
1703
|
+
);
|
|
1704
|
+
segments.push({
|
|
1705
|
+
text: buffer.snapshot(),
|
|
1706
|
+
provenance: { ...provenance, visible_offset: visibleOffset }
|
|
1707
|
+
});
|
|
1708
|
+
},
|
|
1709
|
+
/**
|
|
1710
|
+
* Assembles the final stitched output.
|
|
1711
|
+
*
|
|
1712
|
+
* @returns StitchedOutput with concatenated text, segments, and boundaries.
|
|
1713
|
+
*/
|
|
1714
|
+
assemble() {
|
|
1715
|
+
const text = segments.map((s) => s.text).join("");
|
|
1716
|
+
const provenanceList = segments.map((s) => s.provenance);
|
|
1717
|
+
const continuationBoundaries = provenanceList.slice(1).map((p) => p.visible_offset);
|
|
1718
|
+
return {
|
|
1719
|
+
text,
|
|
1720
|
+
segments: provenanceList,
|
|
1721
|
+
continuation_boundaries: continuationBoundaries
|
|
1722
|
+
};
|
|
1723
|
+
},
|
|
1724
|
+
/**
|
|
1725
|
+
* Returns the number of segments added so far.
|
|
1726
|
+
*
|
|
1727
|
+
* @returns Segment count.
|
|
1728
|
+
*/
|
|
1729
|
+
segmentCount() {
|
|
1730
|
+
return segments.length;
|
|
1731
|
+
}
|
|
1732
|
+
};
|
|
1733
|
+
};
|
|
1734
|
+
|
|
1735
|
+
// src/execution/audit-collector.ts
|
|
1736
|
+
var createAuditCollector = (logger, requestId) => {
|
|
1737
|
+
const requestLogger = createRequestLogger(logger, requestId);
|
|
1738
|
+
const attempts = [];
|
|
1739
|
+
const segments = [];
|
|
1740
|
+
return {
|
|
1741
|
+
recordAttempt(attempt) {
|
|
1742
|
+
attempts.push(attempt);
|
|
1743
|
+
},
|
|
1744
|
+
recordSegment(segment) {
|
|
1745
|
+
segments.push(segment);
|
|
1746
|
+
},
|
|
1747
|
+
flush(outcome, finalTarget, stats) {
|
|
1748
|
+
requestLogger.info({
|
|
1749
|
+
event_type: "request_complete",
|
|
1750
|
+
outcome,
|
|
1751
|
+
final_target: finalTarget,
|
|
1752
|
+
total_attempts: attempts.length,
|
|
1753
|
+
total_segments: segments.length,
|
|
1754
|
+
total_retries: stats?.total_retries,
|
|
1755
|
+
total_failovers: stats?.total_failovers,
|
|
1756
|
+
latency_ms: stats?.latency_ms,
|
|
1757
|
+
attempts: [...attempts],
|
|
1758
|
+
segments: [...segments]
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
};
|
|
1762
|
+
};
|
|
1763
|
+
|
|
1764
|
+
// src/execution/orchestrator.ts
|
|
1765
|
+
var createExecutionOrchestrator = (deps) => ({
|
|
1766
|
+
async execute(request) {
|
|
1767
|
+
const requestId = request.request_id || generateCorrelationId();
|
|
1768
|
+
const auditCollector = createAuditCollector(deps.logger, requestId);
|
|
1769
|
+
const stitcher = createStreamStitcher(requestId);
|
|
1770
|
+
const startMs = Date.now();
|
|
1771
|
+
const heuristicFlags = [];
|
|
1772
|
+
let outcome = "failure";
|
|
1773
|
+
let finalTarget;
|
|
1774
|
+
let summaryAttempts = 0;
|
|
1775
|
+
let summaryRetries = 0;
|
|
1776
|
+
let summaryFailovers = 0;
|
|
1777
|
+
let summaryTargets = [];
|
|
1778
|
+
try {
|
|
1779
|
+
const failoverResult = await deps.failover.execute(
|
|
1780
|
+
requestId,
|
|
1781
|
+
async (targetId) => {
|
|
1782
|
+
const buffer = createStreamBuffer();
|
|
1783
|
+
const adapterResult = await deps.adapter.execute(targetId, request);
|
|
1784
|
+
for (const chunk of adapterResult.chunks) {
|
|
1785
|
+
buffer.append(chunk);
|
|
1786
|
+
}
|
|
1787
|
+
heuristicFlags.push(
|
|
1788
|
+
adapterResult.detection_mode === "heuristic"
|
|
1789
|
+
);
|
|
1790
|
+
const value = { buffer, adapterResult };
|
|
1791
|
+
return {
|
|
1792
|
+
success: adapterResult.success,
|
|
1793
|
+
value,
|
|
1794
|
+
error_class: adapterResult.error_class,
|
|
1795
|
+
latency_ms: adapterResult.latency_ms
|
|
1796
|
+
};
|
|
1797
|
+
},
|
|
1798
|
+
[]
|
|
1799
|
+
);
|
|
1800
|
+
for (const attempt of failoverResult.attempts) {
|
|
1801
|
+
auditCollector.recordAttempt(attempt);
|
|
1802
|
+
}
|
|
1803
|
+
summaryAttempts = failoverResult.attempts.length;
|
|
1804
|
+
summaryRetries = failoverResult.total_retries;
|
|
1805
|
+
summaryFailovers = failoverResult.total_failovers;
|
|
1806
|
+
summaryTargets = [...new Set(failoverResult.attempts.map((a) => a.target_id))];
|
|
1807
|
+
if (failoverResult.outcome === "success") {
|
|
1808
|
+
outcome = "success";
|
|
1809
|
+
finalTarget = failoverResult.target_id;
|
|
1810
|
+
const attemptValue = failoverResult.value;
|
|
1811
|
+
const continuationMode = stitcher.segmentCount() === 0 ? "same_target_resume" : "same_target_resume";
|
|
1812
|
+
stitcher.addSegment(attemptValue.buffer, {
|
|
1813
|
+
request_id: requestId,
|
|
1814
|
+
segment_id: stitcher.segmentCount(),
|
|
1815
|
+
source_target_id: failoverResult.target_id,
|
|
1816
|
+
continuation_mode: continuationMode,
|
|
1817
|
+
non_deterministic: false
|
|
1818
|
+
});
|
|
1819
|
+
const segmentProvenance = stitcher.assemble().segments;
|
|
1820
|
+
for (const seg of segmentProvenance) {
|
|
1821
|
+
auditCollector.recordSegment(seg);
|
|
1822
|
+
}
|
|
1823
|
+
const stitchedOutput = stitcher.assemble();
|
|
1824
|
+
const isDegraded2 = deps.mode === "plugin-only";
|
|
1825
|
+
const isHeuristic2 = heuristicFlags.some(Boolean);
|
|
1826
|
+
const uniqueModes = [
|
|
1827
|
+
...new Set(stitchedOutput.segments.map((s) => s.continuation_mode))
|
|
1828
|
+
];
|
|
1829
|
+
const uniqueTargets = [
|
|
1830
|
+
...new Set(stitchedOutput.segments.map((s) => s.source_target_id))
|
|
1831
|
+
];
|
|
1832
|
+
const provenance2 = {
|
|
1833
|
+
request_id: requestId,
|
|
1834
|
+
segments: stitchedOutput.segments,
|
|
1835
|
+
continuation_modes: uniqueModes,
|
|
1836
|
+
targets_involved: uniqueTargets,
|
|
1837
|
+
total_attempts: failoverResult.attempts.length,
|
|
1838
|
+
degraded: isDegraded2,
|
|
1839
|
+
degraded_reason: isDegraded2 ? "plugin-only mode: limited failover capability" : void 0,
|
|
1840
|
+
heuristic_detection: isHeuristic2
|
|
1841
|
+
};
|
|
1842
|
+
return {
|
|
1843
|
+
success: true,
|
|
1844
|
+
output: stitchedOutput.text,
|
|
1845
|
+
provenance: provenance2
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
const isDegraded = deps.mode === "plugin-only";
|
|
1849
|
+
const isHeuristic = heuristicFlags.some(Boolean);
|
|
1850
|
+
const provenance = {
|
|
1851
|
+
request_id: requestId,
|
|
1852
|
+
segments: [],
|
|
1853
|
+
continuation_modes: [],
|
|
1854
|
+
targets_involved: [],
|
|
1855
|
+
total_attempts: failoverResult.attempts.length,
|
|
1856
|
+
degraded: isDegraded,
|
|
1857
|
+
degraded_reason: isDegraded ? "plugin-only mode: limited failover capability" : void 0,
|
|
1858
|
+
heuristic_detection: isHeuristic
|
|
1859
|
+
};
|
|
1860
|
+
return {
|
|
1861
|
+
success: false,
|
|
1862
|
+
output: "",
|
|
1863
|
+
provenance
|
|
1864
|
+
};
|
|
1865
|
+
} finally {
|
|
1866
|
+
auditCollector.flush(outcome, finalTarget);
|
|
1867
|
+
const requestLogger = createRequestLogger(deps.logger, requestId);
|
|
1868
|
+
const endMs = Date.now();
|
|
1869
|
+
requestLogger.info({
|
|
1870
|
+
event: "request_summary",
|
|
1871
|
+
component: "execution",
|
|
1872
|
+
outcome,
|
|
1873
|
+
total_attempts: summaryAttempts,
|
|
1874
|
+
total_failovers: summaryFailovers,
|
|
1875
|
+
total_retries: summaryRetries,
|
|
1876
|
+
latency_ms: endMs - startMs,
|
|
1877
|
+
targets_used: summaryTargets,
|
|
1878
|
+
final_target: finalTarget ?? null
|
|
1879
|
+
}, "Request complete");
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
});
|
|
1883
|
+
|
|
1884
|
+
// src/execution/adapters/plugin-adapter.ts
|
|
1885
|
+
var createPluginAdapter = (_deps) => ({
|
|
1886
|
+
/**
|
|
1887
|
+
* Executes a request in plugin-only mode.
|
|
1888
|
+
*
|
|
1889
|
+
* Phase 3 stub: returns a not-yet-implemented result.
|
|
1890
|
+
* Phase 4 will wire this to OpenCode plugin lifecycle hooks.
|
|
1891
|
+
*
|
|
1892
|
+
* @param _targetId - The target to execute against.
|
|
1893
|
+
* @param _request - The adapter request payload.
|
|
1894
|
+
* @returns AdapterResult with heuristic detection mode.
|
|
1895
|
+
*/
|
|
1896
|
+
async execute(_targetId, _request) {
|
|
1897
|
+
const start = Date.now();
|
|
1898
|
+
return {
|
|
1899
|
+
success: false,
|
|
1900
|
+
chunks: [],
|
|
1901
|
+
error_class: void 0,
|
|
1902
|
+
latency_ms: Date.now() - start,
|
|
1903
|
+
detection_mode: "heuristic"
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
});
|
|
1907
|
+
|
|
1908
|
+
// src/execution/adapters/server-adapter.ts
|
|
1909
|
+
var createServerAdapter = (_deps) => ({
|
|
1910
|
+
/**
|
|
1911
|
+
* Executes a request in server-companion mode.
|
|
1912
|
+
*
|
|
1913
|
+
* Phase 3 stub: returns a not-yet-implemented result.
|
|
1914
|
+
* Phase 4 will wire this to the OpenCode SDK client, extracting
|
|
1915
|
+
* statusCode, responseHeaders['retry-after'], and responseBody
|
|
1916
|
+
* from SDK ApiError for direct error classification.
|
|
1917
|
+
*
|
|
1918
|
+
* @param _targetId - The target to execute against.
|
|
1919
|
+
* @param _request - The adapter request payload.
|
|
1920
|
+
* @returns AdapterResult with direct detection mode.
|
|
1921
|
+
*/
|
|
1922
|
+
async execute(_targetId, _request) {
|
|
1923
|
+
const start = Date.now();
|
|
1924
|
+
return {
|
|
1925
|
+
success: false,
|
|
1926
|
+
chunks: [],
|
|
1927
|
+
error_class: void 0,
|
|
1928
|
+
latency_ms: Date.now() - start,
|
|
1929
|
+
detection_mode: "direct"
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
});
|
|
1933
|
+
|
|
1934
|
+
// src/execution/adapters/sdk-adapter.ts
|
|
1935
|
+
var createSdkAdapter = (_deps) => ({
|
|
1936
|
+
/**
|
|
1937
|
+
* Executes a request in SDK-control mode.
|
|
1938
|
+
*
|
|
1939
|
+
* Phase 3 stub: returns a not-yet-implemented result.
|
|
1940
|
+
* Phase 4 will wire this to the OpenCode SDK client with full
|
|
1941
|
+
* session management and provider control capabilities.
|
|
1942
|
+
*
|
|
1943
|
+
* @param _targetId - The target to execute against.
|
|
1944
|
+
* @param _request - The adapter request payload.
|
|
1945
|
+
* @returns AdapterResult with direct detection mode.
|
|
1946
|
+
*/
|
|
1947
|
+
async execute(_targetId, _request) {
|
|
1948
|
+
const start = Date.now();
|
|
1949
|
+
return {
|
|
1950
|
+
success: false,
|
|
1951
|
+
chunks: [],
|
|
1952
|
+
error_class: void 0,
|
|
1953
|
+
latency_ms: Date.now() - start,
|
|
1954
|
+
detection_mode: "direct"
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
});
|
|
1958
|
+
|
|
1959
|
+
// src/execution/adapters/adapter-factory.ts
|
|
1960
|
+
var createModeAdapter = (mode, deps) => {
|
|
1961
|
+
switch (mode) {
|
|
1962
|
+
case "plugin-only":
|
|
1963
|
+
return createPluginAdapter(deps);
|
|
1964
|
+
case "server-companion":
|
|
1965
|
+
return createServerAdapter(deps);
|
|
1966
|
+
case "sdk-control":
|
|
1967
|
+
return createSdkAdapter(deps);
|
|
1968
|
+
default: {
|
|
1969
|
+
const _exhaustive = mode;
|
|
1970
|
+
throw new Error(`Unknown deployment mode: ${String(_exhaustive)}`);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
};
|
|
1974
|
+
|
|
1975
|
+
// src/operator/reload.ts
|
|
1976
|
+
var computeConfigDiff = (oldConfig, newConfig) => {
|
|
1977
|
+
const oldTargets = oldConfig.targets ?? [];
|
|
1978
|
+
const newTargets = newConfig.targets ?? [];
|
|
1979
|
+
const oldIds = new Set(oldTargets.map((t) => t.target_id));
|
|
1980
|
+
const newIds = new Set(newTargets.map((t) => t.target_id));
|
|
1981
|
+
const oldMap = new Map(oldTargets.map((t) => [t.target_id, t]));
|
|
1982
|
+
const added = newTargets.filter((t) => !oldIds.has(t.target_id));
|
|
1983
|
+
const removed = oldTargets.filter((t) => !newIds.has(t.target_id)).map((t) => t.target_id);
|
|
1984
|
+
const modified = newTargets.filter((t) => {
|
|
1985
|
+
if (!oldIds.has(t.target_id)) {
|
|
1986
|
+
return false;
|
|
1987
|
+
}
|
|
1988
|
+
const old = oldMap.get(t.target_id);
|
|
1989
|
+
if (!old) {
|
|
1990
|
+
return false;
|
|
1991
|
+
}
|
|
1992
|
+
return hasConfigChanged(old, t);
|
|
1993
|
+
});
|
|
1994
|
+
return { added, removed, modified };
|
|
1995
|
+
};
|
|
1996
|
+
var hasConfigChanged = (oldTarget, newTarget) => {
|
|
1997
|
+
if (oldTarget.profile !== newTarget.profile) return true;
|
|
1998
|
+
if (oldTarget.enabled !== newTarget.enabled) return true;
|
|
1999
|
+
if (oldTarget.operator_priority !== newTarget.operator_priority) return true;
|
|
2000
|
+
if (JSON.stringify(oldTarget.policy_tags) !== JSON.stringify(newTarget.policy_tags)) return true;
|
|
2001
|
+
if (JSON.stringify(oldTarget.capabilities) !== JSON.stringify(newTarget.capabilities)) return true;
|
|
2002
|
+
if (oldTarget.retry_budget !== newTarget.retry_budget) return true;
|
|
2003
|
+
if (oldTarget.failover_budget !== newTarget.failover_budget) return true;
|
|
2004
|
+
if (oldTarget.concurrency_limit !== newTarget.concurrency_limit) return true;
|
|
2005
|
+
return false;
|
|
2006
|
+
};
|
|
2007
|
+
var applyConfigDiff = (registry, diff) => {
|
|
2008
|
+
for (const tc of diff.added) {
|
|
2009
|
+
const entry = {
|
|
2010
|
+
target_id: tc.target_id,
|
|
2011
|
+
provider_id: tc.provider_id,
|
|
2012
|
+
profile: tc.profile,
|
|
2013
|
+
endpoint_id: tc.endpoint_id,
|
|
2014
|
+
capabilities: [...tc.capabilities],
|
|
2015
|
+
enabled: tc.enabled,
|
|
2016
|
+
state: "Active",
|
|
2017
|
+
health_score: INITIAL_HEALTH_SCORE,
|
|
2018
|
+
cooldown_until: null,
|
|
2019
|
+
latency_ema_ms: 0,
|
|
2020
|
+
failure_score: 0,
|
|
2021
|
+
operator_priority: tc.operator_priority,
|
|
2022
|
+
policy_tags: [...tc.policy_tags]
|
|
2023
|
+
};
|
|
2024
|
+
registry.addTarget(entry);
|
|
2025
|
+
}
|
|
2026
|
+
for (const id of diff.removed) {
|
|
2027
|
+
registry.updateState(id, "Disabled");
|
|
2028
|
+
}
|
|
2029
|
+
for (const tc of diff.modified) {
|
|
2030
|
+
const existing = registry.getTarget(tc.target_id);
|
|
2031
|
+
if (existing) {
|
|
2032
|
+
const updated = {
|
|
2033
|
+
...existing,
|
|
2034
|
+
provider_id: tc.provider_id,
|
|
2035
|
+
profile: tc.profile,
|
|
2036
|
+
endpoint_id: tc.endpoint_id,
|
|
2037
|
+
capabilities: [...tc.capabilities],
|
|
2038
|
+
enabled: tc.enabled,
|
|
2039
|
+
operator_priority: tc.operator_priority,
|
|
2040
|
+
policy_tags: [...tc.policy_tags]
|
|
2041
|
+
};
|
|
2042
|
+
registry.removeTarget(tc.target_id);
|
|
2043
|
+
registry.addTarget(updated);
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
};
|
|
2047
|
+
|
|
2048
|
+
// src/operator/commands.ts
|
|
2049
|
+
var createRequestTraceBuffer = (capacity) => {
|
|
2050
|
+
const entries = /* @__PURE__ */ new Map();
|
|
2051
|
+
const order = [];
|
|
2052
|
+
return {
|
|
2053
|
+
record(entry) {
|
|
2054
|
+
if (entries.has(entry.request_id)) {
|
|
2055
|
+
const idx = order.indexOf(entry.request_id);
|
|
2056
|
+
if (idx !== -1) {
|
|
2057
|
+
order.splice(idx, 1);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
while (order.length >= capacity) {
|
|
2061
|
+
const oldest = order.shift();
|
|
2062
|
+
if (oldest !== void 0) {
|
|
2063
|
+
entries.delete(oldest);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
entries.set(entry.request_id, entry);
|
|
2067
|
+
order.push(entry.request_id);
|
|
2068
|
+
},
|
|
2069
|
+
lookup(requestId) {
|
|
2070
|
+
return entries.get(requestId);
|
|
2071
|
+
},
|
|
2072
|
+
size() {
|
|
2073
|
+
return entries.size;
|
|
2074
|
+
}
|
|
2075
|
+
};
|
|
2076
|
+
};
|
|
2077
|
+
var listTargets = (deps) => {
|
|
2078
|
+
const allTargets = deps.registry.getAllTargets();
|
|
2079
|
+
const targets = allTargets.map((t) => {
|
|
2080
|
+
const cb = deps.circuitBreakers.get(t.target_id);
|
|
2081
|
+
const cbState = cb ? cb.state() : "Active";
|
|
2082
|
+
return {
|
|
2083
|
+
target_id: t.target_id,
|
|
2084
|
+
provider_id: t.provider_id,
|
|
2085
|
+
profile: t.profile,
|
|
2086
|
+
state: t.state,
|
|
2087
|
+
health_score: t.health_score,
|
|
2088
|
+
circuit_breaker_state: cbState,
|
|
2089
|
+
cooldown_until: t.cooldown_until,
|
|
2090
|
+
enabled: t.enabled,
|
|
2091
|
+
latency_ema_ms: t.latency_ema_ms,
|
|
2092
|
+
failure_score: t.failure_score,
|
|
2093
|
+
operator_priority: t.operator_priority
|
|
2094
|
+
};
|
|
2095
|
+
});
|
|
2096
|
+
return { targets, count: targets.length };
|
|
2097
|
+
};
|
|
2098
|
+
var targetAction = (deps, targetId, action, newState) => {
|
|
2099
|
+
const updated = deps.registry.updateState(targetId, newState);
|
|
2100
|
+
if (!updated) {
|
|
2101
|
+
return { success: false, target_id: targetId, action, error: "target not found" };
|
|
2102
|
+
}
|
|
2103
|
+
return { success: true, target_id: targetId, action };
|
|
2104
|
+
};
|
|
2105
|
+
var pauseTarget = (deps, targetId) => targetAction(deps, targetId, "pause", "Disabled");
|
|
2106
|
+
var resumeTarget = (deps, targetId) => targetAction(deps, targetId, "resume", "Active");
|
|
2107
|
+
var drainTarget = (deps, targetId) => targetAction(deps, targetId, "drain", "Draining");
|
|
2108
|
+
var disableTarget = (deps, targetId) => targetAction(deps, targetId, "disable", "Disabled");
|
|
2109
|
+
var inspectRequest = (deps, requestId) => {
|
|
2110
|
+
const trace = deps.traceBuffer.lookup(requestId);
|
|
2111
|
+
if (!trace) {
|
|
2112
|
+
return { found: false, request_id: requestId };
|
|
2113
|
+
}
|
|
2114
|
+
return { found: true, request_id: requestId, trace };
|
|
2115
|
+
};
|
|
2116
|
+
var reloadConfig = (deps, rawConfig) => {
|
|
2117
|
+
try {
|
|
2118
|
+
const newConfig = validateConfig(rawConfig);
|
|
2119
|
+
const currentConfig = deps.configRef.current();
|
|
2120
|
+
const diff = computeConfigDiff(currentConfig, newConfig);
|
|
2121
|
+
applyConfigDiff(deps.registry, diff);
|
|
2122
|
+
deps.configRef.swap(newConfig);
|
|
2123
|
+
return {
|
|
2124
|
+
success: true,
|
|
2125
|
+
added: diff.added.map((t) => t.target_id),
|
|
2126
|
+
removed: [...diff.removed],
|
|
2127
|
+
modified: diff.modified.map((t) => t.target_id)
|
|
2128
|
+
};
|
|
2129
|
+
} catch (err) {
|
|
2130
|
+
if (err instanceof ConfigValidationError) {
|
|
2131
|
+
return {
|
|
2132
|
+
success: false,
|
|
2133
|
+
added: [],
|
|
2134
|
+
removed: [],
|
|
2135
|
+
modified: [],
|
|
2136
|
+
diagnostics: err.diagnostics
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
throw err;
|
|
2140
|
+
}
|
|
2141
|
+
};
|
|
2142
|
+
|
|
2143
|
+
// src/operator/server-auth.ts
|
|
2144
|
+
var import_node_crypto2 = require("crypto");
|
|
2145
|
+
var validateBearerToken = (authHeader, expectedToken) => {
|
|
2146
|
+
if (authHeader === void 0) {
|
|
2147
|
+
return { authorized: false, reason: "missing Authorization header" };
|
|
2148
|
+
}
|
|
2149
|
+
if (!authHeader.startsWith("Bearer ")) {
|
|
2150
|
+
return { authorized: false, reason: "invalid Authorization scheme" };
|
|
2151
|
+
}
|
|
2152
|
+
const token = authHeader.slice(7);
|
|
2153
|
+
if (token.length !== expectedToken.length) {
|
|
2154
|
+
return { authorized: false, reason: "invalid token" };
|
|
2155
|
+
}
|
|
2156
|
+
const tokenBuffer = Buffer.from(token);
|
|
2157
|
+
const expectedBuffer = Buffer.from(expectedToken);
|
|
2158
|
+
if (!(0, import_node_crypto2.timingSafeEqual)(tokenBuffer, expectedBuffer)) {
|
|
2159
|
+
return { authorized: false, reason: "invalid token" };
|
|
2160
|
+
}
|
|
2161
|
+
return { authorized: true };
|
|
2162
|
+
};
|
|
2163
|
+
|
|
2164
|
+
// src/operator/plugin-tools.ts
|
|
2165
|
+
var import_tool = require("@opencode-ai/plugin/tool");
|
|
2166
|
+
var { schema: z3 } = import_tool.tool;
|
|
2167
|
+
var createOperatorTools = (deps) => ({
|
|
2168
|
+
listTargets: (0, import_tool.tool)({
|
|
2169
|
+
description: "List all routing targets with health scores, states, and circuit breaker status.",
|
|
2170
|
+
args: {},
|
|
2171
|
+
async execute() {
|
|
2172
|
+
deps.logger.info({ op: "listTargets" }, "operator: listTargets");
|
|
2173
|
+
const result = listTargets(deps);
|
|
2174
|
+
return JSON.stringify(result, null, 2);
|
|
2175
|
+
}
|
|
2176
|
+
}),
|
|
2177
|
+
pauseTarget: (0, import_tool.tool)({
|
|
2178
|
+
description: "Pause a target, preventing new requests from being routed to it.",
|
|
2179
|
+
args: { target_id: z3.string().min(1) },
|
|
2180
|
+
async execute(args) {
|
|
2181
|
+
deps.logger.info(
|
|
2182
|
+
{ op: "pauseTarget", target_id: args.target_id },
|
|
2183
|
+
"operator: pauseTarget"
|
|
2184
|
+
);
|
|
2185
|
+
const result = pauseTarget(deps, args.target_id);
|
|
2186
|
+
return JSON.stringify(result, null, 2);
|
|
2187
|
+
}
|
|
2188
|
+
}),
|
|
2189
|
+
resumeTarget: (0, import_tool.tool)({
|
|
2190
|
+
description: "Resume a previously paused or disabled target, allowing new requests.",
|
|
2191
|
+
args: { target_id: z3.string().min(1) },
|
|
2192
|
+
async execute(args) {
|
|
2193
|
+
deps.logger.info(
|
|
2194
|
+
{ op: "resumeTarget", target_id: args.target_id },
|
|
2195
|
+
"operator: resumeTarget"
|
|
2196
|
+
);
|
|
2197
|
+
const result = resumeTarget(deps, args.target_id);
|
|
2198
|
+
return JSON.stringify(result, null, 2);
|
|
2199
|
+
}
|
|
2200
|
+
}),
|
|
2201
|
+
drainTarget: (0, import_tool.tool)({
|
|
2202
|
+
description: "Drain a target, allowing in-flight requests to complete but preventing new ones.",
|
|
2203
|
+
args: { target_id: z3.string().min(1) },
|
|
2204
|
+
async execute(args) {
|
|
2205
|
+
deps.logger.info(
|
|
2206
|
+
{ op: "drainTarget", target_id: args.target_id },
|
|
2207
|
+
"operator: drainTarget"
|
|
2208
|
+
);
|
|
2209
|
+
const result = drainTarget(deps, args.target_id);
|
|
2210
|
+
return JSON.stringify(result, null, 2);
|
|
2211
|
+
}
|
|
2212
|
+
}),
|
|
2213
|
+
disableTarget: (0, import_tool.tool)({
|
|
2214
|
+
description: "Disable a target entirely, removing it from routing.",
|
|
2215
|
+
args: { target_id: z3.string().min(1) },
|
|
2216
|
+
async execute(args) {
|
|
2217
|
+
deps.logger.info(
|
|
2218
|
+
{ op: "disableTarget", target_id: args.target_id },
|
|
2219
|
+
"operator: disableTarget"
|
|
2220
|
+
);
|
|
2221
|
+
const result = disableTarget(deps, args.target_id);
|
|
2222
|
+
return JSON.stringify(result, null, 2);
|
|
2223
|
+
}
|
|
2224
|
+
}),
|
|
2225
|
+
inspectRequest: (0, import_tool.tool)({
|
|
2226
|
+
description: "Inspect a request trace by ID, showing attempts, segments, and outcome.",
|
|
2227
|
+
args: { request_id: z3.string().min(1) },
|
|
2228
|
+
async execute(args) {
|
|
2229
|
+
deps.logger.info(
|
|
2230
|
+
{ op: "inspectRequest", request_id: args.request_id },
|
|
2231
|
+
"operator: inspectRequest"
|
|
2232
|
+
);
|
|
2233
|
+
const result = inspectRequest(deps, args.request_id);
|
|
2234
|
+
return JSON.stringify(result, null, 2);
|
|
2235
|
+
}
|
|
2236
|
+
}),
|
|
2237
|
+
reloadConfig: (0, import_tool.tool)({
|
|
2238
|
+
description: "Reload routing configuration with diff-apply. Validates new config before applying.",
|
|
2239
|
+
args: { config: z3.record(z3.string(), z3.unknown()) },
|
|
2240
|
+
async execute(args) {
|
|
2241
|
+
deps.logger.info({ op: "reloadConfig" }, "operator: reloadConfig");
|
|
2242
|
+
const result = reloadConfig(deps, args.config);
|
|
2243
|
+
return JSON.stringify(result, null, 2);
|
|
2244
|
+
}
|
|
2245
|
+
})
|
|
2246
|
+
});
|
|
2247
|
+
|
|
2248
|
+
// src/profiles/store.ts
|
|
2249
|
+
var import_promises = require("fs/promises");
|
|
2250
|
+
var import_node_os = require("os");
|
|
2251
|
+
var import_node_path = require("path");
|
|
2252
|
+
var PROFILES_DIR = (0, import_node_path.join)((0, import_node_os.homedir)(), ".local", "share", "o-switcher");
|
|
2253
|
+
var PROFILES_PATH = (0, import_node_path.join)(PROFILES_DIR, "profiles.json");
|
|
2254
|
+
var loadProfiles = async (path = PROFILES_PATH) => {
|
|
2255
|
+
try {
|
|
2256
|
+
const content = await (0, import_promises.readFile)(path, "utf-8");
|
|
2257
|
+
return JSON.parse(content);
|
|
2258
|
+
} catch (err) {
|
|
2259
|
+
const code = err.code;
|
|
2260
|
+
if (code === "ENOENT") {
|
|
2261
|
+
return {};
|
|
2262
|
+
}
|
|
2263
|
+
throw err;
|
|
2264
|
+
}
|
|
2265
|
+
};
|
|
2266
|
+
var saveProfiles = async (store, path = PROFILES_PATH, logger) => {
|
|
2267
|
+
const dir = (0, import_node_path.dirname)(path);
|
|
2268
|
+
await (0, import_promises.mkdir)(dir, { recursive: true });
|
|
2269
|
+
const tmpPath = `${path}.tmp`;
|
|
2270
|
+
await (0, import_promises.writeFile)(tmpPath, JSON.stringify(store, null, 2), "utf-8");
|
|
2271
|
+
await (0, import_promises.rename)(tmpPath, path);
|
|
2272
|
+
logger?.info({ path }, "Profiles saved to disk");
|
|
2273
|
+
};
|
|
2274
|
+
var credentialsMatch = (a, b) => {
|
|
2275
|
+
if (a.type !== b.type) return false;
|
|
2276
|
+
if (a.type === "api-key" && b.type === "api-key") {
|
|
2277
|
+
return a.key === b.key;
|
|
2278
|
+
}
|
|
2279
|
+
if (a.type === "oauth" && b.type === "oauth") {
|
|
2280
|
+
return a.refresh === b.refresh && a.access === b.access && a.expires === b.expires && a.accountId === b.accountId;
|
|
2281
|
+
}
|
|
2282
|
+
return false;
|
|
2283
|
+
};
|
|
2284
|
+
var addProfile = (store, provider, credentials) => {
|
|
2285
|
+
const isDuplicate = Object.values(store).some(
|
|
2286
|
+
(entry2) => entry2.provider === provider && credentialsMatch(entry2.credentials, credentials)
|
|
2287
|
+
);
|
|
2288
|
+
if (isDuplicate) {
|
|
2289
|
+
return store;
|
|
2290
|
+
}
|
|
2291
|
+
const id = nextProfileId(store, provider);
|
|
2292
|
+
const entry = {
|
|
2293
|
+
id,
|
|
2294
|
+
provider,
|
|
2295
|
+
type: credentials.type,
|
|
2296
|
+
credentials,
|
|
2297
|
+
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
2298
|
+
};
|
|
2299
|
+
return { ...store, [id]: entry };
|
|
2300
|
+
};
|
|
2301
|
+
var removeProfile = (store, id) => {
|
|
2302
|
+
if (store[id] === void 0) {
|
|
2303
|
+
return { store, removed: false };
|
|
2304
|
+
}
|
|
2305
|
+
const { [id]: _removed, ...rest } = store;
|
|
2306
|
+
return { store: rest, removed: true };
|
|
2307
|
+
};
|
|
2308
|
+
var listProfiles = (store) => {
|
|
2309
|
+
return Object.values(store).sort(
|
|
2310
|
+
(a, b) => new Date(a.created).getTime() - new Date(b.created).getTime()
|
|
2311
|
+
);
|
|
2312
|
+
};
|
|
2313
|
+
var nextProfileId = (store, provider) => {
|
|
2314
|
+
const prefix = `${provider}-`;
|
|
2315
|
+
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);
|
|
2316
|
+
return `${provider}-${maxN + 1}`;
|
|
2317
|
+
};
|
|
2318
|
+
|
|
2319
|
+
// src/profiles/watcher.ts
|
|
2320
|
+
var import_promises2 = require("fs/promises");
|
|
2321
|
+
var import_node_os2 = require("os");
|
|
2322
|
+
var import_node_path2 = require("path");
|
|
2323
|
+
var AUTH_JSON_PATH = (0, import_node_path2.join)(
|
|
2324
|
+
(0, import_node_os2.homedir)(),
|
|
2325
|
+
".local",
|
|
2326
|
+
"share",
|
|
2327
|
+
"opencode",
|
|
2328
|
+
"auth.json"
|
|
2329
|
+
);
|
|
2330
|
+
var DEBOUNCE_MS = 100;
|
|
2331
|
+
var readAuthJson = async (path) => {
|
|
2332
|
+
try {
|
|
2333
|
+
const content = await (0, import_promises2.readFile)(path, "utf-8");
|
|
2334
|
+
return JSON.parse(content);
|
|
2335
|
+
} catch {
|
|
2336
|
+
return {};
|
|
2337
|
+
}
|
|
2338
|
+
};
|
|
2339
|
+
var toCredential = (entry) => {
|
|
2340
|
+
if (entry.type === "oauth" || entry.refresh !== void 0 && entry.access !== void 0) {
|
|
2341
|
+
return {
|
|
2342
|
+
type: "oauth",
|
|
2343
|
+
refresh: String(entry.refresh ?? ""),
|
|
2344
|
+
access: String(entry.access ?? ""),
|
|
2345
|
+
expires: Number(entry.expires ?? 0),
|
|
2346
|
+
accountId: entry.accountId !== void 0 ? String(entry.accountId) : void 0
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
return {
|
|
2350
|
+
type: "api-key",
|
|
2351
|
+
key: String(entry.key ?? "")
|
|
2352
|
+
};
|
|
2353
|
+
};
|
|
2354
|
+
var entriesEqual = (a, b) => {
|
|
2355
|
+
if (a === void 0 && b === void 0) return true;
|
|
2356
|
+
if (a === void 0 || b === void 0) return false;
|
|
2357
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
2358
|
+
};
|
|
2359
|
+
var createAuthWatcher = (options) => {
|
|
2360
|
+
const authPath = options?.authJsonPath ?? AUTH_JSON_PATH;
|
|
2361
|
+
const profPath = options?.profilesPath ?? PROFILES_PATH;
|
|
2362
|
+
const log = options?.logger?.child({ component: "profile-watcher" });
|
|
2363
|
+
let lastKnownAuth = {};
|
|
2364
|
+
let abortController = null;
|
|
2365
|
+
let debounceTimer = null;
|
|
2366
|
+
let watchPromise = null;
|
|
2367
|
+
const processChange = async () => {
|
|
2368
|
+
const newAuth = await readAuthJson(authPath);
|
|
2369
|
+
let store = await loadProfiles(profPath);
|
|
2370
|
+
let changed = false;
|
|
2371
|
+
for (const [provider, entry] of Object.entries(newAuth)) {
|
|
2372
|
+
if (!entry) continue;
|
|
2373
|
+
const previousEntry = lastKnownAuth[provider];
|
|
2374
|
+
if (previousEntry !== void 0 && !entriesEqual(previousEntry, entry)) {
|
|
2375
|
+
log?.info({ provider, action: "credential_changed" }, "Credential change detected \u2014 saving previous credential");
|
|
2376
|
+
const prevCredential = toCredential(previousEntry);
|
|
2377
|
+
const newStore = addProfile(store, provider, prevCredential);
|
|
2378
|
+
if (newStore !== store) {
|
|
2379
|
+
store = newStore;
|
|
2380
|
+
changed = true;
|
|
2381
|
+
}
|
|
2382
|
+
} else if (previousEntry === void 0) {
|
|
2383
|
+
log?.info({ provider, action: "new_provider" }, "New provider detected");
|
|
2384
|
+
const credential = toCredential(entry);
|
|
2385
|
+
const newStore = addProfile(store, provider, credential);
|
|
2386
|
+
if (newStore !== store) {
|
|
2387
|
+
store = newStore;
|
|
2388
|
+
changed = true;
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
if (changed) {
|
|
2393
|
+
await saveProfiles(store, profPath);
|
|
2394
|
+
log?.info({ profiles_saved: true }, "Profiles saved to disk");
|
|
2395
|
+
}
|
|
2396
|
+
lastKnownAuth = newAuth;
|
|
2397
|
+
};
|
|
2398
|
+
const onFileChange = () => {
|
|
2399
|
+
if (debounceTimer !== null) {
|
|
2400
|
+
clearTimeout(debounceTimer);
|
|
2401
|
+
}
|
|
2402
|
+
debounceTimer = setTimeout(() => {
|
|
2403
|
+
debounceTimer = null;
|
|
2404
|
+
processChange().catch((err) => {
|
|
2405
|
+
log?.warn({ err }, "Error processing auth change");
|
|
2406
|
+
});
|
|
2407
|
+
}, DEBOUNCE_MS);
|
|
2408
|
+
};
|
|
2409
|
+
const start = async () => {
|
|
2410
|
+
const currentAuth = await readAuthJson(authPath);
|
|
2411
|
+
const currentStore = await loadProfiles(profPath);
|
|
2412
|
+
log?.info({ providers: Object.keys(currentAuth) }, "Auth watcher started");
|
|
2413
|
+
if (Object.keys(currentStore).length === 0 && Object.keys(currentAuth).length > 0) {
|
|
2414
|
+
let store = {};
|
|
2415
|
+
for (const [provider, entry] of Object.entries(currentAuth)) {
|
|
2416
|
+
if (!entry) continue;
|
|
2417
|
+
const credential = toCredential(entry);
|
|
2418
|
+
store = addProfile(store, provider, credential);
|
|
2419
|
+
}
|
|
2420
|
+
await saveProfiles(store, profPath);
|
|
2421
|
+
log?.info({ profiles_initialized: Object.keys(store).length }, "Initialized profiles from auth.json");
|
|
2422
|
+
}
|
|
2423
|
+
lastKnownAuth = currentAuth;
|
|
2424
|
+
abortController = new AbortController();
|
|
2425
|
+
const parentDir = (0, import_node_path2.dirname)(authPath);
|
|
2426
|
+
watchPromise = (async () => {
|
|
2427
|
+
try {
|
|
2428
|
+
const watcher = (0, import_promises2.watch)(parentDir, {
|
|
2429
|
+
signal: abortController.signal
|
|
2430
|
+
});
|
|
2431
|
+
for await (const event of watcher) {
|
|
2432
|
+
if (event.filename === "auth.json" || event.filename === null) {
|
|
2433
|
+
onFileChange();
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
} catch (err) {
|
|
2437
|
+
const name = err.name;
|
|
2438
|
+
if (name !== "AbortError") {
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
})();
|
|
2442
|
+
};
|
|
2443
|
+
const stop = () => {
|
|
2444
|
+
if (debounceTimer !== null) {
|
|
2445
|
+
clearTimeout(debounceTimer);
|
|
2446
|
+
debounceTimer = null;
|
|
2447
|
+
}
|
|
2448
|
+
if (abortController !== null) {
|
|
2449
|
+
abortController.abort();
|
|
2450
|
+
abortController = null;
|
|
2451
|
+
}
|
|
2452
|
+
watchPromise = null;
|
|
2453
|
+
log?.info("Auth watcher stopped");
|
|
2454
|
+
};
|
|
2455
|
+
return { start, stop };
|
|
2456
|
+
};
|
|
2457
|
+
|
|
2458
|
+
// src/profiles/tools.ts
|
|
2459
|
+
var import_tool2 = require("@opencode-ai/plugin/tool");
|
|
2460
|
+
var { schema: z4 } = import_tool2.tool;
|
|
2461
|
+
var createProfileTools = (options) => ({
|
|
2462
|
+
profilesList: (0, import_tool2.tool)({
|
|
2463
|
+
description: "List all saved auth profiles with provider, type, and creation date.",
|
|
2464
|
+
args: {},
|
|
2465
|
+
async execute() {
|
|
2466
|
+
const store = await loadProfiles(options?.profilesPath);
|
|
2467
|
+
const profiles = listProfiles(store);
|
|
2468
|
+
const result = profiles.map((entry) => ({
|
|
2469
|
+
id: entry.id,
|
|
2470
|
+
provider: entry.provider,
|
|
2471
|
+
type: entry.type,
|
|
2472
|
+
created: entry.created
|
|
2473
|
+
}));
|
|
2474
|
+
return JSON.stringify(result, null, 2);
|
|
2475
|
+
}
|
|
2476
|
+
}),
|
|
2477
|
+
profilesRemove: (0, import_tool2.tool)({
|
|
2478
|
+
description: "Remove a saved auth profile by ID.",
|
|
2479
|
+
args: { id: z4.string().min(1) },
|
|
2480
|
+
async execute(args) {
|
|
2481
|
+
const store = await loadProfiles(options?.profilesPath);
|
|
2482
|
+
const { store: newStore, removed } = removeProfile(store, args.id);
|
|
2483
|
+
if (removed) {
|
|
2484
|
+
await saveProfiles(newStore, options?.profilesPath);
|
|
2485
|
+
return JSON.stringify({ success: true, id: args.id });
|
|
2486
|
+
}
|
|
2487
|
+
return JSON.stringify({
|
|
2488
|
+
success: false,
|
|
2489
|
+
id: args.id,
|
|
2490
|
+
error: "Profile not found"
|
|
2491
|
+
});
|
|
2492
|
+
}
|
|
2493
|
+
})
|
|
2494
|
+
});
|
|
2495
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2496
|
+
0 && (module.exports = {
|
|
2497
|
+
ADMISSION_RESULTS,
|
|
2498
|
+
BackoffConfigSchema,
|
|
2499
|
+
ConfigValidationError,
|
|
2500
|
+
DEFAULT_ALPHA,
|
|
2501
|
+
DEFAULT_BACKOFF_BASE_MS,
|
|
2502
|
+
DEFAULT_BACKOFF_JITTER,
|
|
2503
|
+
DEFAULT_BACKOFF_MAX_MS,
|
|
2504
|
+
DEFAULT_BACKOFF_MULTIPLIER,
|
|
2505
|
+
DEFAULT_BACKOFF_PARAMS,
|
|
2506
|
+
DEFAULT_FAILOVER_BUDGET,
|
|
2507
|
+
DEFAULT_RETRY,
|
|
2508
|
+
DEFAULT_RETRY_BUDGET,
|
|
2509
|
+
DEFAULT_TIMEOUT_MS,
|
|
2510
|
+
DualBreaker,
|
|
2511
|
+
EXCLUSION_REASONS,
|
|
2512
|
+
ErrorClassSchema,
|
|
2513
|
+
HEURISTIC_PATTERNS,
|
|
2514
|
+
INITIAL_HEALTH_SCORE,
|
|
2515
|
+
PROVIDER_PATTERNS,
|
|
2516
|
+
REDACT_PATHS,
|
|
2517
|
+
SwitcherConfigSchema,
|
|
2518
|
+
TARGET_STATES,
|
|
2519
|
+
TEMPORAL_QUOTA_PATTERN,
|
|
2520
|
+
TargetConfigSchema,
|
|
2521
|
+
TargetRegistry,
|
|
2522
|
+
addProfile,
|
|
2523
|
+
applyConfigDiff,
|
|
2524
|
+
checkHardRejects,
|
|
2525
|
+
classify,
|
|
2526
|
+
computeBackoffMs,
|
|
2527
|
+
computeConfigDiff,
|
|
2528
|
+
computeCooldownMs,
|
|
2529
|
+
computeScore,
|
|
2530
|
+
createAdmissionController,
|
|
2531
|
+
createAuditCollector,
|
|
2532
|
+
createAuditLogger,
|
|
2533
|
+
createAuthWatcher,
|
|
2534
|
+
createCircuitBreaker,
|
|
2535
|
+
createConcurrencyTracker,
|
|
2536
|
+
createCooldownManager,
|
|
2537
|
+
createExecutionOrchestrator,
|
|
2538
|
+
createFailoverOrchestrator,
|
|
2539
|
+
createLogSubscriber,
|
|
2540
|
+
createModeAdapter,
|
|
2541
|
+
createOperatorTools,
|
|
2542
|
+
createProfileTools,
|
|
2543
|
+
createRegistry,
|
|
2544
|
+
createRequestLogger,
|
|
2545
|
+
createRequestTraceBuffer,
|
|
2546
|
+
createRetryPolicy,
|
|
2547
|
+
createRoutingEventBus,
|
|
2548
|
+
createStreamBuffer,
|
|
2549
|
+
createStreamStitcher,
|
|
2550
|
+
detectDeploymentMode,
|
|
2551
|
+
determineContinuationMode,
|
|
2552
|
+
directSignalFromResponse,
|
|
2553
|
+
disableTarget,
|
|
2554
|
+
discoverTargets,
|
|
2555
|
+
discoverTargetsFromProfiles,
|
|
2556
|
+
drainTarget,
|
|
2557
|
+
extractRetryAfterMs,
|
|
2558
|
+
generateCorrelationId,
|
|
2559
|
+
getExclusionReason,
|
|
2560
|
+
getModeCapabilities,
|
|
2561
|
+
getSignalFidelity,
|
|
2562
|
+
getTargetStateTransition,
|
|
2563
|
+
heuristicSignalFromEvent,
|
|
2564
|
+
inspectRequest,
|
|
2565
|
+
isRetryable,
|
|
2566
|
+
listProfiles,
|
|
2567
|
+
listTargets,
|
|
2568
|
+
loadProfiles,
|
|
2569
|
+
nextProfileId,
|
|
2570
|
+
normalizeLatency,
|
|
2571
|
+
pauseTarget,
|
|
2572
|
+
reloadConfig,
|
|
2573
|
+
removeProfile,
|
|
2574
|
+
resumeTarget,
|
|
2575
|
+
saveProfiles,
|
|
2576
|
+
selectTarget,
|
|
2577
|
+
updateHealthScore,
|
|
2578
|
+
updateLatencyEma,
|
|
2579
|
+
validateBearerToken,
|
|
2580
|
+
validateConfig
|
|
2581
|
+
});
|
|
2582
|
+
//# sourceMappingURL=index.cjs.map
|