@hasna/switcher 0.1.2 → 0.1.4

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.
@@ -345,6 +345,30 @@ export interface components {
345
345
  /** @enum {string} */
346
346
  harness: "claude" | "codex" | "grok" | "opencode" | "opencode2" | "pi" | "omp" | "dsh" | "cline" | "hermes" | "prime-agent" | "gemini" | "aider" | "kilo";
347
347
  model: string;
348
+ modelPolicy?: {
349
+ /**
350
+ * @default 1
351
+ * @enum {number}
352
+ */
353
+ version: 1;
354
+ roles?: {
355
+ subagent?: string;
356
+ fast?: string;
357
+ planning?: string;
358
+ review?: string;
359
+ summary?: string;
360
+ compaction?: string;
361
+ weak?: string;
362
+ editor?: string;
363
+ };
364
+ allowedModels?: string[];
365
+ aliases?: {
366
+ [key: string]: string;
367
+ };
368
+ fallbacks?: {
369
+ [key: string]: string[];
370
+ };
371
+ };
348
372
  };
349
373
  Profile: {
350
374
  id: string;
@@ -353,6 +377,30 @@ export interface components {
353
377
  /** @enum {string} */
354
378
  harness: "claude" | "codex" | "grok" | "opencode" | "opencode2" | "pi" | "omp" | "dsh" | "cline" | "hermes" | "prime-agent" | "gemini" | "aider" | "kilo";
355
379
  model: string;
380
+ modelPolicy?: {
381
+ /**
382
+ * @default 1
383
+ * @enum {number}
384
+ */
385
+ version: 1;
386
+ roles?: {
387
+ subagent?: string;
388
+ fast?: string;
389
+ planning?: string;
390
+ review?: string;
391
+ summary?: string;
392
+ compaction?: string;
393
+ weak?: string;
394
+ editor?: string;
395
+ };
396
+ allowedModels?: string[];
397
+ aliases?: {
398
+ [key: string]: string;
399
+ };
400
+ fallbacks?: {
401
+ [key: string]: string[];
402
+ };
403
+ };
356
404
  version: number;
357
405
  updatedAt: string;
358
406
  };
@@ -368,6 +416,44 @@ export interface components {
368
416
  supportedParameters?: string[];
369
417
  supportedGenerationMethods?: string[];
370
418
  };
419
+ ModelPolicy: {
420
+ /**
421
+ * @default 1
422
+ * @enum {number}
423
+ */
424
+ version: 1;
425
+ roles?: {
426
+ subagent?: string;
427
+ fast?: string;
428
+ planning?: string;
429
+ review?: string;
430
+ summary?: string;
431
+ compaction?: string;
432
+ weak?: string;
433
+ editor?: string;
434
+ };
435
+ allowedModels?: string[];
436
+ aliases?: {
437
+ [key: string]: string;
438
+ };
439
+ fallbacks?: {
440
+ [key: string]: string[];
441
+ };
442
+ };
443
+ RoutingEvent: {
444
+ /** Format: date-time */
445
+ at: string;
446
+ requestId: string;
447
+ requestedModel: string;
448
+ resolvedModel?: string;
449
+ reportedModel?: string;
450
+ /** @enum {string} */
451
+ decision: "allow" | "alias" | "reject" | "fallback";
452
+ /** @enum {string} */
453
+ role?: "main" | "subagent" | "fast" | "planning" | "review" | "summary" | "compaction" | "weak" | "editor";
454
+ reason?: string;
455
+ upstreamStatus?: number;
456
+ };
371
457
  ModelPage: {
372
458
  data: {
373
459
  id: string;
@@ -451,6 +537,30 @@ export interface components {
451
537
  /** @enum {string} */
452
538
  harness: "claude" | "codex" | "grok" | "opencode" | "opencode2" | "pi" | "omp" | "dsh" | "cline" | "hermes" | "prime-agent" | "gemini" | "aider" | "kilo";
453
539
  model: string;
540
+ modelPolicy?: {
541
+ /**
542
+ * @default 1
543
+ * @enum {number}
544
+ */
545
+ version: 1;
546
+ roles?: {
547
+ subagent?: string;
548
+ fast?: string;
549
+ planning?: string;
550
+ review?: string;
551
+ summary?: string;
552
+ compaction?: string;
553
+ weak?: string;
554
+ editor?: string;
555
+ };
556
+ allowedModels?: string[];
557
+ aliases?: {
558
+ [key: string]: string;
559
+ };
560
+ fallbacks?: {
561
+ [key: string]: string[];
562
+ };
563
+ };
454
564
  version: number;
455
565
  updatedAt: string;
456
566
  };
@@ -475,22 +585,89 @@ export interface components {
475
585
  warnings: string[];
476
586
  };
477
587
  RunInput: {
588
+ /** @enum {number} */
589
+ modelPolicyVersion: 1;
478
590
  profileId: string;
479
591
  /** @enum {string} */
480
592
  harness: "claude" | "codex" | "grok" | "opencode" | "opencode2" | "pi" | "omp" | "dsh" | "cline" | "hermes" | "prime-agent" | "gemini" | "aider" | "kilo";
481
593
  model: string;
594
+ modelPolicy?: {
595
+ /**
596
+ * @default 1
597
+ * @enum {number}
598
+ */
599
+ version: 1;
600
+ roles?: {
601
+ subagent?: string;
602
+ fast?: string;
603
+ planning?: string;
604
+ review?: string;
605
+ summary?: string;
606
+ compaction?: string;
607
+ weak?: string;
608
+ editor?: string;
609
+ };
610
+ allowedModels?: string[];
611
+ aliases?: {
612
+ [key: string]: string;
613
+ };
614
+ fallbacks?: {
615
+ [key: string]: string[];
616
+ };
617
+ };
482
618
  planToken: string;
483
619
  };
484
620
  RunUpdate: {
485
621
  /** @enum {string} */
486
622
  status: "exited" | "failed" | "interrupted";
487
623
  exitCode: number;
624
+ routingEvents?: {
625
+ /** Format: date-time */
626
+ at: string;
627
+ requestId: string;
628
+ requestedModel: string;
629
+ resolvedModel?: string;
630
+ reportedModel?: string;
631
+ /** @enum {string} */
632
+ decision: "allow" | "alias" | "reject" | "fallback";
633
+ /** @enum {string} */
634
+ role?: "main" | "subagent" | "fast" | "planning" | "review" | "summary" | "compaction" | "weak" | "editor";
635
+ reason?: string;
636
+ upstreamStatus?: number;
637
+ }[];
638
+ routingEventsDropped?: number;
488
639
  };
489
640
  Run: {
641
+ /** @enum {number} */
642
+ modelPolicyVersion?: 1;
490
643
  profileId: string;
491
644
  /** @enum {string} */
492
645
  harness: "claude" | "codex" | "grok" | "opencode" | "opencode2" | "pi" | "omp" | "dsh" | "cline" | "hermes" | "prime-agent" | "gemini" | "aider" | "kilo";
493
646
  model: string;
647
+ modelPolicy?: {
648
+ /**
649
+ * @default 1
650
+ * @enum {number}
651
+ */
652
+ version: 1;
653
+ roles?: {
654
+ subagent?: string;
655
+ fast?: string;
656
+ planning?: string;
657
+ review?: string;
658
+ summary?: string;
659
+ compaction?: string;
660
+ weak?: string;
661
+ editor?: string;
662
+ };
663
+ allowedModels?: string[];
664
+ aliases?: {
665
+ [key: string]: string;
666
+ };
667
+ fallbacks?: {
668
+ [key: string]: string[];
669
+ };
670
+ };
494
671
  planToken: string;
495
672
  version: number;
496
673
  updatedAt: string;
@@ -503,6 +680,21 @@ export interface components {
503
680
  startedAt: string;
504
681
  endedAt?: string;
505
682
  exitCode?: number;
683
+ routingEvents?: {
684
+ /** Format: date-time */
685
+ at: string;
686
+ requestId: string;
687
+ requestedModel: string;
688
+ resolvedModel?: string;
689
+ reportedModel?: string;
690
+ /** @enum {string} */
691
+ decision: "allow" | "alias" | "reject" | "fallback";
692
+ /** @enum {string} */
693
+ role?: "main" | "subagent" | "fast" | "planning" | "review" | "summary" | "compaction" | "weak" | "editor";
694
+ reason?: string;
695
+ upstreamStatus?: number;
696
+ }[];
697
+ routingEventsDropped?: number;
506
698
  };
507
699
  Health: {
508
700
  /** @enum {string} */
@@ -1,4 +1,7 @@
1
1
  import type { Model, Provider, Profile } from "./domain";
2
+ import type { CompiledModelPolicy, ModelPolicy } from "./model-policy";
3
+ import type { NativeModelPolicy } from "./native-model-policy";
4
+ import type { RoutingEvent } from "./inference-gateway";
2
5
  export type HarnessId = Profile["harness"];
3
6
  export type HarnessLaunchInput = {
4
7
  harness: HarnessId;
@@ -14,6 +17,12 @@ export type HarnessLaunchInput = {
14
17
  cwd: string;
15
18
  version?: string;
16
19
  sessionDir?: string;
20
+ providerId?: string;
21
+ providerBaseUrl?: string;
22
+ modelPolicy?: ModelPolicy;
23
+ compiledPolicy?: CompiledModelPolicy;
24
+ nativePolicy?: NativeModelPolicy;
25
+ onRoutingEvent?: (event: RoutingEvent) => void;
17
26
  };
18
27
  export type PreparedLaunch = {
19
28
  executable: string;
@@ -0,0 +1,30 @@
1
+ export type HermesModelRoles = {
2
+ subagent?: string;
3
+ fast?: string;
4
+ planning?: string;
5
+ review?: string;
6
+ summary?: string;
7
+ compaction?: string;
8
+ weak?: string;
9
+ editor?: string;
10
+ };
11
+ export type HermesModelPolicyFragment = {
12
+ auxiliary: Record<string, {
13
+ api_mode: string;
14
+ provider: "custom";
15
+ model: string;
16
+ base_url: string;
17
+ api_key_env: "SWITCHER_HERMES_AUX_API_KEY";
18
+ fallback_chain: [];
19
+ }>;
20
+ delegation: {
21
+ model: string;
22
+ provider: "custom";
23
+ base_url: string;
24
+ };
25
+ env: {
26
+ apiKeyEnv: "SWITCHER_HERMES_AUX_API_KEY";
27
+ };
28
+ };
29
+ /** Compile verified Hermes auxiliary config fields without serializing credentials. */
30
+ export declare function compileHermesModelPolicy(currentModel: string, roles: HermesModelRoles, baseUrl: string, apiMode?: string): HermesModelPolicyFragment;
package/dist/index.js CHANGED
@@ -1,10 +1,56 @@
1
1
  // src/domain.ts
2
+ import { z as z2 } from "zod";
3
+
4
+ // src/model-policy-schema.ts
2
5
  import { z } from "zod";
3
- var harnessSchema = z.enum(["claude", "codex", "grok", "opencode", "opencode2", "pi", "omp", "dsh", "cline", "hermes", "prime-agent", "gemini", "aider", "kilo"]);
4
- var protocolSchema = z.enum(["anthropic-messages", "openai-responses", "openai-chat", "gemini-generate-content"]);
5
- var idSchema = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/);
6
- var label = z.string().min(1).max(200);
7
- var envRef = z.string().regex(/^SWITCHER_PROVIDER_[A-Z0-9_]+$/);
6
+ var policyModelIdSchema = z.string().min(1).max(300).regex(/^[^\u0000-\u001f\u007f]+$/);
7
+ var modelPolicyRoleSchema = z.enum(["subagent", "fast", "planning", "review", "summary", "compaction", "weak", "editor"]);
8
+ var boundedModelList = z.array(policyModelIdSchema).max(500);
9
+ var aliasSchema = z.string().regex(/^[A-Za-z0-9._/-]{1,120}$/).refine((v) => !["__proto__", "prototype", "constructor"].includes(v));
10
+ var boundedModelMap = z.record(aliasSchema, policyModelIdSchema).superRefine((value, ctx) => {
11
+ if (Object.keys(value).length > 200)
12
+ ctx.addIssue({ code: "custom", message: "Model policy maps may contain at most 200 entries." });
13
+ });
14
+ var fallbacksSchema = z.record(policyModelIdSchema, z.array(policyModelIdSchema).max(20)).superRefine((value, ctx) => {
15
+ if (Object.keys(value).length > 200)
16
+ ctx.addIssue({ code: "custom", message: "Model policy fallback maps may contain at most 200 entries." });
17
+ });
18
+ var modelPolicySchema = z.object({
19
+ version: z.literal(1).default(1),
20
+ roles: z.object({
21
+ subagent: policyModelIdSchema.optional(),
22
+ fast: policyModelIdSchema.optional(),
23
+ planning: policyModelIdSchema.optional(),
24
+ review: policyModelIdSchema.optional(),
25
+ summary: policyModelIdSchema.optional(),
26
+ compaction: policyModelIdSchema.optional(),
27
+ weak: policyModelIdSchema.optional(),
28
+ editor: policyModelIdSchema.optional()
29
+ }).strict().optional(),
30
+ allowedModels: boundedModelList.optional(),
31
+ aliases: boundedModelMap.optional(),
32
+ fallbacks: fallbacksSchema.optional()
33
+ }).strict();
34
+ var routingDecisionSchema = z.enum(["allow", "alias", "reject", "fallback"]);
35
+ var routingEventRoleSchema = z.enum(["main", ...modelPolicyRoleSchema.options]);
36
+ var routingEventSchema = z.object({
37
+ at: z.string().datetime({ offset: true }),
38
+ requestId: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/),
39
+ requestedModel: policyModelIdSchema,
40
+ resolvedModel: policyModelIdSchema.optional(),
41
+ reportedModel: policyModelIdSchema.optional(),
42
+ decision: routingDecisionSchema,
43
+ role: routingEventRoleSchema.optional(),
44
+ reason: z.string().regex(/^[a-z][a-z0-9_]{0,63}$/).optional(),
45
+ upstreamStatus: z.number().int().min(100).max(599).optional()
46
+ }).strict();
47
+ var routingEventsSchema = z.array(routingEventSchema).max(1000);
48
+ // src/domain.ts
49
+ var harnessSchema = z2.enum(["claude", "codex", "grok", "opencode", "opencode2", "pi", "omp", "dsh", "cline", "hermes", "prime-agent", "gemini", "aider", "kilo"]);
50
+ var protocolSchema = z2.enum(["anthropic-messages", "openai-responses", "openai-chat", "gemini-generate-content"]);
51
+ var idSchema = z2.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/);
52
+ var label = z2.string().min(1).max(200);
53
+ var envRef = z2.string().regex(/^SWITCHER_PROVIDER_[A-Z0-9_]+$/);
8
54
  function endpoint(value) {
9
55
  let url;
10
56
  try {
@@ -17,74 +63,79 @@ function endpoint(value) {
17
63
  throw new Fault(400, "invalid_url", "URL must use HTTPS, contain no credentials/query/fragment, or use HTTP on loopback.");
18
64
  return url.href.replace(/\/+$/, "");
19
65
  }
20
- var urlSchema = z.string().max(2000).superRefine((v, ctx) => {
66
+ var urlSchema = z2.string().max(2000).superRefine((v, ctx) => {
21
67
  try {
22
68
  endpoint(v);
23
69
  } catch {
24
70
  ctx.addIssue({ code: "custom", message: "Invalid endpoint URL" });
25
71
  }
26
72
  }).transform(endpoint);
27
- var modelSchema = z.object({
28
- id: z.string().min(1).max(300),
73
+ var modelSchema = z2.object({
74
+ id: z2.string().min(1).max(300),
29
75
  name: label,
30
- description: z.string().max(8000).optional(),
31
- available: z.boolean().optional(),
32
- contextWindow: z.number().int().positive().optional(),
33
- maxOutputTokens: z.number().int().positive().optional(),
34
- inputModalities: z.array(z.string().max(50)).max(20).optional(),
35
- outputModalities: z.array(z.string().max(50)).max(20).optional(),
36
- supportedParameters: z.array(z.string().max(100)).max(100).optional(),
37
- supportedGenerationMethods: z.array(z.string().min(1).max(100)).max(100).optional()
76
+ description: z2.string().max(8000).optional(),
77
+ available: z2.boolean().optional(),
78
+ contextWindow: z2.number().int().positive().optional(),
79
+ maxOutputTokens: z2.number().int().positive().optional(),
80
+ inputModalities: z2.array(z2.string().max(50)).max(20).optional(),
81
+ outputModalities: z2.array(z2.string().max(50)).max(20).optional(),
82
+ supportedParameters: z2.array(z2.string().max(100)).max(100).optional(),
83
+ supportedGenerationMethods: z2.array(z2.string().min(1).max(100)).max(100).optional()
38
84
  }).strict();
39
- var providerInputSchema = z.object({
85
+ var providerInputSchema = z2.object({
40
86
  id: idSchema,
41
87
  name: label,
42
88
  baseUrl: urlSchema,
43
89
  protocol: protocolSchema,
44
90
  credentialEnv: envRef.optional(),
45
- authStyle: z.enum(["bearer", "x-api-key", "api-key"]).default("bearer"),
91
+ authStyle: z2.enum(["bearer", "x-api-key", "api-key"]).default("bearer"),
46
92
  catalogBaseUrl: urlSchema.optional(),
47
- catalogFormat: z.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "gemini", "none"]).optional(),
48
- catalogAuthStyle: z.enum(["bearer", "x-api-key", "api-key", "none"]).optional(),
93
+ catalogFormat: z2.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "gemini", "none"]).optional(),
94
+ catalogAuthStyle: z2.enum(["bearer", "x-api-key", "api-key", "none"]).optional(),
49
95
  catalogCredentialEnv: envRef.optional(),
50
- catalogAccountId: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/).optional(),
51
- modelsPath: z.string().regex(/^[a-zA-Z0-9_/-]+$/).max(200).default("models"),
52
- manualModels: z.array(modelSchema).max(1e4).default([])
96
+ catalogAccountId: z2.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/).optional(),
97
+ modelsPath: z2.string().regex(/^[a-zA-Z0-9_/-]+$/).max(200).default("models"),
98
+ manualModels: z2.array(modelSchema).max(1e4).default([])
53
99
  }).strict().refine((p) => !p.modelsPath.split("/").includes("..") && !p.modelsPath.startsWith("/"), "modelsPath must be relative");
54
- var providerPresetSchema = z.object({
100
+ var providerPresetSchema = z2.object({
55
101
  id: idSchema,
56
102
  name: label,
57
103
  credentialEnv: envRef.optional(),
58
- credentialAliases: z.array(z.string().regex(/^[A-Z][A-Z0-9_]+$/)),
59
- protocols: z.array(z.object({
104
+ credentialAliases: z2.array(z2.string().regex(/^[A-Z][A-Z0-9_]+$/)),
105
+ protocols: z2.array(z2.object({
60
106
  protocol: protocolSchema,
61
107
  baseUrl: urlSchema.optional(),
62
- authStyle: z.enum(["bearer", "x-api-key", "api-key"]),
108
+ authStyle: z2.enum(["bearer", "x-api-key", "api-key"]),
63
109
  catalogBaseUrl: urlSchema.optional(),
64
- catalogFormat: z.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "gemini", "none"]),
65
- catalogAuthStyle: z.enum(["bearer", "x-api-key", "api-key", "none"]).optional(),
66
- modelsPath: z.string(),
67
- notes: z.array(z.string())
110
+ catalogFormat: z2.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "gemini", "none"]),
111
+ catalogAuthStyle: z2.enum(["bearer", "x-api-key", "api-key", "none"]).optional(),
112
+ modelsPath: z2.string(),
113
+ notes: z2.array(z2.string())
68
114
  }).strict()).min(1),
69
- sources: z.array(z.string().url()),
70
- verification: z.literal("documented")
115
+ sources: z2.array(z2.string().url()),
116
+ verification: z2.literal("documented")
71
117
  }).strict();
72
- var profileInputSchema = z.object({
118
+ var profileInputSchema = z2.object({
73
119
  id: idSchema,
74
120
  name: label,
75
121
  providerId: idSchema,
76
122
  harness: harnessSchema,
77
- model: z.string().min(1).max(300)
123
+ model: z2.string().min(1).max(300),
124
+ modelPolicy: modelPolicySchema.optional()
78
125
  }).strict();
79
- var runInputSchema = z.object({
126
+ var runInputSchema = z2.object({
127
+ modelPolicyVersion: z2.literal(1),
80
128
  profileId: idSchema,
81
129
  harness: harnessSchema,
82
- model: z.string().min(1).max(300),
83
- planToken: z.string().regex(/^[a-f0-9]{64}$/)
130
+ model: z2.string().min(1).max(300),
131
+ modelPolicy: modelPolicySchema.optional(),
132
+ planToken: z2.string().regex(/^[a-f0-9]{64}$/)
84
133
  }).strict();
85
- var runUpdateSchema = z.object({
86
- status: z.enum(["exited", "failed", "interrupted"]),
87
- exitCode: z.number().int().min(0).max(255)
134
+ var runUpdateSchema = z2.object({
135
+ status: z2.enum(["exited", "failed", "interrupted"]),
136
+ exitCode: z2.number().int().min(0).max(255),
137
+ routingEvents: routingEventsSchema.optional(),
138
+ routingEventsDropped: z2.number().int().min(0).max(1e6).optional()
88
139
  }).strict();
89
140
 
90
141
  class Fault extends Error {
@@ -149,7 +200,7 @@ async function boundedJson(response, maxBytes = MAX_BYTES) {
149
200
  }
150
201
 
151
202
  // src/sdk.ts
152
- import { resolveCredential } from "@hasna/contracts/client";
203
+ import { createClientTransport } from "@hasna/contracts/client";
153
204
 
154
205
  // src/presets.ts
155
206
  var route = (protocol, baseUrl, options = {}) => ({
@@ -310,6 +361,7 @@ function apiError(status, data, apiKey) {
310
361
 
311
362
  class SwitcherClient {
312
363
  options;
364
+ transport;
313
365
  baseUrl;
314
366
  constructor(options) {
315
367
  this.baseUrl = endpoint(options.baseUrl).replace(/\/v1$/, "");
@@ -317,9 +369,50 @@ class SwitcherClient {
317
369
  throw new Error("Switcher API key is required.");
318
370
  this.options = { ...options };
319
371
  }
372
+ static fromEnvironment(env = process.env, options = {}) {
373
+ const shared = createClientTransport("switcher", env, {
374
+ credentials: options.credentials,
375
+ timeoutMs: options.timeoutMs ?? 120000,
376
+ retry: false,
377
+ fetchImpl: async (input, init) => {
378
+ const address = String(input).replace(/\/v1\/(health|ready|version)$/, "/$1");
379
+ const key = new Headers(init?.headers).get("x-api-key") ?? "";
380
+ let response;
381
+ try {
382
+ response = await (options.fetch ?? fetch)(address, init);
383
+ } catch {
384
+ throw new SwitcherError(0, "connection_failed", "Switcher API request failed; check endpoint and service availability.");
385
+ }
386
+ if (response.status === 401 || response.status === 403) {
387
+ await response.body?.cancel().catch(() => {});
388
+ throw new SwitcherError(response.status, "api_error", `Switcher API returned HTTP ${response.status}. Check the configured credential; no alternate identity was selected.`);
389
+ }
390
+ let data;
391
+ try {
392
+ data = await boundedJson(response);
393
+ } catch {
394
+ throw new SwitcherError(response.status, "invalid_response", "Switcher API returned invalid JSON.");
395
+ }
396
+ if (!response.ok)
397
+ throw apiError(response.status, data, key);
398
+ return Response.json(data);
399
+ }
400
+ });
401
+ const client = new SwitcherClient({ baseUrl: shared.resolution.baseUrl, apiKey: () => {
402
+ throw new Error("Shared credential transport was not invoked.");
403
+ } });
404
+ client.transport = shared.client;
405
+ return client;
406
+ }
320
407
  async request(method, path, body, options = {}) {
321
408
  if (!/^\/v1\/[a-zA-Z0-9/?&=._%+-]+$/.test(path) && !["/health", "/ready", "/version"].includes(path) || path.includes(".."))
322
409
  throw new Error("Invalid API path.");
410
+ if (this.transport)
411
+ return this.transport.request(method, path.startsWith("/v1/") ? path.slice(3) : path, body, {
412
+ idempotencyKey: method === "GET" ? undefined : options.idempotencyKey ?? crypto.randomUUID(),
413
+ headers: options.version === undefined ? undefined : { "if-match": String(options.version) },
414
+ retry: false
415
+ });
323
416
  const apiKey = typeof this.options.apiKey === "function" ? this.options.apiKey() : this.options.apiKey;
324
417
  if (!apiKey || /[\r\n]/.test(apiKey))
325
418
  throw new Error("Switcher API key is required.");
@@ -410,17 +503,14 @@ class SwitcherClient {
410
503
  return this.request("GET", `/v1/runs/${encodeURIComponent(id)}`);
411
504
  }
412
505
  createRun(input, idempotencyKey) {
413
- return this.request("POST", "/v1/runs", input, { idempotencyKey });
506
+ return this.request("POST", "/v1/runs", { ...input, modelPolicyVersion: 1 }, { idempotencyKey });
414
507
  }
415
508
  finishRun(id, version, input, idempotencyKey) {
416
509
  return this.request("PATCH", `/v1/runs/${encodeURIComponent(id)}`, input, { version, idempotencyKey });
417
510
  }
418
511
  }
419
- function clientFromEnv(env = process.env) {
420
- const credential = () => resolveCredential("switcher", Object.fromEntries(Object.entries(env).filter(([name]) => name === "HASNA_SWITCHER_API_KEY")), { keychain: { enabled: false } })?.apiKey ?? "";
421
- if (!env.HASNA_SWITCHER_API_URL || !credential())
422
- throw new Error("Set HASNA_SWITCHER_API_URL and HASNA_SWITCHER_API_KEY; no local database fallback is available.");
423
- return new SwitcherClient({ baseUrl: env.HASNA_SWITCHER_API_URL, apiKey: credential });
512
+ function clientFromEnv(env = process.env, options = {}) {
513
+ return SwitcherClient.fromEnvironment(env, options);
424
514
  }
425
515
  export {
426
516
  providerFromPreset,
@@ -0,0 +1,24 @@
1
+ import type { HarnessLaunchInput } from "./harness-types";
2
+ import { type CompiledModelPolicy } from "./model-policy";
3
+ export type RoutingEvent = {
4
+ at: string;
5
+ requestId: string;
6
+ requestedModel: string;
7
+ resolvedModel?: string;
8
+ reportedModel?: string;
9
+ decision: "allow" | "alias" | "reject" | "fallback";
10
+ reason?: string;
11
+ upstreamStatus?: number;
12
+ };
13
+ type GatewayInput = HarnessLaunchInput & {
14
+ compiledPolicy: CompiledModelPolicy;
15
+ catalogPath: string;
16
+ onRoutingEvent?: (event: RoutingEvent) => void;
17
+ };
18
+ /** A per-launch credential boundary: the native client never receives the provider key. */
19
+ export declare function createInferenceGateway(input: GatewayInput): {
20
+ baseUrl: string;
21
+ token: string;
22
+ cleanup: () => Promise<void>;
23
+ };
24
+ export {};
@@ -1,6 +1,8 @@
1
+ import type { PreparedLaunch } from "./harness-types";
1
2
  import { SwitcherClient } from "./sdk";
2
3
  import { type LaunchPlan, type ProviderInput } from "./domain";
3
- import { type OriContract, type OriLaunchPlan } from "./ori-backend";
4
+ import { type OriContract } from "./ori-backend";
5
+ import type { RoutingEvent } from "./inference-gateway";
4
6
  export { childEnvironment } from "./harness-environment";
5
7
  export type LaunchBackend = "direct" | "ori";
6
8
  export type LaunchOptions = {
@@ -18,6 +20,7 @@ export type LaunchOptions = {
18
20
  type OriPreparationOptions = Pick<LaunchOptions, "oriExecutable" | "args" | "resolveCredential" | "credentialEnv"> & {
19
21
  stateDir?: string;
20
22
  cwd?: string;
23
+ onRoutingEvent?: (event: RoutingEvent) => void;
21
24
  };
22
25
  type OriSupportedHarness = Exclude<LaunchPlan["profile"]["harness"], "omp" | "cline" | "hermes" | "prime-agent" | "gemini" | "aider" | "opencode" | "kilo">;
23
26
  export declare function validateOriForPlan(plan: LaunchPlan, options?: Pick<OriPreparationOptions, "oriExecutable" | "args" | "credentialEnv" | "cwd">): Promise<{
@@ -38,8 +41,10 @@ declare function buildOriRequest(plan: LaunchPlan, target: OriSupportedHarness,
38
41
  };
39
42
  readonly args: string[];
40
43
  };
41
- export declare function prepareOriForPlan(plan: LaunchPlan, options?: OriPreparationOptions): Promise<{
44
+ export declare function prepareOriForPlan(plan: LaunchPlan, options: OriPreparationOptions & {
45
+ stateDir: string;
46
+ }): Promise<{
42
47
  contract: OriContract;
43
- prepared: OriLaunchPlan;
48
+ prepared: PreparedLaunch;
44
49
  }>;
45
50
  export declare function launch(client: SwitcherClient, profileId: string, options?: LaunchOptions): Promise<number>;