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