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.
@@ -0,0 +1,2020 @@
1
+ import { z } from 'zod';
2
+ import pino from 'pino';
3
+ import { EventEmitter } from 'eventemitter3';
4
+ import { IBreaker, CircuitState } from 'cockatiel';
5
+ import { ToolDefinition } from '@opencode-ai/plugin/tool';
6
+
7
+ /**
8
+ * Zod 4 schemas for O-Switcher configuration.
9
+ *
10
+ * Defines validation schemas for the switcher configuration subtree
11
+ * within OpenCode's effective merged config (D-04, D-05, D-06).
12
+ * All optional fields have sensible defaults so minimal config works.
13
+ */
14
+
15
+ /**
16
+ * Schema for exponential backoff configuration.
17
+ * All fields have defaults per D-10.
18
+ */
19
+ declare const BackoffConfigSchema: z.ZodObject<{
20
+ base_ms: z.ZodDefault<z.ZodNumber>;
21
+ multiplier: z.ZodDefault<z.ZodNumber>;
22
+ max_ms: z.ZodDefault<z.ZodNumber>;
23
+ jitter: z.ZodDefault<z.ZodEnum<{
24
+ full: "full";
25
+ equal: "equal";
26
+ none: "none";
27
+ }>>;
28
+ }, z.core.$strip>;
29
+ /**
30
+ * Schema for a single target definition.
31
+ * target_id and provider_id are required; all other fields have defaults.
32
+ * No credential fields -- provider_id is a pointer to OpenCode's auth store (D-13).
33
+ */
34
+ declare const TargetConfigSchema: z.ZodObject<{
35
+ target_id: z.ZodString;
36
+ provider_id: z.ZodString;
37
+ profile: z.ZodOptional<z.ZodString>;
38
+ endpoint_id: z.ZodOptional<z.ZodString>;
39
+ capabilities: z.ZodDefault<z.ZodArray<z.ZodString>>;
40
+ enabled: z.ZodDefault<z.ZodBoolean>;
41
+ operator_priority: z.ZodDefault<z.ZodNumber>;
42
+ policy_tags: z.ZodDefault<z.ZodArray<z.ZodString>>;
43
+ retry_budget: z.ZodOptional<z.ZodNumber>;
44
+ failover_budget: z.ZodOptional<z.ZodNumber>;
45
+ backoff: z.ZodOptional<z.ZodObject<{
46
+ base_ms: z.ZodDefault<z.ZodNumber>;
47
+ multiplier: z.ZodDefault<z.ZodNumber>;
48
+ max_ms: z.ZodDefault<z.ZodNumber>;
49
+ jitter: z.ZodDefault<z.ZodEnum<{
50
+ full: "full";
51
+ equal: "equal";
52
+ none: "none";
53
+ }>>;
54
+ }, z.core.$strip>>;
55
+ concurrency_limit: z.ZodOptional<z.ZodNumber>;
56
+ circuit_breaker: z.ZodOptional<z.ZodObject<{
57
+ failure_threshold: z.ZodDefault<z.ZodNumber>;
58
+ failure_rate_threshold: z.ZodDefault<z.ZodNumber>;
59
+ sliding_window_size: z.ZodDefault<z.ZodNumber>;
60
+ half_open_after_ms: z.ZodDefault<z.ZodNumber>;
61
+ half_open_max_probes: z.ZodDefault<z.ZodNumber>;
62
+ success_threshold: z.ZodDefault<z.ZodNumber>;
63
+ }, z.core.$strip>>;
64
+ }, z.core.$strip>;
65
+ /**
66
+ * Top-level schema for the O-Switcher configuration subtree.
67
+ * Targets are optional — when absent, auto-discovery populates them from OpenCode providers.
68
+ * When provided, at least one target is required (min 1).
69
+ * Shorthand fields `retry` and `timeout` map to retry_budget and max_expected_latency_ms.
70
+ */
71
+ declare const SwitcherConfigSchema: z.ZodObject<{
72
+ targets: z.ZodOptional<z.ZodArray<z.ZodObject<{
73
+ target_id: z.ZodString;
74
+ provider_id: z.ZodString;
75
+ profile: z.ZodOptional<z.ZodString>;
76
+ endpoint_id: z.ZodOptional<z.ZodString>;
77
+ capabilities: z.ZodDefault<z.ZodArray<z.ZodString>>;
78
+ enabled: z.ZodDefault<z.ZodBoolean>;
79
+ operator_priority: z.ZodDefault<z.ZodNumber>;
80
+ policy_tags: z.ZodDefault<z.ZodArray<z.ZodString>>;
81
+ retry_budget: z.ZodOptional<z.ZodNumber>;
82
+ failover_budget: z.ZodOptional<z.ZodNumber>;
83
+ backoff: z.ZodOptional<z.ZodObject<{
84
+ base_ms: z.ZodDefault<z.ZodNumber>;
85
+ multiplier: z.ZodDefault<z.ZodNumber>;
86
+ max_ms: z.ZodDefault<z.ZodNumber>;
87
+ jitter: z.ZodDefault<z.ZodEnum<{
88
+ full: "full";
89
+ equal: "equal";
90
+ none: "none";
91
+ }>>;
92
+ }, z.core.$strip>>;
93
+ concurrency_limit: z.ZodOptional<z.ZodNumber>;
94
+ circuit_breaker: z.ZodOptional<z.ZodObject<{
95
+ failure_threshold: z.ZodDefault<z.ZodNumber>;
96
+ failure_rate_threshold: z.ZodDefault<z.ZodNumber>;
97
+ sliding_window_size: z.ZodDefault<z.ZodNumber>;
98
+ half_open_after_ms: z.ZodDefault<z.ZodNumber>;
99
+ half_open_max_probes: z.ZodDefault<z.ZodNumber>;
100
+ success_threshold: z.ZodDefault<z.ZodNumber>;
101
+ }, z.core.$strip>>;
102
+ }, z.core.$strip>>>;
103
+ retry: z.ZodOptional<z.ZodNumber>;
104
+ timeout: z.ZodOptional<z.ZodNumber>;
105
+ retry_budget: z.ZodDefault<z.ZodNumber>;
106
+ failover_budget: z.ZodDefault<z.ZodNumber>;
107
+ backoff: z.ZodDefault<z.ZodObject<{
108
+ base_ms: z.ZodDefault<z.ZodNumber>;
109
+ multiplier: z.ZodDefault<z.ZodNumber>;
110
+ max_ms: z.ZodDefault<z.ZodNumber>;
111
+ jitter: z.ZodDefault<z.ZodEnum<{
112
+ full: "full";
113
+ equal: "equal";
114
+ none: "none";
115
+ }>>;
116
+ }, z.core.$strip>>;
117
+ deployment_mode_hint: z.ZodDefault<z.ZodEnum<{
118
+ "plugin-only": "plugin-only";
119
+ "server-companion": "server-companion";
120
+ "sdk-control": "sdk-control";
121
+ auto: "auto";
122
+ }>>;
123
+ routing_weights: z.ZodDefault<z.ZodObject<{
124
+ health: z.ZodDefault<z.ZodNumber>;
125
+ latency: z.ZodDefault<z.ZodNumber>;
126
+ failure: z.ZodDefault<z.ZodNumber>;
127
+ priority: z.ZodDefault<z.ZodNumber>;
128
+ }, z.core.$strip>>;
129
+ queue_limit: z.ZodDefault<z.ZodNumber>;
130
+ concurrency_limit: z.ZodDefault<z.ZodNumber>;
131
+ backpressure_threshold: z.ZodDefault<z.ZodNumber>;
132
+ circuit_breaker: z.ZodDefault<z.ZodObject<{
133
+ failure_threshold: z.ZodDefault<z.ZodNumber>;
134
+ failure_rate_threshold: z.ZodDefault<z.ZodNumber>;
135
+ sliding_window_size: z.ZodDefault<z.ZodNumber>;
136
+ half_open_after_ms: z.ZodDefault<z.ZodNumber>;
137
+ half_open_max_probes: z.ZodDefault<z.ZodNumber>;
138
+ success_threshold: z.ZodDefault<z.ZodNumber>;
139
+ }, z.core.$strip>>;
140
+ max_expected_latency_ms: z.ZodDefault<z.ZodNumber>;
141
+ }, z.core.$strip>;
142
+ /** Inferred type for a validated backoff configuration. */
143
+ type BackoffConfig = z.infer<typeof BackoffConfigSchema>;
144
+ /** Inferred type for a validated target configuration. */
145
+ type TargetConfig = z.infer<typeof TargetConfigSchema>;
146
+ /** Inferred type for a validated switcher configuration. */
147
+ type SwitcherConfig = z.infer<typeof SwitcherConfigSchema>;
148
+
149
+ /**
150
+ * Configuration validation with field-path diagnostics.
151
+ *
152
+ * Validates raw configuration input against the SwitcherConfigSchema
153
+ * and returns a frozen, immutable config object on success.
154
+ * On failure, throws ConfigValidationError with structured diagnostics
155
+ * containing field paths, messages, and received values (D-05).
156
+ */
157
+
158
+ /** A single diagnostic entry describing a validation failure. */
159
+ interface ConfigDiagnostic {
160
+ readonly path: string;
161
+ readonly message: string;
162
+ readonly received: unknown;
163
+ }
164
+ /**
165
+ * Error thrown when configuration validation fails.
166
+ * Contains structured diagnostics with field paths and error messages.
167
+ */
168
+ declare class ConfigValidationError extends Error {
169
+ readonly diagnostics: ReadonlyArray<ConfigDiagnostic>;
170
+ constructor(diagnostics: ReadonlyArray<ConfigDiagnostic>);
171
+ }
172
+ /**
173
+ * Validates raw input against the SwitcherConfigSchema.
174
+ *
175
+ * On success, returns a frozen (immutable) SwitcherConfig.
176
+ * On failure, throws ConfigValidationError with structured diagnostics.
177
+ */
178
+ declare const validateConfig: (raw: unknown) => SwitcherConfig;
179
+
180
+ /**
181
+ * Default configuration values for O-Switcher.
182
+ *
183
+ * These constants define sensible defaults for all optional configuration
184
+ * fields, ensuring minimal config works out of the box (D-10).
185
+ */
186
+ /** Default maximum retry attempts per request. */
187
+ declare const DEFAULT_RETRY_BUDGET = 3;
188
+ /** Default maximum failover attempts per request. */
189
+ declare const DEFAULT_FAILOVER_BUDGET = 2;
190
+ /** Default base delay in milliseconds for exponential backoff. */
191
+ declare const DEFAULT_BACKOFF_BASE_MS = 1000;
192
+ /** Default multiplier for exponential backoff. */
193
+ declare const DEFAULT_BACKOFF_MULTIPLIER = 2;
194
+ /** Default maximum delay in milliseconds for exponential backoff. */
195
+ declare const DEFAULT_BACKOFF_MAX_MS = 30000;
196
+ /** Default jitter strategy for exponential backoff. */
197
+ declare const DEFAULT_BACKOFF_JITTER: "full";
198
+ /** Default retry shorthand value (maps to retry_budget). */
199
+ declare const DEFAULT_RETRY = 3;
200
+ /** Default timeout shorthand value in ms (maps to max_expected_latency_ms). */
201
+ declare const DEFAULT_TIMEOUT_MS = 30000;
202
+
203
+ /**
204
+ * Profile storage types for O-Switcher.
205
+ *
206
+ * TODO (D-73): When OpenCode supports multiple credentials per provider
207
+ * natively, O-Switcher should read them directly. The watcher + copy mechanism
208
+ * becomes unnecessary.
209
+ */
210
+ /** OAuth credential shape matching OpenCode auth.json OAuth entries. */
211
+ interface OAuthCredential {
212
+ readonly type: 'oauth';
213
+ readonly refresh: string;
214
+ readonly access: string;
215
+ readonly expires: number;
216
+ readonly accountId?: string;
217
+ }
218
+ /** API key credential shape matching OpenCode auth.json API key entries. */
219
+ interface ApiKeyCredential {
220
+ readonly type: 'api-key';
221
+ readonly key: string;
222
+ }
223
+ /** Union of all supported credential types. */
224
+ type AuthCredential = OAuthCredential | ApiKeyCredential;
225
+ /** A single stored profile entry. */
226
+ interface ProfileEntry {
227
+ readonly id: string;
228
+ readonly provider: string;
229
+ readonly type: 'oauth' | 'api-key';
230
+ readonly credentials: AuthCredential;
231
+ readonly created: string;
232
+ }
233
+ /** Profile store keyed by profile ID (e.g., "openai-1"). */
234
+ type ProfileStore = Record<string, ProfileEntry>;
235
+
236
+ /**
237
+ * Auto-discovery of targets from OpenCode provider configuration.
238
+ *
239
+ * Pure functions: given a provider config map or a ProfileStore, produce arrays
240
+ * of TargetConfig objects with sensible defaults (D-55, D-56, D-71).
241
+ */
242
+
243
+ /**
244
+ * Minimal interface for an OpenCode provider config entry.
245
+ * Only the fields needed for target discovery are required.
246
+ */
247
+ interface ProviderConfigLike {
248
+ readonly id?: string;
249
+ readonly name?: string;
250
+ readonly options?: Record<string, unknown>;
251
+ }
252
+ /**
253
+ * Discovers targets from OpenCode provider configuration.
254
+ *
255
+ * For each provider entry in the map, creates a TargetConfig with:
256
+ * - target_id = the config key (e.g., "anthropic", "openai")
257
+ * - provider_id = provider.id if present, otherwise the config key
258
+ * - capabilities = ["chat"]
259
+ * - enabled = true
260
+ * - operator_priority = 0
261
+ * - policy_tags = []
262
+ *
263
+ * Skips falsy entries. Returns validated TargetConfig objects.
264
+ */
265
+ declare const discoverTargets: (providerConfig: Record<string, ProviderConfigLike>) => TargetConfig[];
266
+ /**
267
+ * Discovers targets from the O-Switcher profile store (D-71).
268
+ *
269
+ * For each profile entry in the store, creates a TargetConfig with:
270
+ * - target_id = profile.id (e.g., "openai-1")
271
+ * - provider_id = profile.provider (e.g., "openai")
272
+ * - profile = profile.id (links to profile for credential resolution)
273
+ * - capabilities = ["chat"]
274
+ * - enabled = true
275
+ * - operator_priority = 0
276
+ * - policy_tags = []
277
+ *
278
+ * Returns validated TargetConfig objects via TargetConfigSchema.parse().
279
+ */
280
+ declare const discoverTargetsFromProfiles: (store: ProfileStore) => TargetConfig[];
281
+
282
+ /**
283
+ * Target registry types.
284
+ *
285
+ * Defines the TargetEntry interface with all FOUN-02 fields,
286
+ * the TargetState enum, and RegistrySnapshot for read-only access.
287
+ *
288
+ * SECURITY: No credential fields (api_key, token, secret, password).
289
+ * provider_id is a pointer to OpenCode's auth store (D-13, SECU-01, SECU-02).
290
+ */
291
+ /**
292
+ * All valid states for a target in the registry.
293
+ *
294
+ * - Active: healthy and available for routing
295
+ * - CoolingDown: temporarily unavailable after transient failure
296
+ * - ReauthRequired: authentication failure, awaiting credential refresh
297
+ * - PolicyBlocked: blocked by policy (403), no retry
298
+ * - CircuitOpen: circuit breaker tripped, no requests allowed
299
+ * - CircuitHalfOpen: circuit breaker probing with limited requests
300
+ * - Draining: operator-initiated drain, no new requests
301
+ * - Disabled: operator-disabled or config-disabled
302
+ */
303
+ declare const TARGET_STATES: readonly ["Active", "CoolingDown", "ReauthRequired", "PolicyBlocked", "CircuitOpen", "CircuitHalfOpen", "Draining", "Disabled"];
304
+ /** Target state type. */
305
+ type TargetState = (typeof TARGET_STATES)[number];
306
+ /**
307
+ * A single target entry in the registry.
308
+ *
309
+ * Contains all fields per FOUN-02: target_id, provider_id, endpoint_id,
310
+ * capabilities, enabled, state, health_score, cooldown_until, latency_ema_ms,
311
+ * failure_score, operator_priority, policy_tags.
312
+ *
313
+ * NO credential fields -- provider_id maps to OpenCode's credential store.
314
+ */
315
+ interface TargetEntry {
316
+ readonly target_id: string;
317
+ readonly provider_id: string;
318
+ readonly profile: string | undefined;
319
+ readonly endpoint_id: string | undefined;
320
+ readonly capabilities: readonly string[];
321
+ readonly enabled: boolean;
322
+ readonly state: TargetState;
323
+ readonly health_score: number;
324
+ readonly cooldown_until: number | null;
325
+ readonly latency_ema_ms: number;
326
+ readonly failure_score: number;
327
+ readonly operator_priority: number;
328
+ readonly policy_tags: readonly string[];
329
+ }
330
+ /** Read-only snapshot of the entire registry. */
331
+ interface RegistrySnapshot {
332
+ readonly targets: ReadonlyArray<Readonly<TargetEntry>>;
333
+ }
334
+
335
+ /**
336
+ * In-memory target registry.
337
+ *
338
+ * Initialized from validated SwitcherConfig. Single authoritative source
339
+ * of target state during runtime (D-09). All state mutations go through
340
+ * the registry.
341
+ *
342
+ * No credential material stored -- provider_id is a pointer only (D-13).
343
+ */
344
+
345
+ /**
346
+ * Target registry managing per-target state.
347
+ *
348
+ * Provides methods for state mutation, health scoring, cooldown management,
349
+ * and read-only snapshot generation.
350
+ */
351
+ declare class TargetRegistry {
352
+ private readonly targets;
353
+ constructor(config: SwitcherConfig);
354
+ /** Returns the target entry for the given id, or undefined if not found. */
355
+ getTarget(id: string): TargetEntry | undefined;
356
+ /** Returns a readonly array of all target entries. */
357
+ getAllTargets(): ReadonlyArray<Readonly<TargetEntry>>;
358
+ /**
359
+ * Updates the state of a target.
360
+ * @returns true if the target was found and updated, false otherwise.
361
+ */
362
+ updateState(id: string, newState: TargetState): boolean;
363
+ /**
364
+ * Records a success (1) or failure (0) observation for a target.
365
+ * Updates health_score via EMA.
366
+ */
367
+ recordObservation(id: string, observation: 0 | 1): void;
368
+ /** Sets the cooldown_until timestamp for a target. */
369
+ setCooldown(id: string, untilMs: number): void;
370
+ /** Updates the latency EMA for a target. */
371
+ updateLatency(id: string, ms: number): void;
372
+ /**
373
+ * Adds a new target entry to the registry.
374
+ * @returns true if added, false if target_id already exists.
375
+ */
376
+ addTarget(entry: TargetEntry): boolean;
377
+ /**
378
+ * Removes a target entry from the registry.
379
+ * @returns true if found and removed, false if not found.
380
+ */
381
+ removeTarget(id: string): boolean;
382
+ /** Returns a deep-frozen snapshot of all targets. */
383
+ getSnapshot(): RegistrySnapshot;
384
+ }
385
+ /** Factory function to create a registry from validated config. */
386
+ declare const createRegistry: (config: SwitcherConfig) => TargetRegistry;
387
+
388
+ /**
389
+ * Health score computation using Exponential Moving Average (EMA).
390
+ *
391
+ * Pure functions for updating health scores and latency EMAs.
392
+ * Health score ranges from 0.0 (completely unhealthy) to 1.0 (perfect health).
393
+ */
394
+ /** Initial health score for newly registered targets. */
395
+ declare const INITIAL_HEALTH_SCORE = 1;
396
+ /** Default EMA smoothing factor (alpha). Higher = more reactive to recent observations. */
397
+ declare const DEFAULT_ALPHA = 0.1;
398
+ /**
399
+ * Updates health score using EMA formula.
400
+ *
401
+ * Formula: alpha * observation + (1 - alpha) * currentScore
402
+ *
403
+ * @param currentScore - Current health score (0.0 to 1.0)
404
+ * @param observation - 1 for success, 0 for failure
405
+ * @param alpha - EMA smoothing factor (default 0.1)
406
+ * @returns Updated health score
407
+ */
408
+ declare const updateHealthScore: (currentScore: number, observation: 0 | 1, alpha?: number) => number;
409
+ /**
410
+ * Updates latency EMA using the same EMA formula.
411
+ *
412
+ * @param currentEma - Current latency EMA in milliseconds
413
+ * @param observedMs - Observed latency in milliseconds
414
+ * @param alpha - EMA smoothing factor (default 0.1)
415
+ * @returns Updated latency EMA
416
+ */
417
+ declare const updateLatencyEma: (currentEma: number, observedMs: number, alpha?: number) => number;
418
+
419
+ /**
420
+ * Deployment mode types.
421
+ *
422
+ * Defines the three deployment modes, signal fidelity levels,
423
+ * and per-mode capability maps (FOUN-03, FOUN-04, D-16).
424
+ */
425
+ /** The three supported deployment modes. */
426
+ type DeploymentMode = 'plugin-only' | 'server-companion' | 'sdk-control';
427
+ /**
428
+ * Signal fidelity indicates how error information is obtained.
429
+ *
430
+ * - 'direct': raw HTTP status codes and headers available
431
+ * - 'heuristic': classification from message patterns and events
432
+ */
433
+ type SignalFidelity = 'direct' | 'heuristic';
434
+ /** Capability map for a deployment mode. */
435
+ interface ModeCapabilities {
436
+ readonly mode: DeploymentMode;
437
+ readonly signalFidelity: SignalFidelity;
438
+ readonly hasHttpStatus: boolean;
439
+ readonly hasRetryAfterHeader: boolean;
440
+ readonly hasOperatorApi: boolean;
441
+ }
442
+
443
+ /**
444
+ * Deployment mode detection.
445
+ *
446
+ * Determines the active deployment mode from a config hint.
447
+ * When hint is 'auto', defaults to 'plugin-only' (auto-detection
448
+ * logic will be refined in Phase 3 when mode adapters are built).
449
+ *
450
+ * Maps each mode to its signal fidelity and capability set (FOUN-03, FOUN-04).
451
+ */
452
+
453
+ /**
454
+ * Detects the deployment mode from a configuration hint.
455
+ *
456
+ * If hint is not 'auto', returns the hint directly.
457
+ * If 'auto', defaults to 'plugin-only' (conservative default).
458
+ */
459
+ declare const detectDeploymentMode: (hint: "plugin-only" | "server-companion" | "sdk-control" | "auto") => DeploymentMode;
460
+ /**
461
+ * Returns the signal fidelity for a given deployment mode.
462
+ *
463
+ * - plugin-only: 'heuristic' (no raw HTTP status codes)
464
+ * - server-companion / sdk-control: 'direct' (full HTTP access)
465
+ */
466
+ declare const getSignalFidelity: (mode: DeploymentMode) => SignalFidelity;
467
+ /**
468
+ * Returns the full capability map for a deployment mode.
469
+ *
470
+ * plugin-only has no HTTP status, no Retry-After, no operator API.
471
+ * server-companion and sdk-control have all capabilities.
472
+ */
473
+ declare const getModeCapabilities: (mode: DeploymentMode) => ModeCapabilities;
474
+
475
+ /**
476
+ * Pino-based structured audit logger with credential redaction.
477
+ *
478
+ * Provides logger creation with automatic redaction of secret-pattern
479
+ * fields (SECU-04), child logger creation for per-request correlation IDs,
480
+ * and UUID v4 correlation ID generation.
481
+ */
482
+
483
+ /**
484
+ * Pino redaction paths for secret-pattern fields.
485
+ *
486
+ * Includes both top-level paths (for direct log object fields)
487
+ * and wildcard paths (for nested objects) to ensure comprehensive
488
+ * credential redaction regardless of nesting depth.
489
+ */
490
+ declare const REDACT_PATHS: readonly string[];
491
+ /** Options for creating an audit logger. */
492
+ interface AuditLoggerOptions {
493
+ readonly level?: string;
494
+ readonly destination?: pino.DestinationStream;
495
+ }
496
+ /**
497
+ * Creates a pino logger with secret redaction configured.
498
+ *
499
+ * All fields matching REDACT_PATHS patterns are replaced with '[Redacted]'
500
+ * in log output, preventing credential leakage (SECU-04).
501
+ */
502
+ declare const createAuditLogger: (options?: AuditLoggerOptions) => pino.Logger;
503
+ /**
504
+ * Creates a child logger with a request_id binding for correlation.
505
+ *
506
+ * All log entries from the child logger include the request_id field,
507
+ * enabling end-to-end request tracing in the audit trail.
508
+ */
509
+ declare const createRequestLogger: (baseLogger: pino.Logger, requestId: string) => pino.Logger;
510
+ /**
511
+ * Generates a UUID v4 correlation ID using Node.js crypto.
512
+ *
513
+ * @returns A UUID v4 string suitable for request correlation.
514
+ */
515
+ declare const generateCorrelationId: () => string;
516
+
517
+ /**
518
+ * Audit trail types.
519
+ *
520
+ * Defines the AuditEvent interface and CorrelationId type
521
+ * for structured logging with request correlation (SECU-04).
522
+ */
523
+ /** Unique correlation identifier for request tracing. */
524
+ type CorrelationId = string;
525
+ /**
526
+ * Structured audit event for the O-Switcher audit trail.
527
+ *
528
+ * Every audit event includes a request_id for correlation,
529
+ * a timestamp, and an event_type discriminator.
530
+ * Additional fields are allowed via index signature.
531
+ */
532
+ interface AuditEvent {
533
+ readonly request_id: string;
534
+ readonly timestamp: string;
535
+ readonly event_type: string;
536
+ readonly [key: string]: unknown;
537
+ }
538
+
539
+ /**
540
+ * Error taxonomy: 10 error classes as a Zod discriminated union.
541
+ *
542
+ * Defines NormalizedSignal (classifier input), ClassificationResult (classifier output),
543
+ * and helper functions for retry decisions and target state transitions.
544
+ *
545
+ * Per D-01, D-02, D-03, ERRC-01.
546
+ */
547
+
548
+ /**
549
+ * Discriminated union of all 10 error classes.
550
+ * The 'class' field is the discriminator.
551
+ */
552
+ declare const ErrorClassSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
553
+ class: z.ZodLiteral<"RateLimited">;
554
+ retryable: z.ZodLiteral<true>;
555
+ retry_after_ms: z.ZodOptional<z.ZodNumber>;
556
+ provider_reason: z.ZodOptional<z.ZodString>;
557
+ }, z.core.$strip>, z.ZodObject<{
558
+ class: z.ZodLiteral<"QuotaExhausted">;
559
+ retryable: z.ZodLiteral<false>;
560
+ provider_reason: z.ZodOptional<z.ZodString>;
561
+ }, z.core.$strip>, z.ZodObject<{
562
+ class: z.ZodLiteral<"AuthFailure">;
563
+ retryable: z.ZodLiteral<false>;
564
+ recovery_attempted: z.ZodDefault<z.ZodBoolean>;
565
+ }, z.core.$strip>, z.ZodObject<{
566
+ class: z.ZodLiteral<"PermissionFailure">;
567
+ retryable: z.ZodLiteral<false>;
568
+ }, z.core.$strip>, z.ZodObject<{
569
+ class: z.ZodLiteral<"PolicyFailure">;
570
+ retryable: z.ZodLiteral<false>;
571
+ }, z.core.$strip>, z.ZodObject<{
572
+ class: z.ZodLiteral<"RegionRestriction">;
573
+ retryable: z.ZodLiteral<false>;
574
+ }, z.core.$strip>, z.ZodObject<{
575
+ class: z.ZodLiteral<"ModelUnavailable">;
576
+ retryable: z.ZodLiteral<false>;
577
+ failover_eligible: z.ZodLiteral<true>;
578
+ }, z.core.$strip>, z.ZodObject<{
579
+ class: z.ZodLiteral<"TransientServerFailure">;
580
+ retryable: z.ZodLiteral<true>;
581
+ http_status: z.ZodOptional<z.ZodNumber>;
582
+ }, z.core.$strip>, z.ZodObject<{
583
+ class: z.ZodLiteral<"TransportFailure">;
584
+ retryable: z.ZodLiteral<true>;
585
+ }, z.core.$strip>, z.ZodObject<{
586
+ class: z.ZodLiteral<"InterruptedExecution">;
587
+ retryable: z.ZodLiteral<true>;
588
+ partial_output_bytes: z.ZodOptional<z.ZodNumber>;
589
+ }, z.core.$strip>], "class">;
590
+ /** Inferred type for any error class variant. */
591
+ type ErrorClass = z.infer<typeof ErrorClassSchema>;
592
+ /**
593
+ * Normalized signal: the common input format for the classifier.
594
+ *
595
+ * Direct adapters populate http_status and response_body.
596
+ * Heuristic adapters populate error_message and error_type.
597
+ * detection_mode is always set by the adapter (D-02).
598
+ */
599
+ interface NormalizedSignal {
600
+ readonly http_status?: number;
601
+ readonly error_type?: string;
602
+ readonly error_message?: string;
603
+ readonly response_body?: unknown;
604
+ readonly detection_mode: 'direct' | 'heuristic';
605
+ readonly provider_id?: string;
606
+ readonly retry_after_ms?: number;
607
+ }
608
+ /**
609
+ * Classification result: the classifier output.
610
+ *
611
+ * Contains the error class, detection mode, confidence level,
612
+ * and the raw signal that produced it.
613
+ */
614
+ interface ClassificationResult {
615
+ readonly error_class: ErrorClass;
616
+ readonly detection_mode: 'direct' | 'heuristic';
617
+ readonly confidence: 'high' | 'medium' | 'low';
618
+ readonly raw_signal: NormalizedSignal;
619
+ }
620
+ /**
621
+ * Checks whether an error class is retryable.
622
+ *
623
+ * @param errorClass - The classified error.
624
+ * @returns true if the error is retryable.
625
+ */
626
+ declare const isRetryable: (errorClass: ErrorClass) => boolean;
627
+ /**
628
+ * Maps an error class to the target state transition it triggers.
629
+ *
630
+ * Returns null when the error should not cause a target state change
631
+ * (e.g., transient failures that are handled by retry).
632
+ *
633
+ * @param errorClass - The classified error.
634
+ * @returns The new target state, or null if no transition.
635
+ */
636
+ declare const getTargetStateTransition: (errorClass: ErrorClass) => TargetState | null;
637
+
638
+ /**
639
+ * Mode-agnostic error classifier core.
640
+ *
641
+ * Accepts a NormalizedSignal and routes to direct (HTTP) or heuristic
642
+ * (message pattern) classification based on signal content.
643
+ *
644
+ * Per D-01, ERRC-01.
645
+ */
646
+
647
+ /**
648
+ * Classifies a NormalizedSignal into an ErrorClass.
649
+ *
650
+ * Routes to direct (HTTP) or heuristic (message pattern) classification
651
+ * based on whether http_status is present in the signal.
652
+ *
653
+ * @param signal - The normalized error signal to classify.
654
+ * @returns ClassificationResult with error class, mode, confidence, and raw signal.
655
+ */
656
+ declare const classify: (signal: NormalizedSignal) => ClassificationResult;
657
+
658
+ /**
659
+ * Direct signal adapter: HTTP status + body to NormalizedSignal.
660
+ *
661
+ * Used in server-companion and SDK-control modes where raw HTTP
662
+ * status codes, response bodies, and headers are available.
663
+ *
664
+ * Per D-01, ERRC-02.
665
+ */
666
+
667
+ /**
668
+ * Extracts Retry-After value from response headers as milliseconds.
669
+ *
670
+ * Supports integer seconds format. HTTP-date format is not supported
671
+ * (providers typically send integer seconds).
672
+ *
673
+ * @param headers - Response headers (case-insensitive lookup for retry-after).
674
+ * @returns Retry-After in milliseconds, or undefined if not present/parseable.
675
+ */
676
+ declare const extractRetryAfterMs: (headers?: Record<string, string>) => number | undefined;
677
+ /**
678
+ * Creates a NormalizedSignal from an HTTP response.
679
+ *
680
+ * Always sets detection_mode='direct' since we have raw HTTP access.
681
+ *
682
+ * @param status - HTTP status code.
683
+ * @param body - Parsed response body.
684
+ * @param headers - Response headers (optional; used for Retry-After).
685
+ * @returns NormalizedSignal ready for classifier.
686
+ */
687
+ declare const directSignalFromResponse: (status: number, body: unknown, headers?: Record<string, string>) => NormalizedSignal;
688
+
689
+ /**
690
+ * Heuristic signal adapter: message pattern to NormalizedSignal.
691
+ *
692
+ * Used in plugin-only mode where HTTP status codes are not available.
693
+ * Classification relies on message string matching against known patterns.
694
+ *
695
+ * Per D-01, ERRC-03.
696
+ */
697
+
698
+ /**
699
+ * Creates a NormalizedSignal from a session event message.
700
+ *
701
+ * Always sets detection_mode='heuristic' since no HTTP access is available.
702
+ * No http_status field is set (not available in plugin-only mode).
703
+ *
704
+ * @param eventType - The event type (e.g., 'error', 'retry', 'status').
705
+ * @param message - The error or status message string.
706
+ * @param providerId - Optional provider identifier.
707
+ * @returns NormalizedSignal ready for classifier.
708
+ */
709
+ declare const heuristicSignalFromEvent: (eventType: string, message: string, providerId?: string) => NormalizedSignal;
710
+
711
+ /**
712
+ * Provider-specific error pattern corpus.
713
+ *
714
+ * Contains known error response patterns for Anthropic, OpenAI, Google,
715
+ * and AWS Bedrock (direct classification) and heuristic message patterns
716
+ * for plugin-only mode classification.
717
+ *
718
+ * Per ERRC-02, ERRC-05, Pitfalls 1-3.
719
+ */
720
+ /** A provider-specific error response pattern for direct classification. */
721
+ interface ProviderErrorPattern {
722
+ readonly provider: string;
723
+ readonly http_status: number;
724
+ readonly error_type_field: string;
725
+ readonly error_type_value: string;
726
+ readonly error_class: string;
727
+ readonly notes?: string;
728
+ }
729
+ /**
730
+ * Known provider error patterns for direct HTTP status classification.
731
+ *
732
+ * Order matters for matching: more specific patterns (with error_type_value)
733
+ * should be checked before generic status-code fallbacks.
734
+ */
735
+ declare const PROVIDER_PATTERNS: ReadonlyArray<ProviderErrorPattern>;
736
+ /** A heuristic pattern for plugin-only mode message classification. */
737
+ interface HeuristicPattern {
738
+ readonly pattern: RegExp;
739
+ readonly error_class: string;
740
+ readonly confidence: 'high' | 'medium' | 'low';
741
+ readonly provider?: string;
742
+ }
743
+ /**
744
+ * Heuristic patterns for plugin-only mode classification.
745
+ *
746
+ * Order matters: more specific patterns should come first.
747
+ * TEMPORAL_QUOTA_PATTERN is checked separately before these patterns
748
+ * to correctly handle Pitfall 2.
749
+ */
750
+ declare const HEURISTIC_PATTERNS: ReadonlyArray<HeuristicPattern>;
751
+ /**
752
+ * Temporal quota pattern to distinguish quota exhaustion from transient rate limits.
753
+ *
754
+ * Matches messages like "Too many tokens per day" from Bedrock, which arrive as
755
+ * ThrottlingException but represent non-retryable quota exhaustion (Pitfall 2).
756
+ */
757
+ declare const TEMPORAL_QUOTA_PATTERN: RegExp;
758
+
759
+ /**
760
+ * Exponential backoff with jitter and Retry-After floor.
761
+ *
762
+ * Pure function that computes the delay before the next retry attempt.
763
+ * Supports full jitter, equal jitter, and no jitter modes.
764
+ *
765
+ * Per D-10, D-11, Pitfall 4.
766
+ */
767
+ /** Configuration for backoff computation. */
768
+ interface BackoffParams {
769
+ readonly base_ms: number;
770
+ readonly multiplier: number;
771
+ readonly max_ms: number;
772
+ readonly jitter: 'full' | 'equal' | 'none';
773
+ }
774
+ /** Default backoff parameters per D-10. */
775
+ declare const DEFAULT_BACKOFF_PARAMS: BackoffParams;
776
+ /**
777
+ * Computes the backoff delay in milliseconds for a given retry attempt.
778
+ *
779
+ * Formula: rawDelay = min(base_ms * multiplier^attempt, max_ms)
780
+ *
781
+ * Jitter modes:
782
+ * - 'full': Math.random() * rawDelay (uniform [0, rawDelay])
783
+ * - 'equal': rawDelay/2 + Math.random() * rawDelay/2 (uniform [rawDelay/2, rawDelay])
784
+ * - 'none': rawDelay (deterministic)
785
+ *
786
+ * The Retry-After value (if provided) is used as a floor: the result
787
+ * is at least retryAfterMs, preventing retry storms (Pitfall 4 fix).
788
+ *
789
+ * @param attempt - Zero-based retry attempt number.
790
+ * @param params - Backoff configuration parameters.
791
+ * @param retryAfterMs - Optional Retry-After value in ms to use as floor.
792
+ * @returns Delay in milliseconds before next retry.
793
+ */
794
+ declare const computeBackoffMs: (attempt: number, params: BackoffParams, retryAfterMs?: number) => number;
795
+
796
+ /**
797
+ * Bounded retry policy with budget tracking.
798
+ *
799
+ * Decides whether to retry, abort, or declare exhaustion based on
800
+ * the error classification and remaining budget.
801
+ *
802
+ * Per D-10, D-12, ERRC-06, ERRC-07, ERRC-08.
803
+ */
804
+
805
+ /**
806
+ * A retry decision: retry with delay, abort immediately, or budget exhausted.
807
+ */
808
+ type RetryDecision = {
809
+ readonly action: 'retry';
810
+ readonly delay_ms: number;
811
+ readonly attempt: number;
812
+ } | {
813
+ readonly action: 'abort';
814
+ readonly reason: string;
815
+ readonly target_state: TargetState | null;
816
+ } | {
817
+ readonly action: 'exhausted';
818
+ readonly attempts_used: number;
819
+ };
820
+ /**
821
+ * A retry policy with budget and backoff configuration.
822
+ */
823
+ interface RetryPolicy {
824
+ readonly budget: number;
825
+ readonly backoffParams: BackoffParams;
826
+ readonly decide: (classification: ClassificationResult, attemptsSoFar: number) => RetryDecision;
827
+ }
828
+ /**
829
+ * Creates a bounded retry policy.
830
+ *
831
+ * Decision logic:
832
+ * 1. Non-retryable errors (AuthFailure, PermissionFailure, QuotaExhausted,
833
+ * PolicyFailure, RegionRestriction, ModelUnavailable) -> abort immediately
834
+ * with target state transition (D-12)
835
+ * 2. Budget exhausted -> return 'exhausted' with attempt count
836
+ * 3. Retryable errors -> compute backoff with Retry-After floor and return 'retry'
837
+ *
838
+ * AuthFailure special handling (ERRC-06): if recovery_attempted is true,
839
+ * abort with ReauthRequired state. The caller is responsible for setting
840
+ * recovery_attempted on the first recovery attempt.
841
+ *
842
+ * @param budget - Maximum number of retry attempts.
843
+ * @param backoffParams - Backoff configuration (defaults to full jitter).
844
+ * @returns A RetryPolicy instance.
845
+ */
846
+ declare const createRetryPolicy: (budget: number, backoffParams?: BackoffParams) => RetryPolicy;
847
+
848
+ /**
849
+ * Routing domain types for Phase 2.
850
+ *
851
+ * Defines ExclusionReason, AdmissionResult, SelectionRecord, RoutingWeights,
852
+ * CircuitBreakerConfig, CircuitBreaker, and ConcurrencyTrackerApi types.
853
+ *
854
+ * Per D-17, D-20, D-22, D-24, D-26.
855
+ */
856
+
857
+ /** 7 exclusion reasons per ROUT-03, D-17. */
858
+ declare const EXCLUSION_REASONS: readonly ["disabled", "policy_blocked", "reauth_required", "cooldown_active", "circuit_open", "capability_mismatch", "draining"];
859
+ /** Why a target was excluded from routing. */
860
+ type ExclusionReason = (typeof EXCLUSION_REASONS)[number];
861
+ /** 4 admission outcomes per ADMC-02, D-20. */
862
+ declare const ADMISSION_RESULTS: readonly ["admitted", "queued", "degraded", "rejected"];
863
+ /** Admission control decision outcome. */
864
+ type AdmissionResult = (typeof ADMISSION_RESULTS)[number];
865
+ /** Admission decision with reason string. */
866
+ interface AdmissionDecision {
867
+ readonly result: AdmissionResult;
868
+ readonly reason: string;
869
+ }
870
+ /** Scoring weights for target selection per D-17. */
871
+ interface RoutingWeights {
872
+ readonly health: number;
873
+ readonly latency: number;
874
+ readonly failure: number;
875
+ readonly priority: number;
876
+ }
877
+ /** Circuit breaker configuration per D-22. */
878
+ interface CircuitBreakerConfig {
879
+ readonly failure_threshold: number;
880
+ readonly failure_rate_threshold: number;
881
+ readonly sliding_window_size: number;
882
+ readonly half_open_after_ms: number;
883
+ readonly half_open_max_probes: number;
884
+ readonly success_threshold: number;
885
+ }
886
+ /** Full audit record for a target selection per D-26. */
887
+ interface SelectionRecord {
888
+ readonly request_id: string;
889
+ readonly timestamp_ms: number;
890
+ readonly candidates: ReadonlyArray<{
891
+ readonly target_id: string;
892
+ readonly score: number;
893
+ readonly health_score: number;
894
+ readonly latency_ema_ms: number;
895
+ }>;
896
+ readonly excluded: ReadonlyArray<{
897
+ readonly target_id: string;
898
+ readonly reason: ExclusionReason;
899
+ }>;
900
+ readonly selected: {
901
+ readonly target_id: string;
902
+ readonly score: number;
903
+ readonly rank: number;
904
+ } | null;
905
+ }
906
+ /**
907
+ * Circuit breaker interface per D-18.
908
+ * recordSuccess/recordFailure are async because they use cockatiel's
909
+ * execute() internally to properly manage state transitions.
910
+ */
911
+ interface CircuitBreaker {
912
+ state(): TargetState;
913
+ recordSuccess(): Promise<void>;
914
+ recordFailure(): Promise<void>;
915
+ allowRequest(): boolean;
916
+ reset(): void;
917
+ }
918
+ /** Concurrency tracker interface per D-24. */
919
+ interface ConcurrencyTrackerApi {
920
+ acquire(target_id: string): boolean;
921
+ release(target_id: string): void;
922
+ headroom(target_id: string): number;
923
+ active(target_id: string): number;
924
+ }
925
+
926
+ /**
927
+ * Typed event bus for routing events per D-25.
928
+ *
929
+ * Uses eventemitter3 with a typed event map for type-safe emit/on.
930
+ * 6 event types: circuit_state_change, target_excluded, health_updated,
931
+ * admission_decision, cooldown_set, cooldown_expired.
932
+ */
933
+
934
+ /** Typed event map for all routing events. */
935
+ interface RoutingEventMap {
936
+ circuit_state_change: (payload: {
937
+ target_id: string;
938
+ from: TargetState;
939
+ to: TargetState;
940
+ reason: string;
941
+ }) => void;
942
+ target_excluded: (payload: {
943
+ target_id: string;
944
+ reason: ExclusionReason;
945
+ }) => void;
946
+ health_updated: (payload: {
947
+ target_id: string;
948
+ old_score: number;
949
+ new_score: number;
950
+ }) => void;
951
+ admission_decision: (payload: {
952
+ request_id: string;
953
+ result: AdmissionResult;
954
+ reason: string;
955
+ }) => void;
956
+ cooldown_set: (payload: {
957
+ target_id: string;
958
+ until_ms: number;
959
+ error_class: string;
960
+ }) => void;
961
+ cooldown_expired: (payload: {
962
+ target_id: string;
963
+ }) => void;
964
+ }
965
+ /** Creates a typed event bus for routing events. */
966
+ declare const createRoutingEventBus: () => EventEmitter<RoutingEventMap>;
967
+
968
+ /**
969
+ * DualBreaker: composite IBreaker for cockatiel circuit breaker.
970
+ *
971
+ * Composes ConsecutiveBreaker (N consecutive failures) with
972
+ * CountBreaker (failure rate in sliding window). The circuit opens
973
+ * if EITHER trigger fires (OR logic per D-22 dual trigger).
974
+ *
975
+ * Uses CountBreaker (not SamplingBreaker) because D-22's sliding_window
976
+ * refers to request count, not time duration (Pitfall 2 from research).
977
+ */
978
+
979
+ /** Composite breaker that trips on consecutive failures OR failure rate. */
980
+ declare class DualBreaker implements IBreaker {
981
+ private readonly consecutive;
982
+ private readonly count;
983
+ constructor(consecutiveThreshold: number, countOpts: {
984
+ threshold: number;
985
+ size: number;
986
+ minimumNumberOfCalls?: number;
987
+ });
988
+ /** Serializable state for both internal breakers. */
989
+ get state(): unknown;
990
+ /** Restore state for both internal breakers. */
991
+ set state(value: unknown);
992
+ /**
993
+ * Record success on both breakers.
994
+ * ConsecutiveBreaker.success() takes no args (resets counter).
995
+ * CountBreaker.success() takes CircuitState.
996
+ */
997
+ success(state: CircuitState): void;
998
+ /**
999
+ * Record failure on both breakers.
1000
+ * Returns true if EITHER breaker trips (OR logic).
1001
+ * ConsecutiveBreaker.failure() takes no args.
1002
+ * CountBreaker.failure() takes CircuitState.
1003
+ */
1004
+ failure(state: CircuitState): boolean;
1005
+ }
1006
+
1007
+ /**
1008
+ * CircuitBreaker wrapper around cockatiel's CircuitBreakerPolicy.
1009
+ *
1010
+ * Wraps cockatiel internals behind the CircuitBreaker interface from types.ts.
1011
+ * Maps cockatiel CircuitState to TargetState. Emits circuit_state_change events
1012
+ * via the routing event bus.
1013
+ *
1014
+ * Per D-18: cockatiel in wrapper pattern. We do NOT use execute() for routing;
1015
+ * instead we use recordSuccess/recordFailure to update the breaker state after
1016
+ * external request completion. Internally, we call policy.execute() with
1017
+ * success/failure functions to let cockatiel manage state transitions correctly.
1018
+ *
1019
+ * Note: cockatiel closes from HalfOpen after 1 success (native behavior).
1020
+ * This is accepted per research recommendation. D-22's success_threshold:2
1021
+ * is documented but not enforced at this layer.
1022
+ */
1023
+
1024
+ /**
1025
+ * Create a circuit breaker for a specific target.
1026
+ *
1027
+ * @param targetId - Target identifier for event attribution.
1028
+ * @param config - Circuit breaker configuration.
1029
+ * @param eventBus - Optional event bus for state change notifications.
1030
+ * @returns CircuitBreaker interface implementation.
1031
+ */
1032
+ declare const createCircuitBreaker: (targetId: string, config: CircuitBreakerConfig, eventBus?: EventEmitter<RoutingEventMap>) => CircuitBreaker;
1033
+
1034
+ /**
1035
+ * Per-target concurrency tracking per D-24.
1036
+ *
1037
+ * Tracks in-flight request counts per target with acquire/release semantics.
1038
+ * Each target has an independent slot counter with a configurable limit.
1039
+ */
1040
+
1041
+ /**
1042
+ * Create a concurrency tracker with a default per-target limit.
1043
+ *
1044
+ * @param defaultLimit - Default concurrency limit for targets.
1045
+ * @returns ConcurrencyTrackerApi with additional setLimit method for per-target overrides.
1046
+ */
1047
+ declare const createConcurrencyTracker: (defaultLimit: number) => ConcurrencyTrackerApi & {
1048
+ setLimit(target_id: string, limit: number): void;
1049
+ };
1050
+
1051
+ /**
1052
+ * Policy engine: filter-then-score target selection per D-17.
1053
+ *
1054
+ * Pure functions that select the best target from a registry snapshot.
1055
+ * No I/O, no external state dependencies -- deterministic for identical inputs.
1056
+ *
1057
+ * Score formula (D-17):
1058
+ * score = w_health * health_score
1059
+ * - w_latency * normalizeLatency(latency_ema_ms, maxLatencyMs)
1060
+ * - w_failure * failure_score
1061
+ * + w_priority * operator_priority
1062
+ *
1063
+ * Tie-breaking: target_id lexicographic ascending for full determinism (Pitfall 5).
1064
+ *
1065
+ * Exclusion order:
1066
+ * 1. !enabled -> 'disabled'
1067
+ * 2. PolicyBlocked -> 'policy_blocked'
1068
+ * 3. ReauthRequired -> 'reauth_required'
1069
+ * 4. Draining -> 'draining'
1070
+ * 5. Disabled state -> 'disabled'
1071
+ * 6. Circuit breaker denies -> 'circuit_open'
1072
+ * 7. Cooldown active -> 'cooldown_active'
1073
+ * 8. Capability mismatch -> 'capability_mismatch'
1074
+ *
1075
+ * Targets in `excludeTargets` are silently filtered (failover loop, D-19).
1076
+ */
1077
+
1078
+ /**
1079
+ * Normalizes latency to [0, 1] range using max expected latency.
1080
+ * Clamps to 1.0 when latency exceeds maxExpectedMs.
1081
+ *
1082
+ * @param latencyEmaMs - Exponential moving average latency in ms.
1083
+ * @param maxExpectedMs - Maximum expected latency for normalization.
1084
+ * @returns Normalized latency in [0, 1].
1085
+ */
1086
+ declare const normalizeLatency: (latencyEmaMs: number, maxExpectedMs: number) => number;
1087
+ /**
1088
+ * Computes the routing score for a target using D-17 weighted formula.
1089
+ *
1090
+ * Higher score = better target.
1091
+ *
1092
+ * @param target - The target entry to score.
1093
+ * @param weights - Scoring weights configuration.
1094
+ * @param maxLatencyMs - Maximum expected latency for normalization.
1095
+ * @returns Numeric score for ranking.
1096
+ */
1097
+ declare const computeScore: (target: TargetEntry, weights: RoutingWeights, maxLatencyMs: number) => number;
1098
+ /**
1099
+ * Checks a single target against all exclusion criteria.
1100
+ *
1101
+ * Returns the first matching ExclusionReason, or null if eligible.
1102
+ * Check order matters -- earlier checks take priority.
1103
+ *
1104
+ * @param target - Target to evaluate.
1105
+ * @param circuitBreakers - Circuit breaker map by target_id.
1106
+ * @param requiredCapabilities - Capabilities the request needs.
1107
+ * @param excludeTargets - Set of target_ids to silently skip (failover).
1108
+ * @param nowMs - Current timestamp for cooldown evaluation.
1109
+ * @returns ExclusionReason or null if eligible.
1110
+ */
1111
+ declare const getExclusionReason: (target: TargetEntry, circuitBreakers: ReadonlyMap<string, CircuitBreaker>, requiredCapabilities: readonly string[], excludeTargets: ReadonlySet<string>, nowMs: number) => ExclusionReason | null;
1112
+ /**
1113
+ * Selects the best target from a registry snapshot using filter-then-score.
1114
+ *
1115
+ * 1. Filter: exclude ineligible targets, recording reasons.
1116
+ * 2. Silently filter targets in excludeTargets (failover re-selection prevention).
1117
+ * 3. Score: compute weighted score for each eligible target.
1118
+ * 4. Sort: by score DESC, then target_id ASC for determinism.
1119
+ * 5. Return full SelectionRecord per D-26.
1120
+ *
1121
+ * @param snapshot - Registry snapshot with all targets.
1122
+ * @param circuitBreakers - Circuit breaker map by target_id.
1123
+ * @param requiredCapabilities - Capabilities the request requires.
1124
+ * @param weights - Scoring weights for ranking.
1125
+ * @param maxLatencyMs - Max expected latency for normalization.
1126
+ * @param nowMs - Current timestamp for cooldown checks.
1127
+ * @param request_id - Request identifier for audit trail.
1128
+ * @param excludeTargets - Optional set of target_ids to silently skip.
1129
+ * @param logger - Optional pino logger for selection audit logging.
1130
+ * @returns SelectionRecord with full audit information.
1131
+ */
1132
+ declare const selectTarget: (snapshot: RegistrySnapshot, circuitBreakers: ReadonlyMap<string, CircuitBreaker>, requiredCapabilities: readonly string[], weights: RoutingWeights, maxLatencyMs: number, nowMs: number, request_id: string, excludeTargets?: ReadonlySet<string>, logger?: pino.Logger) => SelectionRecord;
1133
+
1134
+ /**
1135
+ * Adaptive cooldown manager per D-23.
1136
+ *
1137
+ * computeCooldownMs: Pure function that calculates cooldown duration
1138
+ * based on error class, consecutive failures, and backoff configuration.
1139
+ * - RateLimited with retry_after_ms: uses retry_after as base
1140
+ * - RateLimited without retry_after_ms: exponential backoff
1141
+ * - TransientServerFailure: half the backoff duration
1142
+ * - Other error classes: fixed 5000ms base
1143
+ * - All results include jitter (10-25%) to prevent thundering herd (Pitfall 7)
1144
+ * - All results clamped to maxMs
1145
+ *
1146
+ * CooldownManager: Coordinates cooldown state with TargetRegistry
1147
+ * and event bus. Uses passive checking (no setTimeout, Pitfall 6).
1148
+ */
1149
+
1150
+ /**
1151
+ * Computes the cooldown duration in milliseconds for a target.
1152
+ *
1153
+ * The duration adapts to the error class:
1154
+ * - RateLimited + retry_after_ms: uses server-provided value as base
1155
+ * - RateLimited (no retry_after): exponential backoff from consecutiveFailures
1156
+ * - TransientServerFailure: half the exponential backoff
1157
+ * - All others: fixed 5000ms base
1158
+ *
1159
+ * Jitter of 10-25% is added to prevent thundering herd.
1160
+ * Result is clamped to maxMs.
1161
+ *
1162
+ * @param errorClass - The classified error that triggered cooldown.
1163
+ * @param consecutiveFailures - Number of consecutive failures for this target.
1164
+ * @param backoffParams - Backoff configuration parameters.
1165
+ * @param maxMs - Maximum cooldown duration in ms.
1166
+ * @returns Cooldown duration in ms with jitter applied.
1167
+ */
1168
+ declare const computeCooldownMs: (errorClass: ErrorClass, consecutiveFailures: number, backoffParams: BackoffParams, maxMs: number) => number;
1169
+ /** CooldownManager interface for coordinating cooldown state. */
1170
+ interface CooldownManager {
1171
+ /** Sets a target into cooldown for the given duration. */
1172
+ setCooldown(target_id: string, durationMs: number, errorClass: string): void;
1173
+ /** Checks all targets for expired cooldowns and transitions them to Active. */
1174
+ checkExpired(nowMs: number): readonly string[];
1175
+ /** Checks whether a specific target is currently in cooldown. */
1176
+ isInCooldown(target_id: string, nowMs: number): boolean;
1177
+ }
1178
+ /**
1179
+ * Creates a CooldownManager that coordinates with TargetRegistry and event bus.
1180
+ *
1181
+ * Uses passive checking (no setTimeout) per Pitfall 6.
1182
+ * checkExpired should be called periodically or before routing decisions.
1183
+ *
1184
+ * @param registry - Target registry for state mutations.
1185
+ * @param eventBus - Optional event bus for cooldown events.
1186
+ * @returns CooldownManager instance.
1187
+ */
1188
+ declare const createCooldownManager: (registry: TargetRegistry, eventBus?: EventEmitter<RoutingEventMap>) => CooldownManager;
1189
+
1190
+ /**
1191
+ * Layered admission controller per D-20.
1192
+ *
1193
+ * Three-layer design:
1194
+ * 1. checkHardRejects: Pure function checking 4 preconditions (reject fast path)
1195
+ * 2. Backpressure detection: queue.pending > threshold -> degraded
1196
+ * 3. Concurrency gating via p-queue: bounded concurrency and queue capacity
1197
+ *
1198
+ * Uses queue.size for backpressure (queue depth = items waiting, not running).
1199
+ * Note: p-queue semantics differ from plan assumption -- pending = running, size = waiting.
1200
+ */
1201
+
1202
+ /**
1203
+ * Context for hard reject checks.
1204
+ * All fields are readonly -- pure input for the pure checkHardRejects function.
1205
+ */
1206
+ interface AdmissionContext {
1207
+ readonly hasEligibleTargets: boolean;
1208
+ readonly operatorPaused: boolean;
1209
+ readonly retryBudgetRemaining: number;
1210
+ readonly failoverBudgetRemaining: number;
1211
+ }
1212
+ /**
1213
+ * Configuration for the admission controller.
1214
+ */
1215
+ interface AdmissionConfig {
1216
+ readonly concurrencyLimit: number;
1217
+ readonly queueLimit: number;
1218
+ readonly backpressureThreshold: number;
1219
+ }
1220
+ /**
1221
+ * Admission controller interface.
1222
+ *
1223
+ * admit() checks admission without executing.
1224
+ * execute() checks admission and runs the function through p-queue if admitted.
1225
+ */
1226
+ interface AdmissionController {
1227
+ admit(request_id: string, ctx: AdmissionContext): Promise<AdmissionDecision>;
1228
+ execute<T>(fn: () => Promise<T>, request_id: string, ctx: AdmissionContext): Promise<{
1229
+ decision: AdmissionDecision;
1230
+ result?: T;
1231
+ }>;
1232
+ readonly pending: number;
1233
+ readonly size: number;
1234
+ }
1235
+ /**
1236
+ * Layer 1: Pure hard reject checks.
1237
+ *
1238
+ * Returns an AdmissionDecision with result 'rejected' if any precondition
1239
+ * fails, or null if all checks pass (no hard reject).
1240
+ *
1241
+ * Check order:
1242
+ * 1. No eligible targets
1243
+ * 2. Operator pause active
1244
+ * 3. Retry budget exhausted
1245
+ * 4. Failover budget exhausted
1246
+ *
1247
+ * @param ctx - Admission context with current system state.
1248
+ * @returns Rejection decision or null if no hard reject applies.
1249
+ */
1250
+ declare const checkHardRejects: (ctx: AdmissionContext) => AdmissionDecision | null;
1251
+ /**
1252
+ * Creates a layered admission controller with p-queue integration.
1253
+ *
1254
+ * Layer 1: checkHardRejects (pure, fast path)
1255
+ * Layer 2: Backpressure detection (queue.pending vs threshold)
1256
+ * Layer 3: Queue capacity check and p-queue concurrency gating
1257
+ *
1258
+ * @param config - Admission controller configuration.
1259
+ * @param eventBus - Optional event bus for admission_decision events.
1260
+ * @returns AdmissionController instance.
1261
+ */
1262
+ declare const createAdmissionController: (config: AdmissionConfig, eventBus?: EventEmitter<RoutingEventMap>) => AdmissionController;
1263
+
1264
+ /**
1265
+ * Event bus log subscriber for routing events.
1266
+ *
1267
+ * Subscribes to all RoutingEventMap events and logs them via pino.
1268
+ * Per D-74: child logger with component context.
1269
+ * Per D-75: info for decisions, warn for degraded, debug for scoring.
1270
+ * Per D-79: circuit breaker transitions at info.
1271
+ * Per D-80: cooldown set/expired at info.
1272
+ * Per D-81: admission at info (admitted) or warn (rejected/degraded).
1273
+ */
1274
+
1275
+ /**
1276
+ * Creates a log subscriber for routing events.
1277
+ *
1278
+ * Subscribes to all 6 event types on the provided event bus and logs
1279
+ * each at the appropriate level using a child logger with component context.
1280
+ *
1281
+ * @param eventBus - Typed routing event bus.
1282
+ * @param logger - Base pino logger (child with component:'routing' is created).
1283
+ * @returns A dispose function that removes all listeners.
1284
+ */
1285
+ declare const createLogSubscriber: (eventBus: EventEmitter<RoutingEventMap>, logger: pino.Logger) => (() => void);
1286
+
1287
+ /**
1288
+ * Nested retry-failover orchestrator per D-19.
1289
+ *
1290
+ * Implements the two-loop structure:
1291
+ * - Outer loop: failover across targets (bounded by failoverBudget)
1292
+ * - Inner loop: retry on same target (bounded by retryBudget)
1293
+ *
1294
+ * Decision tree per attempt:
1295
+ * 1. Success -> return immediately, record success
1296
+ * 2. ModelUnavailable -> early failover (break inner, try next target)
1297
+ * 3. Non-retryable (Auth, Permission, Quota, Policy, Region) -> abort entirely
1298
+ * 4. Retryable + budget remaining -> retry with backoff
1299
+ * 5. Retryable + budget exhausted -> failover to next target
1300
+ *
1301
+ * Composes: selectTarget (policy engine), createRetryPolicy (retry),
1302
+ * circuit breakers, cooldown manager, concurrency tracker.
1303
+ */
1304
+
1305
+ /**
1306
+ * Minimal registry interface used by the failover orchestrator.
1307
+ * Decoupled from concrete TargetRegistry class for testability.
1308
+ */
1309
+ interface FailoverRegistry {
1310
+ getSnapshot(): RegistrySnapshot;
1311
+ recordObservation(id: string, observation: 0 | 1): void;
1312
+ updateLatency(id: string, ms: number): void;
1313
+ updateState(id: string, newState: TargetState): boolean;
1314
+ setCooldown(id: string, untilMs: number): void;
1315
+ }
1316
+
1317
+ /**
1318
+ * Record of a single attempt within the failover loop.
1319
+ */
1320
+ interface FailoverAttempt {
1321
+ readonly target_id: string;
1322
+ /** 1-based attempt number within this target. */
1323
+ readonly attempt_no: number;
1324
+ /** 0-based failover iteration. */
1325
+ readonly failover_no: number;
1326
+ readonly outcome: 'success' | 'retry' | 'failover' | 'abort';
1327
+ readonly error_class?: string;
1328
+ readonly latency_ms: number;
1329
+ readonly selection: SelectionRecord;
1330
+ }
1331
+ /**
1332
+ * Result of the failover orchestrator: success with value or failure with reason.
1333
+ */
1334
+ type FailoverResult = {
1335
+ readonly outcome: 'success';
1336
+ readonly target_id: string;
1337
+ readonly attempts: readonly FailoverAttempt[];
1338
+ readonly total_retries: number;
1339
+ readonly total_failovers: number;
1340
+ readonly value: unknown;
1341
+ } | {
1342
+ readonly outcome: 'failure';
1343
+ readonly reason: string;
1344
+ readonly attempts: readonly FailoverAttempt[];
1345
+ readonly total_retries: number;
1346
+ readonly total_failovers: number;
1347
+ readonly last_error_class?: string;
1348
+ };
1349
+ /**
1350
+ * Function type for executing a request attempt against a target.
1351
+ * Provided by the caller (Phase 3 execution layer).
1352
+ */
1353
+ type AttemptFn = (target_id: string) => Promise<{
1354
+ success: boolean;
1355
+ value?: unknown;
1356
+ error_class?: ErrorClass;
1357
+ latency_ms: number;
1358
+ }>;
1359
+ /**
1360
+ * Dependencies for the failover orchestrator.
1361
+ */
1362
+ interface FailoverDeps {
1363
+ readonly registry: FailoverRegistry;
1364
+ readonly circuitBreakers: ReadonlyMap<string, CircuitBreaker>;
1365
+ readonly concurrency: ConcurrencyTrackerApi;
1366
+ readonly cooldownManager: CooldownManager;
1367
+ readonly eventBus?: EventEmitter<RoutingEventMap>;
1368
+ readonly weights: RoutingWeights;
1369
+ readonly maxLatencyMs: number;
1370
+ readonly retryBudget: number;
1371
+ readonly failoverBudget: number;
1372
+ readonly backoffParams: BackoffParams;
1373
+ /** Optional pino logger for structured failover logging (Phase 8). */
1374
+ readonly logger?: pino.Logger;
1375
+ }
1376
+ /**
1377
+ * Failover orchestrator interface.
1378
+ */
1379
+ interface FailoverOrchestrator {
1380
+ execute(request_id: string, attemptFn: AttemptFn, requiredCapabilities: readonly string[]): Promise<FailoverResult>;
1381
+ }
1382
+ /**
1383
+ * Creates a nested retry-failover orchestrator.
1384
+ *
1385
+ * The outer loop iterates up to failoverBudget + 1 targets.
1386
+ * The inner loop iterates up to retryBudget + 1 attempts per target.
1387
+ * Maximum theoretical attempts = (failoverBudget + 1) * (retryBudget + 1).
1388
+ *
1389
+ * @param deps - Orchestrator dependencies.
1390
+ * @returns FailoverOrchestrator instance.
1391
+ */
1392
+ declare const createFailoverOrchestrator: (deps: FailoverDeps) => FailoverOrchestrator;
1393
+
1394
+ /**
1395
+ * Execution layer types for Phase 3.
1396
+ *
1397
+ * Defines StreamChunk, AdapterRequest, AdapterResult, ModeAdapter,
1398
+ * ContinuationMode, SegmentProvenance, StitchedOutput,
1399
+ * ExecutionProvenance, and ExecutionResult.
1400
+ *
1401
+ * Per D-27 through D-38.
1402
+ */
1403
+
1404
+ /**
1405
+ * A single chunk of streamed output with position and timing.
1406
+ */
1407
+ interface StreamChunk {
1408
+ readonly text: string;
1409
+ readonly index: number;
1410
+ readonly timestamp_ms: number;
1411
+ }
1412
+ /**
1413
+ * Request payload for a mode adapter execution.
1414
+ */
1415
+ interface AdapterRequest {
1416
+ readonly request_id: string;
1417
+ readonly prompt: string;
1418
+ readonly params?: Record<string, unknown>;
1419
+ readonly signal?: AbortSignal;
1420
+ }
1421
+ /**
1422
+ * Result from a mode adapter execution attempt.
1423
+ *
1424
+ * detection_mode indicates whether error classification used
1425
+ * raw HTTP signals ('direct') or message pattern matching ('heuristic').
1426
+ */
1427
+ interface AdapterResult {
1428
+ readonly success: boolean;
1429
+ readonly chunks: readonly StreamChunk[];
1430
+ readonly error_class?: ErrorClass;
1431
+ readonly latency_ms: number;
1432
+ readonly http_status?: number;
1433
+ readonly headers?: Readonly<Record<string, string>>;
1434
+ readonly detection_mode: 'direct' | 'heuristic';
1435
+ }
1436
+ /**
1437
+ * Mode adapter interface per D-27.
1438
+ *
1439
+ * Each deployment mode (plugin-only, server-companion, sdk-control)
1440
+ * provides an adapter that executes requests against a target.
1441
+ */
1442
+ interface ModeAdapter {
1443
+ execute(target_id: string, request: AdapterRequest): Promise<AdapterResult>;
1444
+ }
1445
+ /**
1446
+ * Continuation mode per D-31.
1447
+ *
1448
+ * Describes how a resumed segment relates to the interrupted one:
1449
+ * - same_target_resume: retry on the same target
1450
+ * - same_model_alternate_target: same model, different provider target
1451
+ * - cross_model_semantic: different model entirely (non-deterministic)
1452
+ * - controlled_reexecution: full re-execution when no viable continuation exists
1453
+ */
1454
+ type ContinuationMode = 'same_target_resume' | 'same_model_alternate_target' | 'cross_model_semantic' | 'controlled_reexecution';
1455
+ /**
1456
+ * Provenance metadata for a single segment of stitched output per D-32, D-33.
1457
+ *
1458
+ * non_deterministic is true when continuation crosses model boundaries,
1459
+ * meaning the output may not be semantically consistent with the prior segment.
1460
+ */
1461
+ interface SegmentProvenance {
1462
+ readonly request_id: string;
1463
+ readonly segment_id: number;
1464
+ readonly source_target_id: string;
1465
+ readonly continuation_mode: ContinuationMode;
1466
+ readonly failover_reason?: string;
1467
+ readonly visible_offset: number;
1468
+ readonly non_deterministic: boolean;
1469
+ }
1470
+ /**
1471
+ * Assembled multi-segment output with provenance per STRM-05, D-33.
1472
+ *
1473
+ * continuation_boundaries marks character positions where segments join.
1474
+ */
1475
+ interface StitchedOutput {
1476
+ readonly text: string;
1477
+ readonly segments: readonly SegmentProvenance[];
1478
+ readonly continuation_boundaries: readonly number[];
1479
+ }
1480
+ /**
1481
+ * Full execution provenance per D-37, D-38.
1482
+ *
1483
+ * Tracks all segments, continuation modes, involved targets,
1484
+ * and whether the output is degraded.
1485
+ */
1486
+ interface ExecutionProvenance {
1487
+ readonly request_id: string;
1488
+ readonly segments: readonly SegmentProvenance[];
1489
+ readonly continuation_modes: readonly ContinuationMode[];
1490
+ readonly targets_involved: readonly string[];
1491
+ readonly total_attempts: number;
1492
+ readonly degraded: boolean;
1493
+ readonly degraded_reason?: string;
1494
+ readonly heuristic_detection: boolean;
1495
+ }
1496
+ /**
1497
+ * Final execution result combining output text with provenance.
1498
+ */
1499
+ interface ExecutionResult {
1500
+ readonly success: boolean;
1501
+ readonly output: string;
1502
+ readonly provenance: ExecutionProvenance;
1503
+ }
1504
+ /**
1505
+ * StreamBuffer interface for accumulating stream chunks.
1506
+ *
1507
+ * Append-only buffer that provides confirmed text snapshot
1508
+ * on interruption for stream resume/stitching.
1509
+ */
1510
+ interface StreamBuffer {
1511
+ append(chunk: StreamChunk): number;
1512
+ confirmed(): readonly StreamChunk[];
1513
+ confirmedCharCount(): number;
1514
+ snapshot(): string;
1515
+ }
1516
+
1517
+ /**
1518
+ * Append-only stream buffer with confirmed boundary per D-30.
1519
+ *
1520
+ * Accumulates StreamChunks during a streaming response.
1521
+ * On interruption, confirmed() returns all chunks received so far,
1522
+ * and snapshot() returns the concatenated confirmed text for
1523
+ * stream stitching.
1524
+ */
1525
+
1526
+ /**
1527
+ * Creates a new StreamBuffer instance.
1528
+ *
1529
+ * The buffer is append-only: chunks can be added but never removed.
1530
+ * This preserves a reliable confirmed boundary for stream resume.
1531
+ *
1532
+ * @returns A StreamBuffer instance.
1533
+ */
1534
+ declare const createStreamBuffer: () => StreamBuffer;
1535
+
1536
+ /**
1537
+ * Multi-segment stream stitcher with provenance metadata per D-31 through D-33.
1538
+ *
1539
+ * Assembles output from multiple segments (potentially from different targets
1540
+ * after failover) into a single StitchedOutput with per-segment provenance
1541
+ * and continuation boundary markers.
1542
+ *
1543
+ * Cross-target continuations are annotated as non_deterministic per D-33/TRAN-02
1544
+ * because the output may not be semantically consistent across model boundaries.
1545
+ */
1546
+
1547
+ /**
1548
+ * StreamStitcher interface for assembling multi-segment output.
1549
+ */
1550
+ interface StreamStitcher {
1551
+ addSegment(buffer: StreamBuffer, provenance: Omit<SegmentProvenance, 'visible_offset'>): void;
1552
+ assemble(): StitchedOutput;
1553
+ segmentCount(): number;
1554
+ }
1555
+ /**
1556
+ * Determines the continuation mode based on target and model relationship per D-31.
1557
+ *
1558
+ * - Same target: same_target_resume (deterministic)
1559
+ * - Different target, same model: same_model_alternate_target
1560
+ * - Different target, different model: cross_model_semantic (non-deterministic)
1561
+ *
1562
+ * Note: controlled_reexecution is set by the orchestrator when no viable
1563
+ * continuation target exists, not by the stitcher.
1564
+ *
1565
+ * @param oldTargetId - The target ID of the previous segment.
1566
+ * @param newTargetId - The target ID of the new segment.
1567
+ * @param sameModel - Whether both targets serve the same model.
1568
+ * @returns The appropriate ContinuationMode.
1569
+ */
1570
+ declare const determineContinuationMode: (oldTargetId: string, newTargetId: string, sameModel: boolean) => ContinuationMode;
1571
+ /**
1572
+ * Creates a new StreamStitcher instance.
1573
+ *
1574
+ * The stitcher accumulates segments via addSegment() and produces
1575
+ * a final StitchedOutput via assemble() with:
1576
+ * - Concatenated text from all segments
1577
+ * - Per-segment provenance with computed visible_offset
1578
+ * - continuation_boundaries marking join points between segments
1579
+ *
1580
+ * @param _requestId - The correlation request ID (reserved for future audit use).
1581
+ * @returns A StreamStitcher instance.
1582
+ */
1583
+ declare const createStreamStitcher: (_requestId: string) => StreamStitcher;
1584
+
1585
+ /**
1586
+ * Audit collector for per-request audit trail accumulation.
1587
+ *
1588
+ * Subscribes to routing events and accumulates FailoverAttempt
1589
+ * and SegmentProvenance records per request. On flush(), emits
1590
+ * a complete AuditRecord to pino with all AUDT-02 fields
1591
+ * (request_id, attempt_no, segment_no, selected_target, candidates,
1592
+ * selection_reason, failure_class, retry_no, failover_no, latency_ms, outcome).
1593
+ *
1594
+ * flush() is always called in a finally block even on zero-attempt
1595
+ * failures per Pitfall 3.
1596
+ */
1597
+
1598
+ /**
1599
+ * AuditCollector interface for accumulating and flushing audit records.
1600
+ *
1601
+ * Single-use per request: after flush(), the collector emits
1602
+ * its accumulated data and is considered complete.
1603
+ */
1604
+ interface AuditCollector {
1605
+ /** Records a failover attempt. */
1606
+ recordAttempt(attempt: FailoverAttempt): void;
1607
+ /** Records a segment provenance entry. */
1608
+ recordSegment(segment: SegmentProvenance): void;
1609
+ /** Flushes the accumulated audit record to pino. */
1610
+ flush(outcome: 'success' | 'failure', finalTarget?: string, stats?: {
1611
+ total_retries?: number;
1612
+ total_failovers?: number;
1613
+ latency_ms?: number;
1614
+ }): void;
1615
+ }
1616
+ /**
1617
+ * Creates an AuditCollector for a specific request.
1618
+ *
1619
+ * The collector accumulates FailoverAttempt and SegmentProvenance
1620
+ * records and flushes them as a single pino log entry with
1621
+ * event_type 'request_complete'.
1622
+ *
1623
+ * @param logger - The base pino logger (a child logger with request_id is created internally).
1624
+ * @param requestId - The correlation request ID for this request.
1625
+ * @returns An AuditCollector instance.
1626
+ */
1627
+ declare const createAuditCollector: (logger: pino.Logger, requestId: string) => AuditCollector;
1628
+
1629
+ /**
1630
+ * Execution orchestrator: wires ModeAdapter + FailoverOrchestrator +
1631
+ * StreamStitcher + AuditCollector into a single execute call.
1632
+ *
1633
+ * Produces ExecutionResult with full provenance metadata including
1634
+ * segments, continuation_modes, targets_involved, degraded flag (D-38),
1635
+ * and heuristic_detection flag (D-37).
1636
+ *
1637
+ * AuditCollector.flush() is ALWAYS called via try/finally even on
1638
+ * zero-attempt failures (Pitfall 3).
1639
+ */
1640
+
1641
+ /**
1642
+ * Dependencies for the execution orchestrator.
1643
+ */
1644
+ interface ExecutionDeps {
1645
+ /** Mode adapter for executing requests against targets. */
1646
+ readonly adapter: ModeAdapter;
1647
+ /** Failover orchestrator for retry-failover logic. */
1648
+ readonly failover: FailoverOrchestrator;
1649
+ /** Base pino logger for audit trail. */
1650
+ readonly logger: pino.Logger;
1651
+ /** Current deployment mode. */
1652
+ readonly mode: DeploymentMode;
1653
+ }
1654
+ /**
1655
+ * Execution orchestrator interface.
1656
+ */
1657
+ interface ExecutionOrchestrator {
1658
+ /** Executes a request through the full pipeline. */
1659
+ execute(request: AdapterRequest): Promise<ExecutionResult>;
1660
+ }
1661
+ /**
1662
+ * Creates an execution orchestrator that composes adapter, failover,
1663
+ * stream stitcher, and audit collector into an end-to-end pipeline.
1664
+ *
1665
+ * @param deps - Execution dependencies.
1666
+ * @returns An ExecutionOrchestrator instance.
1667
+ */
1668
+ declare const createExecutionOrchestrator: (deps: ExecutionDeps) => ExecutionOrchestrator;
1669
+
1670
+ /**
1671
+ * Shared adapter configuration types.
1672
+ *
1673
+ * Provides the dependency injection interface for all mode adapters.
1674
+ */
1675
+
1676
+ /**
1677
+ * Dependencies injected into mode adapters.
1678
+ *
1679
+ * classifyError is the unified error classification function
1680
+ * that routes to direct or heuristic classification.
1681
+ */
1682
+ interface AdapterDeps {
1683
+ readonly classifyError: (signal: NormalizedSignal) => ClassificationResult;
1684
+ }
1685
+
1686
+ /**
1687
+ * Adapter factory: selects the correct ModeAdapter by DeploymentMode.
1688
+ *
1689
+ * Provides a single entry point for creating the appropriate adapter
1690
+ * based on the resolved deployment mode. Uses exhaustive switch
1691
+ * with never default for type safety.
1692
+ */
1693
+
1694
+ /**
1695
+ * Creates the appropriate ModeAdapter for the given deployment mode.
1696
+ *
1697
+ * @param mode - The resolved deployment mode.
1698
+ * @param deps - Shared adapter dependencies.
1699
+ * @returns The ModeAdapter implementation for the specified mode.
1700
+ * @throws Error if an unknown deployment mode is provided (compile-time exhaustive check).
1701
+ */
1702
+ declare const createModeAdapter: (mode: DeploymentMode, deps: AdapterDeps) => ModeAdapter;
1703
+
1704
+ /**
1705
+ * Operator command result types.
1706
+ *
1707
+ * All types are readonly interfaces per D-39. Operator commands
1708
+ * return structured results for programmatic consumption.
1709
+ */
1710
+
1711
+ /** Dependencies injected into all operator commands. */
1712
+ interface OperatorDeps {
1713
+ readonly registry: TargetRegistry;
1714
+ readonly circuitBreakers: ReadonlyMap<string, CircuitBreaker>;
1715
+ readonly configRef: ConfigRef;
1716
+ readonly logger: pino.Logger;
1717
+ readonly traceBuffer: RequestTraceBuffer;
1718
+ }
1719
+ /** Mutable config reference with atomic swap for reload. */
1720
+ interface ConfigRef {
1721
+ current(): SwitcherConfig;
1722
+ swap(config: SwitcherConfig): void;
1723
+ }
1724
+ /** Enriched target view returned by listTargets. */
1725
+ interface TargetView {
1726
+ readonly target_id: string;
1727
+ readonly provider_id: string;
1728
+ readonly profile: string | undefined;
1729
+ readonly state: TargetState;
1730
+ readonly health_score: number;
1731
+ readonly circuit_breaker_state: TargetState;
1732
+ readonly cooldown_until: number | null;
1733
+ readonly enabled: boolean;
1734
+ readonly latency_ema_ms: number;
1735
+ readonly failure_score: number;
1736
+ readonly operator_priority: number;
1737
+ }
1738
+ /** Result of listTargets command. */
1739
+ interface ListTargetsResult {
1740
+ readonly targets: ReadonlyArray<TargetView>;
1741
+ readonly count: number;
1742
+ }
1743
+ /** Result of pauseTarget, resumeTarget, drainTarget, disableTarget commands. */
1744
+ interface TargetActionResult {
1745
+ readonly success: boolean;
1746
+ readonly target_id: string;
1747
+ readonly action: string;
1748
+ readonly error?: string;
1749
+ }
1750
+ /** Result of inspectRequest command. */
1751
+ interface InspectResult {
1752
+ readonly found: boolean;
1753
+ readonly request_id: string;
1754
+ readonly trace?: RequestTraceEntry;
1755
+ }
1756
+ /** Result of reloadConfig command. */
1757
+ interface ReloadResult {
1758
+ readonly success: boolean;
1759
+ readonly added: readonly string[];
1760
+ readonly removed: readonly string[];
1761
+ readonly modified: readonly string[];
1762
+ readonly diagnostics?: ReadonlyArray<ConfigDiagnostic>;
1763
+ }
1764
+ /** A single request trace entry stored in the ring buffer. */
1765
+ interface RequestTraceEntry {
1766
+ readonly request_id: string;
1767
+ readonly timestamp_ms: number;
1768
+ readonly outcome: string;
1769
+ readonly target_id?: string;
1770
+ readonly total_attempts: number;
1771
+ readonly total_segments: number;
1772
+ readonly attempts: readonly unknown[];
1773
+ readonly segments: readonly unknown[];
1774
+ }
1775
+ /** Ring buffer interface for request trace storage. */
1776
+ interface RequestTraceBuffer {
1777
+ /** Records a trace entry, evicting the oldest if at capacity. */
1778
+ record(entry: RequestTraceEntry): void;
1779
+ /** Looks up a trace entry by request ID. Returns undefined if not found. */
1780
+ lookup(requestId: string): RequestTraceEntry | undefined;
1781
+ /** Returns current number of entries in the buffer. */
1782
+ size(): number;
1783
+ }
1784
+
1785
+ /**
1786
+ * Pure operator command functions per D-39.
1787
+ *
1788
+ * All commands take OperatorDeps and return structured results.
1789
+ * No separate operator state -- all mutations go through TargetRegistry (D-40).
1790
+ * All commands work without code changes (OPER-05) -- they are runtime function calls.
1791
+ */
1792
+
1793
+ /**
1794
+ * Creates a ring buffer for request trace storage.
1795
+ *
1796
+ * Uses Map for O(1) lookup and array for insertion order tracking.
1797
+ * When size exceeds capacity, oldest entries are evicted.
1798
+ *
1799
+ * @param capacity - Maximum number of entries to store.
1800
+ */
1801
+ declare const createRequestTraceBuffer: (capacity: number) => RequestTraceBuffer;
1802
+ /**
1803
+ * Lists all targets with enriched circuit breaker state.
1804
+ * Per D-39: returns target_id, state, health_score, circuit_breaker_state,
1805
+ * cooldown_until, enabled, latency_ema_ms, failure_score, operator_priority.
1806
+ */
1807
+ declare const listTargets: (deps: OperatorDeps) => ListTargetsResult;
1808
+ /** Pauses a target by setting state to Disabled. Per research: reuse Disabled with semantic action name. */
1809
+ declare const pauseTarget: (deps: OperatorDeps, targetId: string) => TargetActionResult;
1810
+ /** Resumes a target by setting state to Active. */
1811
+ declare const resumeTarget: (deps: OperatorDeps, targetId: string) => TargetActionResult;
1812
+ /** Drains a target by setting state to Draining. No new requests will be routed. */
1813
+ declare const drainTarget: (deps: OperatorDeps, targetId: string) => TargetActionResult;
1814
+ /** Disables a target by setting state to Disabled. */
1815
+ declare const disableTarget: (deps: OperatorDeps, targetId: string) => TargetActionResult;
1816
+ /** Inspects a request by looking up its trace in the ring buffer. */
1817
+ declare const inspectRequest: (deps: OperatorDeps, requestId: string) => InspectResult;
1818
+ /**
1819
+ * Reloads configuration with diff-apply logic.
1820
+ *
1821
+ * 1. Validates raw config via validateConfig
1822
+ * 2. On validation failure: returns diagnostics, no changes applied (D-43)
1823
+ * 3. On success: computes diff, applies to registry, swaps config reference
1824
+ */
1825
+ declare const reloadConfig: (deps: OperatorDeps, rawConfig: unknown) => ReloadResult;
1826
+
1827
+ /**
1828
+ * Config reload with diff-apply logic per D-42, D-43.
1829
+ *
1830
+ * computeConfigDiff: Three-pass diff (added, removed, modified) per Pitfall 4.
1831
+ * applyConfigDiff: Applies diff to registry. Removed targets are Disabled
1832
+ * (not deleted) to protect in-flight requests per Pitfall 1.
1833
+ */
1834
+
1835
+ /** The result of diffing old and new configuration. */
1836
+ interface ConfigDiff {
1837
+ readonly added: readonly TargetConfig[];
1838
+ readonly removed: readonly string[];
1839
+ readonly modified: readonly TargetConfig[];
1840
+ }
1841
+ /**
1842
+ * Computes a three-pass diff between old and new configs per Pitfall 4.
1843
+ *
1844
+ * Pass 1: Find targets in newConfig not in oldConfig (added)
1845
+ * Pass 2: Find targets in oldConfig not in newConfig (removed)
1846
+ * Pass 3: For intersection, compare mutable fields (modified)
1847
+ */
1848
+ declare const computeConfigDiff: (oldConfig: SwitcherConfig, newConfig: SwitcherConfig) => ConfigDiff;
1849
+ /**
1850
+ * Applies a config diff to the registry.
1851
+ *
1852
+ * - Added targets: creates fresh TargetEntry with INITIAL_HEALTH_SCORE
1853
+ * - Removed targets: sets state to Disabled (not deleted) per Pitfall 1
1854
+ * - Modified targets: rebuilds entry with updated config fields
1855
+ */
1856
+ declare const applyConfigDiff: (registry: TargetRegistry, diff: ConfigDiff) => void;
1857
+
1858
+ /**
1859
+ * Bearer token authentication middleware for server-companion mode.
1860
+ *
1861
+ * Per D-44: Server-companion mode operator endpoints require Bearer token
1862
+ * authentication. Plugin-only mode commands inherit OpenCode session auth.
1863
+ *
1864
+ * Per Pitfall 3: Uses crypto.timingSafeEqual for constant-time token
1865
+ * comparison to prevent timing attacks.
1866
+ */
1867
+ /** Result of bearer token validation. */
1868
+ interface AuthResult {
1869
+ readonly authorized: boolean;
1870
+ readonly reason?: string;
1871
+ }
1872
+ /**
1873
+ * Validates a Bearer token from the Authorization header.
1874
+ *
1875
+ * @param authHeader - The raw Authorization header value, or undefined if absent.
1876
+ * @param expectedToken - The expected token to compare against.
1877
+ * @returns AuthResult indicating whether the request is authorized.
1878
+ */
1879
+ declare const validateBearerToken: (authHeader: string | undefined, expectedToken: string) => AuthResult;
1880
+
1881
+ /**
1882
+ * OpenCode plugin tool wrappers for operator commands.
1883
+ *
1884
+ * Per D-39 and OPER-05: Each operator command is exposed as an OpenCode
1885
+ * plugin tool with Zod-validated args. Plugin tools return JSON-stringified
1886
+ * results from the pure command functions.
1887
+ *
1888
+ * Uses @opencode-ai/plugin/tool for tool definition with Zod schema.
1889
+ */
1890
+
1891
+ /**
1892
+ * Creates operator tool definitions bound to the given dependencies.
1893
+ *
1894
+ * Each tool wraps a pure operator command function, validates args via Zod,
1895
+ * and returns JSON-stringified results.
1896
+ *
1897
+ * @param deps - Operator dependencies (registry, circuit breakers, config, logger, trace buffer).
1898
+ */
1899
+ /** Map of all operator tool definitions keyed by command name. */
1900
+ interface OperatorTools {
1901
+ readonly listTargets: ToolDefinition;
1902
+ readonly pauseTarget: ToolDefinition;
1903
+ readonly resumeTarget: ToolDefinition;
1904
+ readonly drainTarget: ToolDefinition;
1905
+ readonly disableTarget: ToolDefinition;
1906
+ readonly inspectRequest: ToolDefinition;
1907
+ readonly reloadConfig: ToolDefinition;
1908
+ }
1909
+ declare const createOperatorTools: (deps: OperatorDeps) => OperatorTools;
1910
+
1911
+ /**
1912
+ * Profile store CRUD operations.
1913
+ *
1914
+ * Pure functions for in-memory store manipulation (addProfile, removeProfile,
1915
+ * listProfiles, nextProfileId) plus file I/O helpers (loadProfiles, saveProfiles).
1916
+ *
1917
+ * File writes are atomic: write to .tmp then rename (D-67).
1918
+ * Default paths target ~/.local/share/o-switcher/profiles.json (D-62).
1919
+ */
1920
+
1921
+ /**
1922
+ * Loads the profile store from disk.
1923
+ * Returns empty store if file does not exist.
1924
+ */
1925
+ declare const loadProfiles: (path?: string) => Promise<ProfileStore>;
1926
+ /**
1927
+ * Saves the profile store to disk atomically.
1928
+ * Writes to a .tmp file first, then renames to the target path (D-67).
1929
+ * Creates parent directories if they do not exist.
1930
+ *
1931
+ * @param store - Profile store to persist.
1932
+ * @param path - Target file path.
1933
+ * @param logger - Optional pino logger for disk write logging.
1934
+ */
1935
+ declare const saveProfiles: (store: ProfileStore, path?: string, logger?: pino.Logger) => Promise<void>;
1936
+ /**
1937
+ * Adds a profile to the store. Pure function — returns a new store.
1938
+ *
1939
+ * Assigns a sequential ID via nextProfileId (e.g., openai-1, openai-2).
1940
+ * Skips if credentials match an existing entry for the same provider (dedup).
1941
+ */
1942
+ declare const addProfile: (store: ProfileStore, provider: string, credentials: AuthCredential) => ProfileStore;
1943
+ /**
1944
+ * Removes a profile by ID. Pure function — returns new store and removal status.
1945
+ */
1946
+ declare const removeProfile: (store: ProfileStore, id: string) => {
1947
+ store: ProfileStore;
1948
+ removed: boolean;
1949
+ };
1950
+ /**
1951
+ * Lists all profiles sorted by created date (oldest first).
1952
+ */
1953
+ declare const listProfiles: (store: ProfileStore) => ProfileEntry[];
1954
+ /**
1955
+ * Computes the next sequential profile ID for a provider.
1956
+ *
1957
+ * Finds the maximum N in existing "provider-N" entries and returns "provider-(N+1)".
1958
+ * Returns "provider-1" when no profiles exist for the provider.
1959
+ */
1960
+ declare const nextProfileId: (store: ProfileStore, provider: string) => string;
1961
+
1962
+ /**
1963
+ * Auth.json file watcher with debounce and credential comparison.
1964
+ *
1965
+ * Watches OpenCode's auth.json for changes, saves the PREVIOUS credential
1966
+ * to O-Switcher's profile store before accepting the new one (D-65, D-66).
1967
+ *
1968
+ * On first run with empty profiles, initializes from current auth.json (D-72).
1969
+ * Debounce at 100ms collapses rapid fs.watch events (D-67).
1970
+ */
1971
+
1972
+ /** Watcher interface with start/stop lifecycle. */
1973
+ interface AuthWatcher {
1974
+ start(): Promise<void>;
1975
+ stop(): void;
1976
+ }
1977
+ /** Options for creating an auth watcher. */
1978
+ interface AuthWatcherOptions {
1979
+ readonly authJsonPath?: string;
1980
+ readonly profilesPath?: string;
1981
+ readonly logger?: pino.Logger;
1982
+ }
1983
+ /**
1984
+ * Creates a file watcher for OpenCode's auth.json.
1985
+ *
1986
+ * Watches for credential changes and saves previous credentials to the
1987
+ * O-Switcher profile store before they are overwritten.
1988
+ */
1989
+ declare const createAuthWatcher: (options?: AuthWatcherOptions) => AuthWatcher;
1990
+
1991
+ /**
1992
+ * OpenCode plugin tool definitions for profile management.
1993
+ *
1994
+ * Per D-68, D-69, D-70: profiles-list and profiles-remove tools.
1995
+ * Follows the exact pattern from src/operator/plugin-tools.ts.
1996
+ *
1997
+ * Tools are stateless — they call loadProfiles/saveProfiles on each invocation,
1998
+ * so they can be created eagerly before plugin state is initialized.
1999
+ */
2000
+
2001
+ /** Map of profile tool definitions keyed by command name. */
2002
+ interface ProfileTools {
2003
+ readonly profilesList: ToolDefinition;
2004
+ readonly profilesRemove: ToolDefinition;
2005
+ }
2006
+ /** Options for creating profile tools. */
2007
+ interface ProfileToolsOptions {
2008
+ readonly profilesPath?: string;
2009
+ }
2010
+ /**
2011
+ * Creates profile tool definitions.
2012
+ *
2013
+ * Each tool operates on the profiles.json file directly (load/save per call).
2014
+ * No shared state required — tools are safe to create eagerly.
2015
+ *
2016
+ * @param options - Optional path override for profiles.json (useful for testing).
2017
+ */
2018
+ declare const createProfileTools: (options?: ProfileToolsOptions) => ProfileTools;
2019
+
2020
+ export { ADMISSION_RESULTS, type AdapterRequest, type AdapterResult, type AdmissionConfig, type AdmissionContext, type AdmissionController, type AdmissionDecision, type AdmissionResult, type ApiKeyCredential, type AttemptFn, type AuditCollector, type AuditEvent, type AuditLoggerOptions, type AuthCredential, type AuthResult, type AuthWatcher, type AuthWatcherOptions, type BackoffConfig, BackoffConfigSchema, type BackoffParams, type CircuitBreaker, type CircuitBreakerConfig, type ClassificationResult, type ConcurrencyTrackerApi, type ConfigDiagnostic, type ConfigDiff, type ConfigRef, ConfigValidationError, type ContinuationMode, type CooldownManager, type CorrelationId, DEFAULT_ALPHA, DEFAULT_BACKOFF_BASE_MS, DEFAULT_BACKOFF_JITTER, DEFAULT_BACKOFF_MAX_MS, DEFAULT_BACKOFF_MULTIPLIER, DEFAULT_BACKOFF_PARAMS, DEFAULT_FAILOVER_BUDGET, DEFAULT_RETRY, DEFAULT_RETRY_BUDGET, DEFAULT_TIMEOUT_MS, type DeploymentMode, DualBreaker, EXCLUSION_REASONS, type ErrorClass, ErrorClassSchema, type ExclusionReason, type ExecutionDeps, type ExecutionOrchestrator, type ExecutionProvenance, type ExecutionResult, type FailoverAttempt, type FailoverDeps, type FailoverOrchestrator, type FailoverRegistry, type FailoverResult, HEURISTIC_PATTERNS, INITIAL_HEALTH_SCORE, type InspectResult, type ListTargetsResult, type ModeAdapter, type ModeCapabilities, type NormalizedSignal, type OAuthCredential, type OperatorDeps, type OperatorTools, PROVIDER_PATTERNS, type ProfileEntry, type ProfileStore, type ProfileTools, type ProfileToolsOptions, type ProviderConfigLike, REDACT_PATHS, type RegistrySnapshot, type ReloadResult, type RequestTraceBuffer, type RequestTraceEntry, type RetryDecision, type RetryPolicy, type RoutingEventMap, type RoutingWeights, type SegmentProvenance, type SelectionRecord, type SignalFidelity, type StitchedOutput, type StreamBuffer, type StreamChunk, type StreamStitcher, type SwitcherConfig, SwitcherConfigSchema, TARGET_STATES, TEMPORAL_QUOTA_PATTERN, type TargetActionResult, type TargetConfig, TargetConfigSchema, type TargetEntry, TargetRegistry, type TargetState, type TargetView, addProfile, applyConfigDiff, checkHardRejects, classify, computeBackoffMs, computeConfigDiff, computeCooldownMs, computeScore, createAdmissionController, createAuditCollector, createAuditLogger, createAuthWatcher, createCircuitBreaker, createConcurrencyTracker, createCooldownManager, createExecutionOrchestrator, createFailoverOrchestrator, createLogSubscriber, createModeAdapter, createOperatorTools, createProfileTools, createRegistry, createRequestLogger, createRequestTraceBuffer, createRetryPolicy, createRoutingEventBus, createStreamBuffer, createStreamStitcher, detectDeploymentMode, determineContinuationMode, directSignalFromResponse, disableTarget, discoverTargets, discoverTargetsFromProfiles, drainTarget, extractRetryAfterMs, generateCorrelationId, getExclusionReason, getModeCapabilities, getSignalFidelity, getTargetStateTransition, heuristicSignalFromEvent, inspectRequest, isRetryable, listProfiles, listTargets, loadProfiles, nextProfileId, normalizeLatency, pauseTarget, reloadConfig, removeProfile, resumeTarget, saveProfiles, selectTarget, updateHealthScore, updateLatencyEma, validateBearerToken, validateConfig };