@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.
package/dist/sdk.d.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  import type { components } from "./generated/api";
2
+ import { type CredentialChainOptions } from "@hasna/contracts/client";
2
3
  export { providerFromPreset } from "./presets";
3
4
  export type ProviderPreset = components["schemas"]["ProviderPreset"];
4
5
  export type ProviderInput = components["schemas"]["ProviderInput"];
5
6
  export type Provider = components["schemas"]["Provider"];
6
7
  export type ProfileInput = components["schemas"]["ProfileInput"];
7
8
  export type Profile = components["schemas"]["Profile"];
9
+ export type ModelPolicy = components["schemas"]["ModelPolicy"];
10
+ export type RoutingEvent = components["schemas"]["RoutingEvent"];
8
11
  export type Model = components["schemas"]["Model"];
9
12
  export type Catalog = components["schemas"]["Catalog"];
10
13
  export type LaunchPlan = components["schemas"]["LaunchPlan"];
@@ -27,10 +30,18 @@ export type ClientOptions = {
27
30
  fetch?: typeof fetch;
28
31
  timeoutMs?: number;
29
32
  };
33
+ export type EnvironmentClientOptions = {
34
+ credentials?: CredentialChainOptions;
35
+ fetch?: typeof fetch;
36
+ timeoutMs?: number;
37
+ };
30
38
  export declare class SwitcherClient {
31
39
  private readonly options;
40
+ private transport?;
32
41
  readonly baseUrl: string;
33
42
  constructor(options: ClientOptions);
43
+ /** Use Contracts' paired authority/credential resolution on every request. */
44
+ static fromEnvironment(env?: Record<string, string | undefined>, options?: EnvironmentClientOptions): SwitcherClient;
34
45
  request<T>(method: string, path: string, body?: unknown, options?: {
35
46
  version?: number;
36
47
  idempotencyKey?: string;
@@ -226,6 +237,26 @@ export declare class SwitcherClient {
226
237
  providerId: string;
227
238
  harness: "claude" | "codex" | "grok" | "opencode" | "opencode2" | "pi" | "omp" | "dsh" | "cline" | "hermes" | "prime-agent" | "gemini" | "aider" | "kilo";
228
239
  model: string;
240
+ modelPolicy?: {
241
+ version: 1;
242
+ roles?: {
243
+ subagent?: string;
244
+ fast?: string;
245
+ planning?: string;
246
+ review?: string;
247
+ summary?: string;
248
+ compaction?: string;
249
+ weak?: string;
250
+ editor?: string;
251
+ };
252
+ allowedModels?: string[];
253
+ aliases?: {
254
+ [key: string]: string;
255
+ };
256
+ fallbacks?: {
257
+ [key: string]: string[];
258
+ };
259
+ };
229
260
  version: number;
230
261
  updatedAt: string;
231
262
  }>>;
@@ -235,6 +266,26 @@ export declare class SwitcherClient {
235
266
  providerId: string;
236
267
  harness: "claude" | "codex" | "grok" | "opencode" | "opencode2" | "pi" | "omp" | "dsh" | "cline" | "hermes" | "prime-agent" | "gemini" | "aider" | "kilo";
237
268
  model: string;
269
+ modelPolicy?: {
270
+ version: 1;
271
+ roles?: {
272
+ subagent?: string;
273
+ fast?: string;
274
+ planning?: string;
275
+ review?: string;
276
+ summary?: string;
277
+ compaction?: string;
278
+ weak?: string;
279
+ editor?: string;
280
+ };
281
+ allowedModels?: string[];
282
+ aliases?: {
283
+ [key: string]: string;
284
+ };
285
+ fallbacks?: {
286
+ [key: string]: string[];
287
+ };
288
+ };
238
289
  version: number;
239
290
  updatedAt: string;
240
291
  }>;
@@ -244,6 +295,26 @@ export declare class SwitcherClient {
244
295
  providerId: string;
245
296
  harness: "claude" | "codex" | "grok" | "opencode" | "opencode2" | "pi" | "omp" | "dsh" | "cline" | "hermes" | "prime-agent" | "gemini" | "aider" | "kilo";
246
297
  model: string;
298
+ modelPolicy?: {
299
+ version: 1;
300
+ roles?: {
301
+ subagent?: string;
302
+ fast?: string;
303
+ planning?: string;
304
+ review?: string;
305
+ summary?: string;
306
+ compaction?: string;
307
+ weak?: string;
308
+ editor?: string;
309
+ };
310
+ allowedModels?: string[];
311
+ aliases?: {
312
+ [key: string]: string;
313
+ };
314
+ fallbacks?: {
315
+ [key: string]: string[];
316
+ };
317
+ };
247
318
  version: number;
248
319
  updatedAt: string;
249
320
  }>;
@@ -253,6 +324,26 @@ export declare class SwitcherClient {
253
324
  providerId: string;
254
325
  harness: "claude" | "codex" | "grok" | "opencode" | "opencode2" | "pi" | "omp" | "dsh" | "cline" | "hermes" | "prime-agent" | "gemini" | "aider" | "kilo";
255
326
  model: string;
327
+ modelPolicy?: {
328
+ version: 1;
329
+ roles?: {
330
+ subagent?: string;
331
+ fast?: string;
332
+ planning?: string;
333
+ review?: string;
334
+ summary?: string;
335
+ compaction?: string;
336
+ weak?: string;
337
+ editor?: string;
338
+ };
339
+ allowedModels?: string[];
340
+ aliases?: {
341
+ [key: string]: string;
342
+ };
343
+ fallbacks?: {
344
+ [key: string]: string[];
345
+ };
346
+ };
256
347
  version: number;
257
348
  updatedAt: string;
258
349
  }>;
@@ -294,6 +385,26 @@ export declare class SwitcherClient {
294
385
  providerId: string;
295
386
  harness: "claude" | "codex" | "grok" | "opencode" | "opencode2" | "pi" | "omp" | "dsh" | "cline" | "hermes" | "prime-agent" | "gemini" | "aider" | "kilo";
296
387
  model: string;
388
+ modelPolicy?: {
389
+ version: 1;
390
+ roles?: {
391
+ subagent?: string;
392
+ fast?: string;
393
+ planning?: string;
394
+ review?: string;
395
+ summary?: string;
396
+ compaction?: string;
397
+ weak?: string;
398
+ editor?: string;
399
+ };
400
+ allowedModels?: string[];
401
+ aliases?: {
402
+ [key: string]: string;
403
+ };
404
+ fallbacks?: {
405
+ [key: string]: string[];
406
+ };
407
+ };
297
408
  version: number;
298
409
  updatedAt: string;
299
410
  };
@@ -317,9 +428,30 @@ export declare class SwitcherClient {
317
428
  warnings: string[];
318
429
  }>;
319
430
  listRuns(options?: {}): Promise<Page<{
431
+ modelPolicyVersion?: 1;
320
432
  profileId: string;
321
433
  harness: "claude" | "codex" | "grok" | "opencode" | "opencode2" | "pi" | "omp" | "dsh" | "cline" | "hermes" | "prime-agent" | "gemini" | "aider" | "kilo";
322
434
  model: string;
435
+ modelPolicy?: {
436
+ version: 1;
437
+ roles?: {
438
+ subagent?: string;
439
+ fast?: string;
440
+ planning?: string;
441
+ review?: string;
442
+ summary?: string;
443
+ compaction?: string;
444
+ weak?: string;
445
+ editor?: string;
446
+ };
447
+ allowedModels?: string[];
448
+ aliases?: {
449
+ [key: string]: string;
450
+ };
451
+ fallbacks?: {
452
+ [key: string]: string[];
453
+ };
454
+ };
323
455
  planToken: string;
324
456
  version: number;
325
457
  updatedAt: string;
@@ -331,11 +463,44 @@ export declare class SwitcherClient {
331
463
  startedAt: string;
332
464
  endedAt?: string;
333
465
  exitCode?: number;
466
+ routingEvents?: {
467
+ at: string;
468
+ requestId: string;
469
+ requestedModel: string;
470
+ resolvedModel?: string;
471
+ reportedModel?: string;
472
+ decision: "allow" | "alias" | "reject" | "fallback";
473
+ role?: "main" | "subagent" | "fast" | "planning" | "review" | "summary" | "compaction" | "weak" | "editor";
474
+ reason?: string;
475
+ upstreamStatus?: number;
476
+ }[];
477
+ routingEventsDropped?: number;
334
478
  }>>;
335
479
  getRun(id: string): Promise<{
480
+ modelPolicyVersion?: 1;
336
481
  profileId: string;
337
482
  harness: "claude" | "codex" | "grok" | "opencode" | "opencode2" | "pi" | "omp" | "dsh" | "cline" | "hermes" | "prime-agent" | "gemini" | "aider" | "kilo";
338
483
  model: string;
484
+ modelPolicy?: {
485
+ version: 1;
486
+ roles?: {
487
+ subagent?: string;
488
+ fast?: string;
489
+ planning?: string;
490
+ review?: string;
491
+ summary?: string;
492
+ compaction?: string;
493
+ weak?: string;
494
+ editor?: string;
495
+ };
496
+ allowedModels?: string[];
497
+ aliases?: {
498
+ [key: string]: string;
499
+ };
500
+ fallbacks?: {
501
+ [key: string]: string[];
502
+ };
503
+ };
339
504
  planToken: string;
340
505
  version: number;
341
506
  updatedAt: string;
@@ -347,11 +512,46 @@ export declare class SwitcherClient {
347
512
  startedAt: string;
348
513
  endedAt?: string;
349
514
  exitCode?: number;
515
+ routingEvents?: {
516
+ at: string;
517
+ requestId: string;
518
+ requestedModel: string;
519
+ resolvedModel?: string;
520
+ reportedModel?: string;
521
+ decision: "allow" | "alias" | "reject" | "fallback";
522
+ role?: "main" | "subagent" | "fast" | "planning" | "review" | "summary" | "compaction" | "weak" | "editor";
523
+ reason?: string;
524
+ upstreamStatus?: number;
525
+ }[];
526
+ routingEventsDropped?: number;
350
527
  }>;
351
- createRun(input: components["schemas"]["RunInput"], idempotencyKey?: string): Promise<{
528
+ createRun(input: Omit<components["schemas"]["RunInput"], "modelPolicyVersion"> & {
529
+ modelPolicyVersion?: 1;
530
+ }, idempotencyKey?: string): Promise<{
531
+ modelPolicyVersion?: 1;
352
532
  profileId: string;
353
533
  harness: "claude" | "codex" | "grok" | "opencode" | "opencode2" | "pi" | "omp" | "dsh" | "cline" | "hermes" | "prime-agent" | "gemini" | "aider" | "kilo";
354
534
  model: string;
535
+ modelPolicy?: {
536
+ version: 1;
537
+ roles?: {
538
+ subagent?: string;
539
+ fast?: string;
540
+ planning?: string;
541
+ review?: string;
542
+ summary?: string;
543
+ compaction?: string;
544
+ weak?: string;
545
+ editor?: string;
546
+ };
547
+ allowedModels?: string[];
548
+ aliases?: {
549
+ [key: string]: string;
550
+ };
551
+ fallbacks?: {
552
+ [key: string]: string[];
553
+ };
554
+ };
355
555
  planToken: string;
356
556
  version: number;
357
557
  updatedAt: string;
@@ -363,14 +563,44 @@ export declare class SwitcherClient {
363
563
  startedAt: string;
364
564
  endedAt?: string;
365
565
  exitCode?: number;
566
+ routingEvents?: {
567
+ at: string;
568
+ requestId: string;
569
+ requestedModel: string;
570
+ resolvedModel?: string;
571
+ reportedModel?: string;
572
+ decision: "allow" | "alias" | "reject" | "fallback";
573
+ role?: "main" | "subagent" | "fast" | "planning" | "review" | "summary" | "compaction" | "weak" | "editor";
574
+ reason?: string;
575
+ upstreamStatus?: number;
576
+ }[];
577
+ routingEventsDropped?: number;
366
578
  }>;
367
- finishRun(id: string, version: number, input: {
368
- status: "exited" | "failed" | "interrupted";
369
- exitCode: number;
370
- }, idempotencyKey?: string): Promise<{
579
+ finishRun(id: string, version: number, input: components["schemas"]["RunUpdate"], idempotencyKey?: string): Promise<{
580
+ modelPolicyVersion?: 1;
371
581
  profileId: string;
372
582
  harness: "claude" | "codex" | "grok" | "opencode" | "opencode2" | "pi" | "omp" | "dsh" | "cline" | "hermes" | "prime-agent" | "gemini" | "aider" | "kilo";
373
583
  model: string;
584
+ modelPolicy?: {
585
+ version: 1;
586
+ roles?: {
587
+ subagent?: string;
588
+ fast?: string;
589
+ planning?: string;
590
+ review?: string;
591
+ summary?: string;
592
+ compaction?: string;
593
+ weak?: string;
594
+ editor?: string;
595
+ };
596
+ allowedModels?: string[];
597
+ aliases?: {
598
+ [key: string]: string;
599
+ };
600
+ fallbacks?: {
601
+ [key: string]: string[];
602
+ };
603
+ };
374
604
  planToken: string;
375
605
  version: number;
376
606
  updatedAt: string;
@@ -382,6 +612,18 @@ export declare class SwitcherClient {
382
612
  startedAt: string;
383
613
  endedAt?: string;
384
614
  exitCode?: number;
615
+ routingEvents?: {
616
+ at: string;
617
+ requestId: string;
618
+ requestedModel: string;
619
+ resolvedModel?: string;
620
+ reportedModel?: string;
621
+ decision: "allow" | "alias" | "reject" | "fallback";
622
+ role?: "main" | "subagent" | "fast" | "planning" | "review" | "summary" | "compaction" | "weak" | "editor";
623
+ reason?: string;
624
+ upstreamStatus?: number;
625
+ }[];
626
+ routingEventsDropped?: number;
385
627
  }>;
386
628
  }
387
- export declare function clientFromEnv(env?: Record<string, string | undefined>): SwitcherClient;
629
+ export declare function clientFromEnv(env?: Record<string, string | undefined>, options?: EnvironmentClientOptions): SwitcherClient;
package/dist/sdk.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,