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/dist/index.js ADDED
@@ -0,0 +1,832 @@
1
+ import {
2
+ ADMISSION_RESULTS,
3
+ BackoffConfigSchema,
4
+ ConfigValidationError,
5
+ DEFAULT_ALPHA,
6
+ DEFAULT_BACKOFF_BASE_MS,
7
+ DEFAULT_BACKOFF_JITTER,
8
+ DEFAULT_BACKOFF_MAX_MS,
9
+ DEFAULT_BACKOFF_MULTIPLIER,
10
+ DEFAULT_BACKOFF_PARAMS,
11
+ DEFAULT_FAILOVER_BUDGET,
12
+ DEFAULT_RETRY,
13
+ DEFAULT_RETRY_BUDGET,
14
+ DEFAULT_TIMEOUT_MS,
15
+ DualBreaker,
16
+ EXCLUSION_REASONS,
17
+ ErrorClassSchema,
18
+ INITIAL_HEALTH_SCORE,
19
+ REDACT_PATHS,
20
+ SwitcherConfigSchema,
21
+ TARGET_STATES,
22
+ TargetConfigSchema,
23
+ TargetRegistry,
24
+ addProfile,
25
+ applyConfigDiff,
26
+ checkHardRejects,
27
+ computeBackoffMs,
28
+ computeConfigDiff,
29
+ computeCooldownMs,
30
+ computeScore,
31
+ createAdmissionController,
32
+ createAuditLogger,
33
+ createAuthWatcher,
34
+ createCircuitBreaker,
35
+ createConcurrencyTracker,
36
+ createCooldownManager,
37
+ createFailoverOrchestrator,
38
+ createLogSubscriber,
39
+ createOperatorTools,
40
+ createProfileTools,
41
+ createRegistry,
42
+ createRequestLogger,
43
+ createRequestTraceBuffer,
44
+ createRetryPolicy,
45
+ createRoutingEventBus,
46
+ disableTarget,
47
+ discoverTargets,
48
+ discoverTargetsFromProfiles,
49
+ drainTarget,
50
+ generateCorrelationId,
51
+ getExclusionReason,
52
+ getTargetStateTransition,
53
+ inspectRequest,
54
+ isRetryable,
55
+ listProfiles,
56
+ listTargets,
57
+ loadProfiles,
58
+ nextProfileId,
59
+ normalizeLatency,
60
+ pauseTarget,
61
+ reloadConfig,
62
+ removeProfile,
63
+ resumeTarget,
64
+ saveProfiles,
65
+ selectTarget,
66
+ updateHealthScore,
67
+ updateLatencyEma,
68
+ validateConfig
69
+ } from "./chunk-BTDKGS7P.js";
70
+
71
+ // src/mode/detection.ts
72
+ var detectDeploymentMode = (hint) => {
73
+ if (hint !== "auto") {
74
+ return hint;
75
+ }
76
+ return "plugin-only";
77
+ };
78
+ var getSignalFidelity = (mode) => {
79
+ if (mode === "plugin-only") {
80
+ return "heuristic";
81
+ }
82
+ return "direct";
83
+ };
84
+ var getModeCapabilities = (mode) => {
85
+ if (mode === "plugin-only") {
86
+ return {
87
+ mode,
88
+ signalFidelity: "heuristic",
89
+ hasHttpStatus: false,
90
+ hasRetryAfterHeader: false,
91
+ hasOperatorApi: false
92
+ };
93
+ }
94
+ return {
95
+ mode,
96
+ signalFidelity: "direct",
97
+ hasHttpStatus: true,
98
+ hasRetryAfterHeader: true,
99
+ hasOperatorApi: true
100
+ };
101
+ };
102
+
103
+ // src/errors/corpus.ts
104
+ var PROVIDER_PATTERNS = [
105
+ // Anthropic
106
+ { provider: "anthropic", http_status: 400, error_type_field: "error.type", error_type_value: "invalid_request_error", error_class: "PolicyFailure" },
107
+ { provider: "anthropic", http_status: 401, error_type_field: "error.type", error_type_value: "authentication_error", error_class: "AuthFailure" },
108
+ { provider: "anthropic", http_status: 402, error_type_field: "error.type", error_type_value: "billing_error", error_class: "QuotaExhausted" },
109
+ { provider: "anthropic", http_status: 403, error_type_field: "error.type", error_type_value: "permission_error", error_class: "PermissionFailure" },
110
+ { provider: "anthropic", http_status: 404, error_type_field: "error.type", error_type_value: "not_found_error", error_class: "ModelUnavailable" },
111
+ { provider: "anthropic", http_status: 429, error_type_field: "error.type", error_type_value: "rate_limit_error", error_class: "RateLimited" },
112
+ { provider: "anthropic", http_status: 500, error_type_field: "error.type", error_type_value: "api_error", error_class: "TransientServerFailure" },
113
+ { provider: "anthropic", http_status: 504, error_type_field: "error.type", error_type_value: "timeout_error", error_class: "TransportFailure" },
114
+ { 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" },
115
+ // OpenAI
116
+ { provider: "openai", http_status: 400, error_type_field: "error.code", error_type_value: "", error_class: "PolicyFailure" },
117
+ { provider: "openai", http_status: 401, error_type_field: "error.code", error_type_value: "invalid_api_key", error_class: "AuthFailure" },
118
+ { provider: "openai", http_status: 403, error_type_field: "error.code", error_type_value: "", error_class: "PermissionFailure" },
119
+ { provider: "openai", http_status: 429, error_type_field: "error.code", error_type_value: "rate_limit_exceeded", error_class: "RateLimited" },
120
+ { provider: "openai", http_status: 429, error_type_field: "error.code", error_type_value: "insufficient_quota", error_class: "QuotaExhausted", notes: "Billing exhausted -- NOT retryable" },
121
+ { provider: "openai", http_status: 500, error_type_field: "error.code", error_type_value: "", error_class: "TransientServerFailure" },
122
+ { provider: "openai", http_status: 503, error_type_field: "error.code", error_type_value: "", error_class: "TransientServerFailure" },
123
+ // Google Vertex AI / Gemini
124
+ { provider: "google", http_status: 400, error_type_field: "error.status", error_type_value: "INVALID_ARGUMENT", error_class: "PolicyFailure" },
125
+ { provider: "google", http_status: 403, error_type_field: "error.status", error_type_value: "PERMISSION_DENIED", error_class: "PermissionFailure" },
126
+ { provider: "google", http_status: 429, error_type_field: "error.status", error_type_value: "RESOURCE_EXHAUSTED", error_class: "RateLimited" },
127
+ { provider: "google", http_status: 500, error_type_field: "error.status", error_type_value: "INTERNAL", error_class: "TransientServerFailure" },
128
+ { provider: "google", http_status: 503, error_type_field: "error.status", error_type_value: "UNAVAILABLE", error_class: "TransientServerFailure" },
129
+ // AWS Bedrock
130
+ { provider: "bedrock", http_status: 429, error_type_field: "__type", error_type_value: "ThrottlingException", error_class: "RateLimited" },
131
+ { provider: "bedrock", http_status: 408, error_type_field: "__type", error_type_value: "ModelTimeoutException", error_class: "TransportFailure" },
132
+ { provider: "bedrock", http_status: 424, error_type_field: "__type", error_type_value: "ModelNotReadyException", error_class: "ModelUnavailable" },
133
+ { provider: "bedrock", http_status: 403, error_type_field: "__type", error_type_value: "AccessDeniedException", error_class: "PermissionFailure" },
134
+ { provider: "bedrock", http_status: 400, error_type_field: "__type", error_type_value: "ValidationException", error_class: "PolicyFailure" },
135
+ { provider: "bedrock", http_status: 500, error_type_field: "__type", error_type_value: "InternalServerException", error_class: "TransientServerFailure" }
136
+ ];
137
+ var HEURISTIC_PATTERNS = [
138
+ // Transport errors (high confidence -- these are unambiguous)
139
+ { pattern: /ECONNREFUSED|ECONNRESET|ETIMEDOUT/, error_class: "TransportFailure", confidence: "high" },
140
+ // Quota patterns (before rate limit to avoid misclassification -- Pitfall 2/3)
141
+ { pattern: /quota\s*(?:exceeded|exhausted)/i, error_class: "QuotaExhausted", confidence: "medium" },
142
+ { pattern: /insufficient\s*quota/i, error_class: "QuotaExhausted", confidence: "medium", provider: "openai" },
143
+ { pattern: /billing/i, error_class: "QuotaExhausted", confidence: "low", provider: "openai" },
144
+ // Rate limit patterns
145
+ { pattern: /rate\s*limit/i, error_class: "RateLimited", confidence: "medium" },
146
+ { pattern: /too many requests/i, error_class: "RateLimited", confidence: "medium" },
147
+ { pattern: /retry\s*after/i, error_class: "RateLimited", confidence: "medium" },
148
+ { pattern: /overloaded/i, error_class: "RateLimited", confidence: "medium", provider: "anthropic" },
149
+ { pattern: /resource\s*exhausted/i, error_class: "RateLimited", confidence: "medium", provider: "google" },
150
+ // Auth patterns
151
+ { pattern: /authentication/i, error_class: "AuthFailure", confidence: "medium" },
152
+ { pattern: /invalid\s*api\s*key/i, error_class: "AuthFailure", confidence: "medium" },
153
+ { pattern: /unauthorized/i, error_class: "AuthFailure", confidence: "medium" },
154
+ // Permission patterns
155
+ { pattern: /permission\s*denied/i, error_class: "PermissionFailure", confidence: "medium" },
156
+ { pattern: /forbidden/i, error_class: "PermissionFailure", confidence: "low" },
157
+ // Model availability patterns
158
+ { pattern: /model\s*not\s*(?:available|ready|found)/i, error_class: "ModelUnavailable", confidence: "medium" },
159
+ { pattern: /not\s*found/i, error_class: "ModelUnavailable", confidence: "low" },
160
+ // Transport patterns (lower confidence than ECONNREFUSED)
161
+ { pattern: /timeout/i, error_class: "TransportFailure", confidence: "low" },
162
+ // Region restriction (requires both keywords)
163
+ { pattern: /region.*restrict|restrict.*region/i, error_class: "RegionRestriction", confidence: "low" }
164
+ ];
165
+ var TEMPORAL_QUOTA_PATTERN = /too many tokens per (?:day|month|hour|week)/i;
166
+
167
+ // src/errors/classifier.ts
168
+ var buildErrorClass = (className, extras) => {
169
+ switch (className) {
170
+ case "RateLimited":
171
+ return {
172
+ class: "RateLimited",
173
+ retryable: true,
174
+ retry_after_ms: extras?.retry_after_ms,
175
+ provider_reason: extras?.provider_reason
176
+ };
177
+ case "QuotaExhausted":
178
+ return {
179
+ class: "QuotaExhausted",
180
+ retryable: false,
181
+ provider_reason: extras?.provider_reason
182
+ };
183
+ case "AuthFailure":
184
+ return { class: "AuthFailure", retryable: false, recovery_attempted: false };
185
+ case "PermissionFailure":
186
+ return { class: "PermissionFailure", retryable: false };
187
+ case "PolicyFailure":
188
+ return { class: "PolicyFailure", retryable: false };
189
+ case "RegionRestriction":
190
+ return { class: "RegionRestriction", retryable: false };
191
+ case "ModelUnavailable":
192
+ return { class: "ModelUnavailable", retryable: false, failover_eligible: true };
193
+ case "TransientServerFailure":
194
+ return {
195
+ class: "TransientServerFailure",
196
+ retryable: true,
197
+ http_status: extras?.http_status
198
+ };
199
+ case "TransportFailure":
200
+ return { class: "TransportFailure", retryable: true };
201
+ case "InterruptedExecution":
202
+ return { class: "InterruptedExecution", retryable: true };
203
+ default:
204
+ return { class: "TransientServerFailure", retryable: true, http_status: extras?.http_status };
205
+ }
206
+ };
207
+ var extractErrorTypeFromBody = (body) => {
208
+ if (typeof body !== "object" || body === null) return void 0;
209
+ const b = body;
210
+ const errorObj = b.error;
211
+ if (typeof errorObj === "object" && errorObj !== null) {
212
+ if (typeof errorObj.type === "string") return errorObj.type;
213
+ if (typeof errorObj.code === "string") return errorObj.code;
214
+ if (typeof errorObj.status === "string") return errorObj.status;
215
+ }
216
+ if (typeof b.__type === "string") return b.__type;
217
+ return void 0;
218
+ };
219
+ var classifyDirect = (signal) => {
220
+ const status = signal.http_status;
221
+ const errorType = signal.error_type ?? extractErrorTypeFromBody(signal.response_body);
222
+ const errorMessage = signal.error_message ?? "";
223
+ if (status === 429 && TEMPORAL_QUOTA_PATTERN.test(errorMessage)) {
224
+ return {
225
+ error_class: buildErrorClass("QuotaExhausted"),
226
+ detection_mode: "direct",
227
+ confidence: "high",
228
+ raw_signal: signal
229
+ };
230
+ }
231
+ if (status === 429 && errorType === "insufficient_quota") {
232
+ return {
233
+ error_class: buildErrorClass("QuotaExhausted"),
234
+ detection_mode: "direct",
235
+ confidence: "high",
236
+ raw_signal: signal
237
+ };
238
+ }
239
+ if (status === 529 && errorType === "overloaded_error") {
240
+ return {
241
+ error_class: buildErrorClass("RateLimited", {
242
+ retry_after_ms: signal.retry_after_ms,
243
+ provider_reason: "server_capacity_exhaustion"
244
+ }),
245
+ detection_mode: "direct",
246
+ confidence: "high",
247
+ raw_signal: signal
248
+ };
249
+ }
250
+ if (errorType) {
251
+ const match = PROVIDER_PATTERNS.find(
252
+ (p) => p.http_status === status && p.error_type_value === errorType
253
+ );
254
+ if (match) {
255
+ return {
256
+ error_class: buildErrorClass(match.error_class, {
257
+ retry_after_ms: signal.retry_after_ms,
258
+ http_status: status
259
+ }),
260
+ detection_mode: "direct",
261
+ confidence: "high",
262
+ raw_signal: signal
263
+ };
264
+ }
265
+ }
266
+ if (errorType === "ThrottlingException" && TEMPORAL_QUOTA_PATTERN.test(errorMessage)) {
267
+ return {
268
+ error_class: buildErrorClass("QuotaExhausted"),
269
+ detection_mode: "direct",
270
+ confidence: "high",
271
+ raw_signal: signal
272
+ };
273
+ }
274
+ const fallbackClass = getClassFromStatus(status);
275
+ return {
276
+ error_class: buildErrorClass(fallbackClass, {
277
+ retry_after_ms: signal.retry_after_ms,
278
+ http_status: status
279
+ }),
280
+ detection_mode: "direct",
281
+ confidence: "high",
282
+ raw_signal: signal
283
+ };
284
+ };
285
+ var getClassFromStatus = (status) => {
286
+ if (status === 400) return "PolicyFailure";
287
+ if (status === 401) return "AuthFailure";
288
+ if (status === 402) return "QuotaExhausted";
289
+ if (status === 403) return "PermissionFailure";
290
+ if (status === 404) return "ModelUnavailable";
291
+ if (status === 408) return "TransportFailure";
292
+ if (status === 429) return "RateLimited";
293
+ if (status >= 500) return "TransientServerFailure";
294
+ return "TransientServerFailure";
295
+ };
296
+ var classifyHeuristic = (signal) => {
297
+ const message = signal.error_message ?? "";
298
+ if (TEMPORAL_QUOTA_PATTERN.test(message)) {
299
+ return {
300
+ error_class: buildErrorClass("QuotaExhausted"),
301
+ detection_mode: "heuristic",
302
+ confidence: "medium",
303
+ raw_signal: signal
304
+ };
305
+ }
306
+ for (const hp of HEURISTIC_PATTERNS) {
307
+ if (hp.pattern.test(message)) {
308
+ return {
309
+ error_class: buildErrorClass(hp.error_class),
310
+ detection_mode: "heuristic",
311
+ confidence: hp.confidence,
312
+ raw_signal: signal
313
+ };
314
+ }
315
+ }
316
+ return {
317
+ error_class: buildErrorClass("TransientServerFailure"),
318
+ detection_mode: "heuristic",
319
+ confidence: "low",
320
+ raw_signal: signal
321
+ };
322
+ };
323
+ var classify = (signal) => {
324
+ if (signal.http_status !== void 0) {
325
+ return classifyDirect(signal);
326
+ }
327
+ return classifyHeuristic(signal);
328
+ };
329
+
330
+ // src/errors/direct-adapter.ts
331
+ var extractErrorType = (body) => {
332
+ if (typeof body !== "object" || body === null) {
333
+ return void 0;
334
+ }
335
+ const b = body;
336
+ const errorObj = b.error;
337
+ if (typeof errorObj === "object" && errorObj !== null) {
338
+ if (typeof errorObj.type === "string") return errorObj.type;
339
+ if (typeof errorObj.code === "string") return errorObj.code;
340
+ if (typeof errorObj.status === "string") return errorObj.status;
341
+ }
342
+ if (typeof b.__type === "string") return b.__type;
343
+ return void 0;
344
+ };
345
+ var extractErrorMessage = (body) => {
346
+ if (typeof body !== "object" || body === null) {
347
+ return void 0;
348
+ }
349
+ const b = body;
350
+ const errorObj = b.error;
351
+ if (typeof errorObj === "object" && errorObj !== null) {
352
+ if (typeof errorObj.message === "string") return errorObj.message;
353
+ }
354
+ if (typeof b.message === "string") return b.message;
355
+ return void 0;
356
+ };
357
+ var extractRetryAfterMs = (headers) => {
358
+ if (!headers) return void 0;
359
+ const value = headers["retry-after"] ?? headers["Retry-After"];
360
+ if (!value) return void 0;
361
+ const seconds = Number(value);
362
+ if (Number.isNaN(seconds) || seconds < 0) return void 0;
363
+ return seconds * 1e3;
364
+ };
365
+ var directSignalFromResponse = (status, body, headers) => ({
366
+ http_status: status,
367
+ error_type: extractErrorType(body),
368
+ error_message: extractErrorMessage(body),
369
+ response_body: body,
370
+ detection_mode: "direct",
371
+ retry_after_ms: extractRetryAfterMs(headers)
372
+ });
373
+
374
+ // src/errors/heuristic-adapter.ts
375
+ var heuristicSignalFromEvent = (eventType, message, providerId) => ({
376
+ error_type: eventType,
377
+ error_message: message,
378
+ detection_mode: "heuristic",
379
+ provider_id: providerId
380
+ });
381
+
382
+ // src/execution/stream-buffer.ts
383
+ var createStreamBuffer = () => {
384
+ const chunks = [];
385
+ return {
386
+ /**
387
+ * Appends a chunk to the buffer.
388
+ *
389
+ * @param chunk - The stream chunk to append.
390
+ * @returns The index of the appended chunk (0-based).
391
+ */
392
+ append(chunk) {
393
+ chunks.push(chunk);
394
+ return chunks.length - 1;
395
+ },
396
+ /**
397
+ * Returns a readonly copy of all accumulated chunks.
398
+ *
399
+ * The returned array is a shallow copy; mutations to it
400
+ * do not affect the internal buffer.
401
+ *
402
+ * @returns Readonly array of confirmed chunks.
403
+ */
404
+ confirmed() {
405
+ return [...chunks];
406
+ },
407
+ /**
408
+ * Returns the total character count across all confirmed chunks.
409
+ *
410
+ * Used to compute visible_offset for segment provenance.
411
+ *
412
+ * @returns Total character count.
413
+ */
414
+ confirmedCharCount() {
415
+ return chunks.reduce((sum, c) => sum + c.text.length, 0);
416
+ },
417
+ /**
418
+ * Returns the concatenated text of all confirmed chunks.
419
+ *
420
+ * This is the confirmed output that can be preserved on
421
+ * interruption and used as prefix for stream stitching.
422
+ *
423
+ * @returns Concatenated confirmed text.
424
+ */
425
+ snapshot() {
426
+ return chunks.map((c) => c.text).join("");
427
+ }
428
+ };
429
+ };
430
+
431
+ // src/execution/stream-stitcher.ts
432
+ var determineContinuationMode = (oldTargetId, newTargetId, sameModel) => {
433
+ if (oldTargetId === newTargetId) {
434
+ return "same_target_resume";
435
+ }
436
+ return sameModel ? "same_model_alternate_target" : "cross_model_semantic";
437
+ };
438
+ var createStreamStitcher = (_requestId) => {
439
+ const segments = [];
440
+ return {
441
+ /**
442
+ * Adds a segment to the stitcher.
443
+ *
444
+ * Computes visible_offset as the cumulative character count
445
+ * of all previously added segments.
446
+ *
447
+ * @param buffer - The stream buffer containing the segment's chunks.
448
+ * @param provenance - Segment provenance without visible_offset (computed here).
449
+ */
450
+ addSegment(buffer, provenance) {
451
+ const visibleOffset = segments.reduce(
452
+ (sum, entry) => sum + entry.text.length,
453
+ 0
454
+ );
455
+ segments.push({
456
+ text: buffer.snapshot(),
457
+ provenance: { ...provenance, visible_offset: visibleOffset }
458
+ });
459
+ },
460
+ /**
461
+ * Assembles the final stitched output.
462
+ *
463
+ * @returns StitchedOutput with concatenated text, segments, and boundaries.
464
+ */
465
+ assemble() {
466
+ const text = segments.map((s) => s.text).join("");
467
+ const provenanceList = segments.map((s) => s.provenance);
468
+ const continuationBoundaries = provenanceList.slice(1).map((p) => p.visible_offset);
469
+ return {
470
+ text,
471
+ segments: provenanceList,
472
+ continuation_boundaries: continuationBoundaries
473
+ };
474
+ },
475
+ /**
476
+ * Returns the number of segments added so far.
477
+ *
478
+ * @returns Segment count.
479
+ */
480
+ segmentCount() {
481
+ return segments.length;
482
+ }
483
+ };
484
+ };
485
+
486
+ // src/execution/audit-collector.ts
487
+ var createAuditCollector = (logger, requestId) => {
488
+ const requestLogger = createRequestLogger(logger, requestId);
489
+ const attempts = [];
490
+ const segments = [];
491
+ return {
492
+ recordAttempt(attempt) {
493
+ attempts.push(attempt);
494
+ },
495
+ recordSegment(segment) {
496
+ segments.push(segment);
497
+ },
498
+ flush(outcome, finalTarget, stats) {
499
+ requestLogger.info({
500
+ event_type: "request_complete",
501
+ outcome,
502
+ final_target: finalTarget,
503
+ total_attempts: attempts.length,
504
+ total_segments: segments.length,
505
+ total_retries: stats?.total_retries,
506
+ total_failovers: stats?.total_failovers,
507
+ latency_ms: stats?.latency_ms,
508
+ attempts: [...attempts],
509
+ segments: [...segments]
510
+ });
511
+ }
512
+ };
513
+ };
514
+
515
+ // src/execution/orchestrator.ts
516
+ var createExecutionOrchestrator = (deps) => ({
517
+ async execute(request) {
518
+ const requestId = request.request_id || generateCorrelationId();
519
+ const auditCollector = createAuditCollector(deps.logger, requestId);
520
+ const stitcher = createStreamStitcher(requestId);
521
+ const startMs = Date.now();
522
+ const heuristicFlags = [];
523
+ let outcome = "failure";
524
+ let finalTarget;
525
+ let summaryAttempts = 0;
526
+ let summaryRetries = 0;
527
+ let summaryFailovers = 0;
528
+ let summaryTargets = [];
529
+ try {
530
+ const failoverResult = await deps.failover.execute(
531
+ requestId,
532
+ async (targetId) => {
533
+ const buffer = createStreamBuffer();
534
+ const adapterResult = await deps.adapter.execute(targetId, request);
535
+ for (const chunk of adapterResult.chunks) {
536
+ buffer.append(chunk);
537
+ }
538
+ heuristicFlags.push(
539
+ adapterResult.detection_mode === "heuristic"
540
+ );
541
+ const value = { buffer, adapterResult };
542
+ return {
543
+ success: adapterResult.success,
544
+ value,
545
+ error_class: adapterResult.error_class,
546
+ latency_ms: adapterResult.latency_ms
547
+ };
548
+ },
549
+ []
550
+ );
551
+ for (const attempt of failoverResult.attempts) {
552
+ auditCollector.recordAttempt(attempt);
553
+ }
554
+ summaryAttempts = failoverResult.attempts.length;
555
+ summaryRetries = failoverResult.total_retries;
556
+ summaryFailovers = failoverResult.total_failovers;
557
+ summaryTargets = [...new Set(failoverResult.attempts.map((a) => a.target_id))];
558
+ if (failoverResult.outcome === "success") {
559
+ outcome = "success";
560
+ finalTarget = failoverResult.target_id;
561
+ const attemptValue = failoverResult.value;
562
+ const continuationMode = stitcher.segmentCount() === 0 ? "same_target_resume" : "same_target_resume";
563
+ stitcher.addSegment(attemptValue.buffer, {
564
+ request_id: requestId,
565
+ segment_id: stitcher.segmentCount(),
566
+ source_target_id: failoverResult.target_id,
567
+ continuation_mode: continuationMode,
568
+ non_deterministic: false
569
+ });
570
+ const segmentProvenance = stitcher.assemble().segments;
571
+ for (const seg of segmentProvenance) {
572
+ auditCollector.recordSegment(seg);
573
+ }
574
+ const stitchedOutput = stitcher.assemble();
575
+ const isDegraded2 = deps.mode === "plugin-only";
576
+ const isHeuristic2 = heuristicFlags.some(Boolean);
577
+ const uniqueModes = [
578
+ ...new Set(stitchedOutput.segments.map((s) => s.continuation_mode))
579
+ ];
580
+ const uniqueTargets = [
581
+ ...new Set(stitchedOutput.segments.map((s) => s.source_target_id))
582
+ ];
583
+ const provenance2 = {
584
+ request_id: requestId,
585
+ segments: stitchedOutput.segments,
586
+ continuation_modes: uniqueModes,
587
+ targets_involved: uniqueTargets,
588
+ total_attempts: failoverResult.attempts.length,
589
+ degraded: isDegraded2,
590
+ degraded_reason: isDegraded2 ? "plugin-only mode: limited failover capability" : void 0,
591
+ heuristic_detection: isHeuristic2
592
+ };
593
+ return {
594
+ success: true,
595
+ output: stitchedOutput.text,
596
+ provenance: provenance2
597
+ };
598
+ }
599
+ const isDegraded = deps.mode === "plugin-only";
600
+ const isHeuristic = heuristicFlags.some(Boolean);
601
+ const provenance = {
602
+ request_id: requestId,
603
+ segments: [],
604
+ continuation_modes: [],
605
+ targets_involved: [],
606
+ total_attempts: failoverResult.attempts.length,
607
+ degraded: isDegraded,
608
+ degraded_reason: isDegraded ? "plugin-only mode: limited failover capability" : void 0,
609
+ heuristic_detection: isHeuristic
610
+ };
611
+ return {
612
+ success: false,
613
+ output: "",
614
+ provenance
615
+ };
616
+ } finally {
617
+ auditCollector.flush(outcome, finalTarget);
618
+ const requestLogger = createRequestLogger(deps.logger, requestId);
619
+ const endMs = Date.now();
620
+ requestLogger.info({
621
+ event: "request_summary",
622
+ component: "execution",
623
+ outcome,
624
+ total_attempts: summaryAttempts,
625
+ total_failovers: summaryFailovers,
626
+ total_retries: summaryRetries,
627
+ latency_ms: endMs - startMs,
628
+ targets_used: summaryTargets,
629
+ final_target: finalTarget ?? null
630
+ }, "Request complete");
631
+ }
632
+ }
633
+ });
634
+
635
+ // src/execution/adapters/plugin-adapter.ts
636
+ var createPluginAdapter = (_deps) => ({
637
+ /**
638
+ * Executes a request in plugin-only mode.
639
+ *
640
+ * Phase 3 stub: returns a not-yet-implemented result.
641
+ * Phase 4 will wire this to OpenCode plugin lifecycle hooks.
642
+ *
643
+ * @param _targetId - The target to execute against.
644
+ * @param _request - The adapter request payload.
645
+ * @returns AdapterResult with heuristic detection mode.
646
+ */
647
+ async execute(_targetId, _request) {
648
+ const start = Date.now();
649
+ return {
650
+ success: false,
651
+ chunks: [],
652
+ error_class: void 0,
653
+ latency_ms: Date.now() - start,
654
+ detection_mode: "heuristic"
655
+ };
656
+ }
657
+ });
658
+
659
+ // src/execution/adapters/server-adapter.ts
660
+ var createServerAdapter = (_deps) => ({
661
+ /**
662
+ * Executes a request in server-companion mode.
663
+ *
664
+ * Phase 3 stub: returns a not-yet-implemented result.
665
+ * Phase 4 will wire this to the OpenCode SDK client, extracting
666
+ * statusCode, responseHeaders['retry-after'], and responseBody
667
+ * from SDK ApiError for direct error classification.
668
+ *
669
+ * @param _targetId - The target to execute against.
670
+ * @param _request - The adapter request payload.
671
+ * @returns AdapterResult with direct detection mode.
672
+ */
673
+ async execute(_targetId, _request) {
674
+ const start = Date.now();
675
+ return {
676
+ success: false,
677
+ chunks: [],
678
+ error_class: void 0,
679
+ latency_ms: Date.now() - start,
680
+ detection_mode: "direct"
681
+ };
682
+ }
683
+ });
684
+
685
+ // src/execution/adapters/sdk-adapter.ts
686
+ var createSdkAdapter = (_deps) => ({
687
+ /**
688
+ * Executes a request in SDK-control mode.
689
+ *
690
+ * Phase 3 stub: returns a not-yet-implemented result.
691
+ * Phase 4 will wire this to the OpenCode SDK client with full
692
+ * session management and provider control capabilities.
693
+ *
694
+ * @param _targetId - The target to execute against.
695
+ * @param _request - The adapter request payload.
696
+ * @returns AdapterResult with direct detection mode.
697
+ */
698
+ async execute(_targetId, _request) {
699
+ const start = Date.now();
700
+ return {
701
+ success: false,
702
+ chunks: [],
703
+ error_class: void 0,
704
+ latency_ms: Date.now() - start,
705
+ detection_mode: "direct"
706
+ };
707
+ }
708
+ });
709
+
710
+ // src/execution/adapters/adapter-factory.ts
711
+ var createModeAdapter = (mode, deps) => {
712
+ switch (mode) {
713
+ case "plugin-only":
714
+ return createPluginAdapter(deps);
715
+ case "server-companion":
716
+ return createServerAdapter(deps);
717
+ case "sdk-control":
718
+ return createSdkAdapter(deps);
719
+ default: {
720
+ const _exhaustive = mode;
721
+ throw new Error(`Unknown deployment mode: ${String(_exhaustive)}`);
722
+ }
723
+ }
724
+ };
725
+
726
+ // src/operator/server-auth.ts
727
+ import { timingSafeEqual } from "crypto";
728
+ var validateBearerToken = (authHeader, expectedToken) => {
729
+ if (authHeader === void 0) {
730
+ return { authorized: false, reason: "missing Authorization header" };
731
+ }
732
+ if (!authHeader.startsWith("Bearer ")) {
733
+ return { authorized: false, reason: "invalid Authorization scheme" };
734
+ }
735
+ const token = authHeader.slice(7);
736
+ if (token.length !== expectedToken.length) {
737
+ return { authorized: false, reason: "invalid token" };
738
+ }
739
+ const tokenBuffer = Buffer.from(token);
740
+ const expectedBuffer = Buffer.from(expectedToken);
741
+ if (!timingSafeEqual(tokenBuffer, expectedBuffer)) {
742
+ return { authorized: false, reason: "invalid token" };
743
+ }
744
+ return { authorized: true };
745
+ };
746
+ export {
747
+ ADMISSION_RESULTS,
748
+ BackoffConfigSchema,
749
+ ConfigValidationError,
750
+ DEFAULT_ALPHA,
751
+ DEFAULT_BACKOFF_BASE_MS,
752
+ DEFAULT_BACKOFF_JITTER,
753
+ DEFAULT_BACKOFF_MAX_MS,
754
+ DEFAULT_BACKOFF_MULTIPLIER,
755
+ DEFAULT_BACKOFF_PARAMS,
756
+ DEFAULT_FAILOVER_BUDGET,
757
+ DEFAULT_RETRY,
758
+ DEFAULT_RETRY_BUDGET,
759
+ DEFAULT_TIMEOUT_MS,
760
+ DualBreaker,
761
+ EXCLUSION_REASONS,
762
+ ErrorClassSchema,
763
+ HEURISTIC_PATTERNS,
764
+ INITIAL_HEALTH_SCORE,
765
+ PROVIDER_PATTERNS,
766
+ REDACT_PATHS,
767
+ SwitcherConfigSchema,
768
+ TARGET_STATES,
769
+ TEMPORAL_QUOTA_PATTERN,
770
+ TargetConfigSchema,
771
+ TargetRegistry,
772
+ addProfile,
773
+ applyConfigDiff,
774
+ checkHardRejects,
775
+ classify,
776
+ computeBackoffMs,
777
+ computeConfigDiff,
778
+ computeCooldownMs,
779
+ computeScore,
780
+ createAdmissionController,
781
+ createAuditCollector,
782
+ createAuditLogger,
783
+ createAuthWatcher,
784
+ createCircuitBreaker,
785
+ createConcurrencyTracker,
786
+ createCooldownManager,
787
+ createExecutionOrchestrator,
788
+ createFailoverOrchestrator,
789
+ createLogSubscriber,
790
+ createModeAdapter,
791
+ createOperatorTools,
792
+ createProfileTools,
793
+ createRegistry,
794
+ createRequestLogger,
795
+ createRequestTraceBuffer,
796
+ createRetryPolicy,
797
+ createRoutingEventBus,
798
+ createStreamBuffer,
799
+ createStreamStitcher,
800
+ detectDeploymentMode,
801
+ determineContinuationMode,
802
+ directSignalFromResponse,
803
+ disableTarget,
804
+ discoverTargets,
805
+ discoverTargetsFromProfiles,
806
+ drainTarget,
807
+ extractRetryAfterMs,
808
+ generateCorrelationId,
809
+ getExclusionReason,
810
+ getModeCapabilities,
811
+ getSignalFidelity,
812
+ getTargetStateTransition,
813
+ heuristicSignalFromEvent,
814
+ inspectRequest,
815
+ isRetryable,
816
+ listProfiles,
817
+ listTargets,
818
+ loadProfiles,
819
+ nextProfileId,
820
+ normalizeLatency,
821
+ pauseTarget,
822
+ reloadConfig,
823
+ removeProfile,
824
+ resumeTarget,
825
+ saveProfiles,
826
+ selectTarget,
827
+ updateHealthScore,
828
+ updateLatencyEma,
829
+ validateBearerToken,
830
+ validateConfig
831
+ };
832
+ //# sourceMappingURL=index.js.map