@hasna/switcher 0.1.2 → 0.1.3

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.
@@ -11,13 +11,63 @@ import { chmod, mkdir } from "fs/promises";
11
11
  import { dirname, resolve } from "path";
12
12
 
13
13
  // src/domain.ts
14
+ import { z as z2 } from "zod";
15
+
16
+ // src/model-policy-schema.ts
14
17
  import { z } from "zod";
15
- var VERSION = "0.1.2";
16
- var harnessSchema = z.enum(["claude", "codex", "grok", "opencode", "opencode2", "pi", "omp", "dsh", "cline", "hermes", "prime-agent", "gemini", "aider", "kilo"]);
17
- var protocolSchema = z.enum(["anthropic-messages", "openai-responses", "openai-chat", "gemini-generate-content"]);
18
- var idSchema = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/);
19
- var label = z.string().min(1).max(200);
20
- var envRef = z.string().regex(/^SWITCHER_PROVIDER_[A-Z0-9_]+$/);
18
+ var policyModelIdSchema = z.string().min(1).max(300).regex(/^[^\u0000-\u001f\u007f]+$/);
19
+ var modelPolicyRoleSchema = z.enum(["subagent", "fast", "planning", "review", "summary", "compaction", "weak", "editor"]);
20
+ var boundedModelList = z.array(policyModelIdSchema).max(500);
21
+ var aliasSchema = z.string().regex(/^[A-Za-z0-9._/-]{1,120}$/).refine((v) => !["__proto__", "prototype", "constructor"].includes(v));
22
+ var boundedModelMap = z.record(aliasSchema, policyModelIdSchema).superRefine((value, ctx) => {
23
+ if (Object.keys(value).length > 200)
24
+ ctx.addIssue({ code: "custom", message: "Model policy maps may contain at most 200 entries." });
25
+ });
26
+ var fallbacksSchema = z.record(policyModelIdSchema, z.array(policyModelIdSchema).max(20)).superRefine((value, ctx) => {
27
+ if (Object.keys(value).length > 200)
28
+ ctx.addIssue({ code: "custom", message: "Model policy fallback maps may contain at most 200 entries." });
29
+ });
30
+ var modelPolicySchema = z.object({
31
+ version: z.literal(1).default(1),
32
+ roles: z.object({
33
+ subagent: policyModelIdSchema.optional(),
34
+ fast: policyModelIdSchema.optional(),
35
+ planning: policyModelIdSchema.optional(),
36
+ review: policyModelIdSchema.optional(),
37
+ summary: policyModelIdSchema.optional(),
38
+ compaction: policyModelIdSchema.optional(),
39
+ weak: policyModelIdSchema.optional(),
40
+ editor: policyModelIdSchema.optional()
41
+ }).strict().optional(),
42
+ allowedModels: boundedModelList.optional(),
43
+ aliases: boundedModelMap.optional(),
44
+ fallbacks: fallbacksSchema.optional()
45
+ }).strict();
46
+ var routingDecisionSchema = z.enum(["allow", "alias", "reject", "fallback"]);
47
+ var routingEventRoleSchema = z.enum(["main", ...modelPolicyRoleSchema.options]);
48
+ var routingEventSchema = z.object({
49
+ at: z.string().datetime({ offset: true }),
50
+ requestId: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/),
51
+ requestedModel: policyModelIdSchema,
52
+ resolvedModel: policyModelIdSchema.optional(),
53
+ reportedModel: policyModelIdSchema.optional(),
54
+ decision: routingDecisionSchema,
55
+ role: routingEventRoleSchema.optional(),
56
+ reason: z.string().regex(/^[a-z][a-z0-9_]{0,63}$/).optional(),
57
+ upstreamStatus: z.number().int().min(100).max(599).optional()
58
+ }).strict();
59
+ var routingEventsSchema = z.array(routingEventSchema).max(1000);
60
+ function canonicalPolicyJSON(value) {
61
+ return JSON.stringify(value, (_key, item) => item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a.localeCompare(b))) : item);
62
+ }
63
+
64
+ // src/domain.ts
65
+ var VERSION = "0.1.3";
66
+ var harnessSchema = z2.enum(["claude", "codex", "grok", "opencode", "opencode2", "pi", "omp", "dsh", "cline", "hermes", "prime-agent", "gemini", "aider", "kilo"]);
67
+ var protocolSchema = z2.enum(["anthropic-messages", "openai-responses", "openai-chat", "gemini-generate-content"]);
68
+ var idSchema = z2.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/);
69
+ var label = z2.string().min(1).max(200);
70
+ var envRef = z2.string().regex(/^SWITCHER_PROVIDER_[A-Z0-9_]+$/);
21
71
  function endpoint(value) {
22
72
  let url;
23
73
  try {
@@ -30,74 +80,79 @@ function endpoint(value) {
30
80
  throw new Fault(400, "invalid_url", "URL must use HTTPS, contain no credentials/query/fragment, or use HTTP on loopback.");
31
81
  return url.href.replace(/\/+$/, "");
32
82
  }
33
- var urlSchema = z.string().max(2000).superRefine((v, ctx) => {
83
+ var urlSchema = z2.string().max(2000).superRefine((v, ctx) => {
34
84
  try {
35
85
  endpoint(v);
36
86
  } catch {
37
87
  ctx.addIssue({ code: "custom", message: "Invalid endpoint URL" });
38
88
  }
39
89
  }).transform(endpoint);
40
- var modelSchema = z.object({
41
- id: z.string().min(1).max(300),
90
+ var modelSchema = z2.object({
91
+ id: z2.string().min(1).max(300),
42
92
  name: label,
43
- description: z.string().max(8000).optional(),
44
- available: z.boolean().optional(),
45
- contextWindow: z.number().int().positive().optional(),
46
- maxOutputTokens: z.number().int().positive().optional(),
47
- inputModalities: z.array(z.string().max(50)).max(20).optional(),
48
- outputModalities: z.array(z.string().max(50)).max(20).optional(),
49
- supportedParameters: z.array(z.string().max(100)).max(100).optional(),
50
- supportedGenerationMethods: z.array(z.string().min(1).max(100)).max(100).optional()
93
+ description: z2.string().max(8000).optional(),
94
+ available: z2.boolean().optional(),
95
+ contextWindow: z2.number().int().positive().optional(),
96
+ maxOutputTokens: z2.number().int().positive().optional(),
97
+ inputModalities: z2.array(z2.string().max(50)).max(20).optional(),
98
+ outputModalities: z2.array(z2.string().max(50)).max(20).optional(),
99
+ supportedParameters: z2.array(z2.string().max(100)).max(100).optional(),
100
+ supportedGenerationMethods: z2.array(z2.string().min(1).max(100)).max(100).optional()
51
101
  }).strict();
52
- var providerInputSchema = z.object({
102
+ var providerInputSchema = z2.object({
53
103
  id: idSchema,
54
104
  name: label,
55
105
  baseUrl: urlSchema,
56
106
  protocol: protocolSchema,
57
107
  credentialEnv: envRef.optional(),
58
- authStyle: z.enum(["bearer", "x-api-key", "api-key"]).default("bearer"),
108
+ authStyle: z2.enum(["bearer", "x-api-key", "api-key"]).default("bearer"),
59
109
  catalogBaseUrl: urlSchema.optional(),
60
- catalogFormat: z.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "gemini", "none"]).optional(),
61
- catalogAuthStyle: z.enum(["bearer", "x-api-key", "api-key", "none"]).optional(),
110
+ catalogFormat: z2.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "gemini", "none"]).optional(),
111
+ catalogAuthStyle: z2.enum(["bearer", "x-api-key", "api-key", "none"]).optional(),
62
112
  catalogCredentialEnv: envRef.optional(),
63
- catalogAccountId: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/).optional(),
64
- modelsPath: z.string().regex(/^[a-zA-Z0-9_/-]+$/).max(200).default("models"),
65
- manualModels: z.array(modelSchema).max(1e4).default([])
113
+ catalogAccountId: z2.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/).optional(),
114
+ modelsPath: z2.string().regex(/^[a-zA-Z0-9_/-]+$/).max(200).default("models"),
115
+ manualModels: z2.array(modelSchema).max(1e4).default([])
66
116
  }).strict().refine((p) => !p.modelsPath.split("/").includes("..") && !p.modelsPath.startsWith("/"), "modelsPath must be relative");
67
- var providerPresetSchema = z.object({
117
+ var providerPresetSchema = z2.object({
68
118
  id: idSchema,
69
119
  name: label,
70
120
  credentialEnv: envRef.optional(),
71
- credentialAliases: z.array(z.string().regex(/^[A-Z][A-Z0-9_]+$/)),
72
- protocols: z.array(z.object({
121
+ credentialAliases: z2.array(z2.string().regex(/^[A-Z][A-Z0-9_]+$/)),
122
+ protocols: z2.array(z2.object({
73
123
  protocol: protocolSchema,
74
124
  baseUrl: urlSchema.optional(),
75
- authStyle: z.enum(["bearer", "x-api-key", "api-key"]),
125
+ authStyle: z2.enum(["bearer", "x-api-key", "api-key"]),
76
126
  catalogBaseUrl: urlSchema.optional(),
77
- catalogFormat: z.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "gemini", "none"]),
78
- catalogAuthStyle: z.enum(["bearer", "x-api-key", "api-key", "none"]).optional(),
79
- modelsPath: z.string(),
80
- notes: z.array(z.string())
127
+ catalogFormat: z2.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "gemini", "none"]),
128
+ catalogAuthStyle: z2.enum(["bearer", "x-api-key", "api-key", "none"]).optional(),
129
+ modelsPath: z2.string(),
130
+ notes: z2.array(z2.string())
81
131
  }).strict()).min(1),
82
- sources: z.array(z.string().url()),
83
- verification: z.literal("documented")
132
+ sources: z2.array(z2.string().url()),
133
+ verification: z2.literal("documented")
84
134
  }).strict();
85
- var profileInputSchema = z.object({
135
+ var profileInputSchema = z2.object({
86
136
  id: idSchema,
87
137
  name: label,
88
138
  providerId: idSchema,
89
139
  harness: harnessSchema,
90
- model: z.string().min(1).max(300)
140
+ model: z2.string().min(1).max(300),
141
+ modelPolicy: modelPolicySchema.optional()
91
142
  }).strict();
92
- var runInputSchema = z.object({
143
+ var runInputSchema = z2.object({
144
+ modelPolicyVersion: z2.literal(1),
93
145
  profileId: idSchema,
94
146
  harness: harnessSchema,
95
- model: z.string().min(1).max(300),
96
- planToken: z.string().regex(/^[a-f0-9]{64}$/)
147
+ model: z2.string().min(1).max(300),
148
+ modelPolicy: modelPolicySchema.optional(),
149
+ planToken: z2.string().regex(/^[a-f0-9]{64}$/)
97
150
  }).strict();
98
- var runUpdateSchema = z.object({
99
- status: z.enum(["exited", "failed", "interrupted"]),
100
- exitCode: z.number().int().min(0).max(255)
151
+ var runUpdateSchema = z2.object({
152
+ status: z2.enum(["exited", "failed", "interrupted"]),
153
+ exitCode: z2.number().int().min(0).max(255),
154
+ routingEvents: routingEventsSchema.optional(),
155
+ routingEventsDropped: z2.number().int().min(0).max(1e6).optional()
101
156
  }).strict();
102
157
 
103
158
  class Fault extends Error {
@@ -298,8 +353,8 @@ class Store {
298
353
  }
299
354
 
300
355
  // src/service.ts
301
- import { createHash, timingSafeEqual } from "crypto";
302
- import { z as z2 } from "zod";
356
+ import { createHash as createHash2, timingSafeEqual } from "crypto";
357
+ import { z as z3 } from "zod";
303
358
 
304
359
  // src/http.ts
305
360
  var MAX_BYTES = 16 * 1024 * 1024;
@@ -595,6 +650,69 @@ async function discover(provider, env = process.env, resolveCredential) {
595
650
  throw new Fault(502, "catalog_too_large", "Provider catalog pagination exceeded 100 pages.");
596
651
  }
597
652
 
653
+ // src/model-policy.ts
654
+ import { createHash } from "crypto";
655
+ var roles = ["subagent", "fast", "planning", "review", "summary", "compaction", "weak", "editor"];
656
+ var id = (v, label2) => {
657
+ if (typeof v !== "string" || !v || v.length > 300 || /[\u0000-\u001f\u007f]/.test(v))
658
+ throw new Fault(400, "invalid_model_policy", `${label2} is invalid.`);
659
+ return v;
660
+ };
661
+ var uniq = (xs) => [...new Set(xs)];
662
+ function stable(value) {
663
+ return JSON.stringify(value, (_k, v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).sort(([a], [b]) => a.localeCompare(b))) : v);
664
+ }
665
+ function compileModelPolicy(model, catalog, policy) {
666
+ const selected = id(model, "model");
667
+ const available = new Set(catalog.filter((m) => m.available !== false).map((m) => m.id));
668
+ if (!available.has(selected))
669
+ throw new Fault(422, "model_unavailable", "Selected model is not present in the eligible catalog.");
670
+ if (policy && policy.version !== undefined && policy.version !== 1)
671
+ throw new Fault(400, "invalid_model_policy", "Unsupported model policy version.");
672
+ const p = parse(modelPolicySchema, policy ?? {});
673
+ const roleInput = p.roles ?? {};
674
+ if (Object.keys(roleInput).some((k) => !roles.includes(k)))
675
+ throw new Fault(400, "invalid_model_policy", "Unknown model policy role.");
676
+ const compiledRoles = Object.fromEntries(roles.map((role) => [role, id(roleInput[role] ?? selected, `roles.${role}`)]));
677
+ for (const value of Object.values(compiledRoles))
678
+ if (!available.has(value))
679
+ throw new Fault(422, "model_unavailable", "A model policy role is not present in the eligible catalog.");
680
+ const explicitAllowed = (p.allowedModels ?? []).map((v, i) => id(v, `allowedModels[${i}]`));
681
+ if (explicitAllowed.length > 500)
682
+ throw new Fault(400, "invalid_model_policy", "Model policy allowedModels is too large.");
683
+ for (const value of explicitAllowed)
684
+ if (!available.has(value))
685
+ throw new Fault(422, "model_unavailable", "An allowed model is not present in the eligible catalog.");
686
+ const aliases = Object.create(null);
687
+ for (const [name, target] of Object.entries(p.aliases ?? {})) {
688
+ if (Object.keys(aliases).length >= 200 || !/^[A-Za-z0-9._/-]{1,120}$/.test(name) || ["__proto__", "prototype", "constructor"].includes(name))
689
+ throw new Fault(400, "invalid_model_policy", "A model alias is invalid or too numerous.");
690
+ const canonical = id(target, `aliases.${name}`);
691
+ if (available.has(name) && name !== canonical)
692
+ throw new Fault(400, "invalid_model_policy", "A model alias cannot shadow a real model ID.");
693
+ if (!available.has(canonical))
694
+ throw new Fault(422, "model_unavailable", "A model alias target is not present in the eligible catalog.");
695
+ aliases[name] = canonical;
696
+ }
697
+ const fallbacks = Object.create(null);
698
+ const allowedSources = new Set([selected, ...Object.values(compiledRoles), ...explicitAllowed]);
699
+ for (const [source, targets] of Object.entries(p.fallbacks ?? {})) {
700
+ if (!allowedSources.has(source) || !available.has(source) || !Array.isArray(targets) || Object.keys(fallbacks).length >= 200 || targets.length > 20)
701
+ throw new Fault(400, "invalid_model_policy", "Fallback source or list is invalid.");
702
+ const values = targets.map((v, i) => id(v, `fallbacks.${source}[${i}]`));
703
+ if (values.some((v) => !available.has(v)))
704
+ throw new Fault(422, "model_unavailable", "A fallback model is not present in the eligible catalog.");
705
+ if (values.includes(source))
706
+ throw new Fault(400, "invalid_model_policy", "A model cannot fall back to itself.");
707
+ fallbacks[source] = uniq(values);
708
+ }
709
+ const allowedModels = uniq([selected, ...Object.values(compiledRoles), ...explicitAllowed, ...Object.values(fallbacks).flat()]).sort();
710
+ if (Object.values(aliases).some((value) => !allowedModels.includes(value)))
711
+ throw new Fault(422, "model_not_allowed", "A model alias target must be explicitly allowed by the launch policy.");
712
+ const result = { version: 1, model: selected, roles: compiledRoles, allowedModels, aliases, fallbacks };
713
+ return { ...result, digest: createHash("sha256").update(stable(result)).digest("hex") };
714
+ }
715
+
598
716
  // src/presets.ts
599
717
  var route = (protocol, baseUrl, options = {}) => ({
600
718
  protocol,
@@ -605,13 +723,13 @@ var route = (protocol, baseUrl, options = {}) => ({
605
723
  notes: [],
606
724
  ...options
607
725
  });
608
- var preset = (id, name, protocols, sources, alias) => parse(providerPresetSchema, {
609
- id,
726
+ var preset = (id2, name, protocols, sources, alias) => parse(providerPresetSchema, {
727
+ id: id2,
610
728
  name,
611
729
  protocols,
612
730
  sources,
613
731
  credentialAliases: alias ? [alias] : [],
614
- credentialEnv: alias ? `SWITCHER_PROVIDER_${id.toUpperCase().replace(/-/g, "_")}` : undefined,
732
+ credentialEnv: alias ? `SWITCHER_PROVIDER_${id2.toUpperCase().replace(/-/g, "_")}` : undefined,
615
733
  verification: "documented"
616
734
  });
617
735
  var providerPresets = [
@@ -679,8 +797,8 @@ var providerPresets = [
679
797
  preset("siliconflow", "SiliconFlow", [route("openai-chat", "https://api.siliconflow.cn/v1", { catalogBaseUrl: "https://api.siliconflow.cn/v1", catalogFormat: "openai", notes: ["The official SiliconCloud OpenAPI contract defines GET /models with Bearer auth and data[] model rows; optional type and sub_type filters are available at the upstream endpoint."] })], ["https://github.com/siliconflow/siliconcloud/blob/main/openapi.yaml", "https://docs.siliconflow.cn/docs/userguide/quickstart", "https://docs.siliconflow.cn/docs/api/chat-completions-post"], "SILICONFLOW_API_KEY"),
680
798
  ...["anthropic-messages", "openai-responses", "openai-chat"].map((protocol) => preset(`generic-${protocol}`, `Custom ${protocol}`, [route(protocol)], []))
681
799
  ];
682
- function getProviderPreset(id) {
683
- const entry = providerPresets.find((p) => p.id === id);
800
+ function getProviderPreset(id2) {
801
+ const entry = providerPresets.find((p) => p.id === id2);
684
802
  if (!entry)
685
803
  throw new Fault(404, "preset_not_found", "Unknown provider preset. Use switcher providers presets to list available presets.");
686
804
  return structuredClone(entry);
@@ -690,7 +808,7 @@ var openapi_default = {
690
808
  openapi: "3.0.3",
691
809
  info: {
692
810
  title: "Switcher API",
693
- version: "0.1.2",
811
+ version: "0.1.3",
694
812
  description: "Authenticated provider/profile/catalog control plane. Launches run locally; the API never returns provider credentials."
695
813
  },
696
814
  security: [
@@ -2364,6 +2482,105 @@ var openapi_default = {
2364
2482
  type: "string",
2365
2483
  minLength: 1,
2366
2484
  maxLength: 300
2485
+ },
2486
+ modelPolicy: {
2487
+ type: "object",
2488
+ properties: {
2489
+ version: {
2490
+ type: "number",
2491
+ enum: [
2492
+ 1
2493
+ ],
2494
+ default: 1
2495
+ },
2496
+ roles: {
2497
+ type: "object",
2498
+ properties: {
2499
+ subagent: {
2500
+ type: "string",
2501
+ minLength: 1,
2502
+ maxLength: 300,
2503
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2504
+ },
2505
+ fast: {
2506
+ type: "string",
2507
+ minLength: 1,
2508
+ maxLength: 300,
2509
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2510
+ },
2511
+ planning: {
2512
+ type: "string",
2513
+ minLength: 1,
2514
+ maxLength: 300,
2515
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2516
+ },
2517
+ review: {
2518
+ type: "string",
2519
+ minLength: 1,
2520
+ maxLength: 300,
2521
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2522
+ },
2523
+ summary: {
2524
+ type: "string",
2525
+ minLength: 1,
2526
+ maxLength: 300,
2527
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2528
+ },
2529
+ compaction: {
2530
+ type: "string",
2531
+ minLength: 1,
2532
+ maxLength: 300,
2533
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2534
+ },
2535
+ weak: {
2536
+ type: "string",
2537
+ minLength: 1,
2538
+ maxLength: 300,
2539
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2540
+ },
2541
+ editor: {
2542
+ type: "string",
2543
+ minLength: 1,
2544
+ maxLength: 300,
2545
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2546
+ }
2547
+ },
2548
+ additionalProperties: false
2549
+ },
2550
+ allowedModels: {
2551
+ type: "array",
2552
+ items: {
2553
+ type: "string",
2554
+ minLength: 1,
2555
+ maxLength: 300,
2556
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2557
+ },
2558
+ maxItems: 500
2559
+ },
2560
+ aliases: {
2561
+ type: "object",
2562
+ additionalProperties: {
2563
+ type: "string",
2564
+ minLength: 1,
2565
+ maxLength: 300,
2566
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2567
+ }
2568
+ },
2569
+ fallbacks: {
2570
+ type: "object",
2571
+ additionalProperties: {
2572
+ type: "array",
2573
+ items: {
2574
+ type: "string",
2575
+ minLength: 1,
2576
+ maxLength: 300,
2577
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2578
+ },
2579
+ maxItems: 20
2580
+ }
2581
+ }
2582
+ },
2583
+ additionalProperties: false
2367
2584
  }
2368
2585
  },
2369
2586
  required: [
@@ -2415,6 +2632,105 @@ var openapi_default = {
2415
2632
  minLength: 1,
2416
2633
  maxLength: 300
2417
2634
  },
2635
+ modelPolicy: {
2636
+ type: "object",
2637
+ properties: {
2638
+ version: {
2639
+ type: "number",
2640
+ enum: [
2641
+ 1
2642
+ ],
2643
+ default: 1
2644
+ },
2645
+ roles: {
2646
+ type: "object",
2647
+ properties: {
2648
+ subagent: {
2649
+ type: "string",
2650
+ minLength: 1,
2651
+ maxLength: 300,
2652
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2653
+ },
2654
+ fast: {
2655
+ type: "string",
2656
+ minLength: 1,
2657
+ maxLength: 300,
2658
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2659
+ },
2660
+ planning: {
2661
+ type: "string",
2662
+ minLength: 1,
2663
+ maxLength: 300,
2664
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2665
+ },
2666
+ review: {
2667
+ type: "string",
2668
+ minLength: 1,
2669
+ maxLength: 300,
2670
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2671
+ },
2672
+ summary: {
2673
+ type: "string",
2674
+ minLength: 1,
2675
+ maxLength: 300,
2676
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2677
+ },
2678
+ compaction: {
2679
+ type: "string",
2680
+ minLength: 1,
2681
+ maxLength: 300,
2682
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2683
+ },
2684
+ weak: {
2685
+ type: "string",
2686
+ minLength: 1,
2687
+ maxLength: 300,
2688
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2689
+ },
2690
+ editor: {
2691
+ type: "string",
2692
+ minLength: 1,
2693
+ maxLength: 300,
2694
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2695
+ }
2696
+ },
2697
+ additionalProperties: false
2698
+ },
2699
+ allowedModels: {
2700
+ type: "array",
2701
+ items: {
2702
+ type: "string",
2703
+ minLength: 1,
2704
+ maxLength: 300,
2705
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2706
+ },
2707
+ maxItems: 500
2708
+ },
2709
+ aliases: {
2710
+ type: "object",
2711
+ additionalProperties: {
2712
+ type: "string",
2713
+ minLength: 1,
2714
+ maxLength: 300,
2715
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2716
+ }
2717
+ },
2718
+ fallbacks: {
2719
+ type: "object",
2720
+ additionalProperties: {
2721
+ type: "array",
2722
+ items: {
2723
+ type: "string",
2724
+ minLength: 1,
2725
+ maxLength: 300,
2726
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2727
+ },
2728
+ maxItems: 20
2729
+ }
2730
+ }
2731
+ },
2732
+ additionalProperties: false
2733
+ },
2418
2734
  version: {
2419
2735
  type: "integer",
2420
2736
  exclusiveMinimum: true,
@@ -2505,6 +2821,175 @@ var openapi_default = {
2505
2821
  ],
2506
2822
  additionalProperties: false
2507
2823
  },
2824
+ ModelPolicy: {
2825
+ type: "object",
2826
+ properties: {
2827
+ version: {
2828
+ type: "number",
2829
+ enum: [
2830
+ 1
2831
+ ],
2832
+ default: 1
2833
+ },
2834
+ roles: {
2835
+ type: "object",
2836
+ properties: {
2837
+ subagent: {
2838
+ type: "string",
2839
+ minLength: 1,
2840
+ maxLength: 300,
2841
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2842
+ },
2843
+ fast: {
2844
+ type: "string",
2845
+ minLength: 1,
2846
+ maxLength: 300,
2847
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2848
+ },
2849
+ planning: {
2850
+ type: "string",
2851
+ minLength: 1,
2852
+ maxLength: 300,
2853
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2854
+ },
2855
+ review: {
2856
+ type: "string",
2857
+ minLength: 1,
2858
+ maxLength: 300,
2859
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2860
+ },
2861
+ summary: {
2862
+ type: "string",
2863
+ minLength: 1,
2864
+ maxLength: 300,
2865
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2866
+ },
2867
+ compaction: {
2868
+ type: "string",
2869
+ minLength: 1,
2870
+ maxLength: 300,
2871
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2872
+ },
2873
+ weak: {
2874
+ type: "string",
2875
+ minLength: 1,
2876
+ maxLength: 300,
2877
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2878
+ },
2879
+ editor: {
2880
+ type: "string",
2881
+ minLength: 1,
2882
+ maxLength: 300,
2883
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2884
+ }
2885
+ },
2886
+ additionalProperties: false
2887
+ },
2888
+ allowedModels: {
2889
+ type: "array",
2890
+ items: {
2891
+ type: "string",
2892
+ minLength: 1,
2893
+ maxLength: 300,
2894
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2895
+ },
2896
+ maxItems: 500
2897
+ },
2898
+ aliases: {
2899
+ type: "object",
2900
+ additionalProperties: {
2901
+ type: "string",
2902
+ minLength: 1,
2903
+ maxLength: 300,
2904
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2905
+ }
2906
+ },
2907
+ fallbacks: {
2908
+ type: "object",
2909
+ additionalProperties: {
2910
+ type: "array",
2911
+ items: {
2912
+ type: "string",
2913
+ minLength: 1,
2914
+ maxLength: 300,
2915
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2916
+ },
2917
+ maxItems: 20
2918
+ }
2919
+ }
2920
+ },
2921
+ additionalProperties: false
2922
+ },
2923
+ RoutingEvent: {
2924
+ type: "object",
2925
+ properties: {
2926
+ at: {
2927
+ type: "string",
2928
+ format: "date-time"
2929
+ },
2930
+ requestId: {
2931
+ type: "string",
2932
+ pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
2933
+ },
2934
+ requestedModel: {
2935
+ type: "string",
2936
+ minLength: 1,
2937
+ maxLength: 300,
2938
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2939
+ },
2940
+ resolvedModel: {
2941
+ type: "string",
2942
+ minLength: 1,
2943
+ maxLength: 300,
2944
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2945
+ },
2946
+ reportedModel: {
2947
+ type: "string",
2948
+ minLength: 1,
2949
+ maxLength: 300,
2950
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
2951
+ },
2952
+ decision: {
2953
+ type: "string",
2954
+ enum: [
2955
+ "allow",
2956
+ "alias",
2957
+ "reject",
2958
+ "fallback"
2959
+ ]
2960
+ },
2961
+ role: {
2962
+ type: "string",
2963
+ enum: [
2964
+ "main",
2965
+ "subagent",
2966
+ "fast",
2967
+ "planning",
2968
+ "review",
2969
+ "summary",
2970
+ "compaction",
2971
+ "weak",
2972
+ "editor"
2973
+ ]
2974
+ },
2975
+ reason: {
2976
+ type: "string",
2977
+ pattern: "^[a-z][a-z0-9_]{0,63}$"
2978
+ },
2979
+ upstreamStatus: {
2980
+ type: "integer",
2981
+ minimum: 100,
2982
+ maximum: 599
2983
+ }
2984
+ },
2985
+ required: [
2986
+ "at",
2987
+ "requestId",
2988
+ "requestedModel",
2989
+ "decision"
2990
+ ],
2991
+ additionalProperties: false
2992
+ },
2508
2993
  ModelPage: {
2509
2994
  type: "object",
2510
2995
  properties: {
@@ -2924,6 +3409,105 @@ var openapi_default = {
2924
3409
  minLength: 1,
2925
3410
  maxLength: 300
2926
3411
  },
3412
+ modelPolicy: {
3413
+ type: "object",
3414
+ properties: {
3415
+ version: {
3416
+ type: "number",
3417
+ enum: [
3418
+ 1
3419
+ ],
3420
+ default: 1
3421
+ },
3422
+ roles: {
3423
+ type: "object",
3424
+ properties: {
3425
+ subagent: {
3426
+ type: "string",
3427
+ minLength: 1,
3428
+ maxLength: 300,
3429
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3430
+ },
3431
+ fast: {
3432
+ type: "string",
3433
+ minLength: 1,
3434
+ maxLength: 300,
3435
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3436
+ },
3437
+ planning: {
3438
+ type: "string",
3439
+ minLength: 1,
3440
+ maxLength: 300,
3441
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3442
+ },
3443
+ review: {
3444
+ type: "string",
3445
+ minLength: 1,
3446
+ maxLength: 300,
3447
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3448
+ },
3449
+ summary: {
3450
+ type: "string",
3451
+ minLength: 1,
3452
+ maxLength: 300,
3453
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3454
+ },
3455
+ compaction: {
3456
+ type: "string",
3457
+ minLength: 1,
3458
+ maxLength: 300,
3459
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3460
+ },
3461
+ weak: {
3462
+ type: "string",
3463
+ minLength: 1,
3464
+ maxLength: 300,
3465
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3466
+ },
3467
+ editor: {
3468
+ type: "string",
3469
+ minLength: 1,
3470
+ maxLength: 300,
3471
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3472
+ }
3473
+ },
3474
+ additionalProperties: false
3475
+ },
3476
+ allowedModels: {
3477
+ type: "array",
3478
+ items: {
3479
+ type: "string",
3480
+ minLength: 1,
3481
+ maxLength: 300,
3482
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3483
+ },
3484
+ maxItems: 500
3485
+ },
3486
+ aliases: {
3487
+ type: "object",
3488
+ additionalProperties: {
3489
+ type: "string",
3490
+ minLength: 1,
3491
+ maxLength: 300,
3492
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3493
+ }
3494
+ },
3495
+ fallbacks: {
3496
+ type: "object",
3497
+ additionalProperties: {
3498
+ type: "array",
3499
+ items: {
3500
+ type: "string",
3501
+ minLength: 1,
3502
+ maxLength: 300,
3503
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3504
+ },
3505
+ maxItems: 20
3506
+ }
3507
+ }
3508
+ },
3509
+ additionalProperties: false
3510
+ },
2927
3511
  version: {
2928
3512
  type: "integer",
2929
3513
  exclusiveMinimum: true,
@@ -3060,6 +3644,12 @@ var openapi_default = {
3060
3644
  RunInput: {
3061
3645
  type: "object",
3062
3646
  properties: {
3647
+ modelPolicyVersion: {
3648
+ type: "number",
3649
+ enum: [
3650
+ 1
3651
+ ]
3652
+ },
3063
3653
  profileId: {
3064
3654
  type: "string",
3065
3655
  pattern: "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$"
@@ -3088,12 +3678,112 @@ var openapi_default = {
3088
3678
  minLength: 1,
3089
3679
  maxLength: 300
3090
3680
  },
3681
+ modelPolicy: {
3682
+ type: "object",
3683
+ properties: {
3684
+ version: {
3685
+ type: "number",
3686
+ enum: [
3687
+ 1
3688
+ ],
3689
+ default: 1
3690
+ },
3691
+ roles: {
3692
+ type: "object",
3693
+ properties: {
3694
+ subagent: {
3695
+ type: "string",
3696
+ minLength: 1,
3697
+ maxLength: 300,
3698
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3699
+ },
3700
+ fast: {
3701
+ type: "string",
3702
+ minLength: 1,
3703
+ maxLength: 300,
3704
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3705
+ },
3706
+ planning: {
3707
+ type: "string",
3708
+ minLength: 1,
3709
+ maxLength: 300,
3710
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3711
+ },
3712
+ review: {
3713
+ type: "string",
3714
+ minLength: 1,
3715
+ maxLength: 300,
3716
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3717
+ },
3718
+ summary: {
3719
+ type: "string",
3720
+ minLength: 1,
3721
+ maxLength: 300,
3722
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3723
+ },
3724
+ compaction: {
3725
+ type: "string",
3726
+ minLength: 1,
3727
+ maxLength: 300,
3728
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3729
+ },
3730
+ weak: {
3731
+ type: "string",
3732
+ minLength: 1,
3733
+ maxLength: 300,
3734
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3735
+ },
3736
+ editor: {
3737
+ type: "string",
3738
+ minLength: 1,
3739
+ maxLength: 300,
3740
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3741
+ }
3742
+ },
3743
+ additionalProperties: false
3744
+ },
3745
+ allowedModels: {
3746
+ type: "array",
3747
+ items: {
3748
+ type: "string",
3749
+ minLength: 1,
3750
+ maxLength: 300,
3751
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3752
+ },
3753
+ maxItems: 500
3754
+ },
3755
+ aliases: {
3756
+ type: "object",
3757
+ additionalProperties: {
3758
+ type: "string",
3759
+ minLength: 1,
3760
+ maxLength: 300,
3761
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3762
+ }
3763
+ },
3764
+ fallbacks: {
3765
+ type: "object",
3766
+ additionalProperties: {
3767
+ type: "array",
3768
+ items: {
3769
+ type: "string",
3770
+ minLength: 1,
3771
+ maxLength: 300,
3772
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3773
+ },
3774
+ maxItems: 20
3775
+ }
3776
+ }
3777
+ },
3778
+ additionalProperties: false
3779
+ },
3091
3780
  planToken: {
3092
3781
  type: "string",
3093
3782
  pattern: "^[a-f0-9]{64}$"
3094
3783
  }
3095
3784
  },
3096
3785
  required: [
3786
+ "modelPolicyVersion",
3097
3787
  "profileId",
3098
3788
  "harness",
3099
3789
  "model",
@@ -3116,6 +3806,85 @@ var openapi_default = {
3116
3806
  type: "integer",
3117
3807
  minimum: 0,
3118
3808
  maximum: 255
3809
+ },
3810
+ routingEvents: {
3811
+ type: "array",
3812
+ items: {
3813
+ type: "object",
3814
+ properties: {
3815
+ at: {
3816
+ type: "string",
3817
+ format: "date-time"
3818
+ },
3819
+ requestId: {
3820
+ type: "string",
3821
+ pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
3822
+ },
3823
+ requestedModel: {
3824
+ type: "string",
3825
+ minLength: 1,
3826
+ maxLength: 300,
3827
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3828
+ },
3829
+ resolvedModel: {
3830
+ type: "string",
3831
+ minLength: 1,
3832
+ maxLength: 300,
3833
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3834
+ },
3835
+ reportedModel: {
3836
+ type: "string",
3837
+ minLength: 1,
3838
+ maxLength: 300,
3839
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3840
+ },
3841
+ decision: {
3842
+ type: "string",
3843
+ enum: [
3844
+ "allow",
3845
+ "alias",
3846
+ "reject",
3847
+ "fallback"
3848
+ ]
3849
+ },
3850
+ role: {
3851
+ type: "string",
3852
+ enum: [
3853
+ "main",
3854
+ "subagent",
3855
+ "fast",
3856
+ "planning",
3857
+ "review",
3858
+ "summary",
3859
+ "compaction",
3860
+ "weak",
3861
+ "editor"
3862
+ ]
3863
+ },
3864
+ reason: {
3865
+ type: "string",
3866
+ pattern: "^[a-z][a-z0-9_]{0,63}$"
3867
+ },
3868
+ upstreamStatus: {
3869
+ type: "integer",
3870
+ minimum: 100,
3871
+ maximum: 599
3872
+ }
3873
+ },
3874
+ required: [
3875
+ "at",
3876
+ "requestId",
3877
+ "requestedModel",
3878
+ "decision"
3879
+ ],
3880
+ additionalProperties: false
3881
+ },
3882
+ maxItems: 1000
3883
+ },
3884
+ routingEventsDropped: {
3885
+ type: "integer",
3886
+ minimum: 0,
3887
+ maximum: 1e6
3119
3888
  }
3120
3889
  },
3121
3890
  required: [
@@ -3127,6 +3896,12 @@ var openapi_default = {
3127
3896
  Run: {
3128
3897
  type: "object",
3129
3898
  properties: {
3899
+ modelPolicyVersion: {
3900
+ type: "number",
3901
+ enum: [
3902
+ 1
3903
+ ]
3904
+ },
3130
3905
  profileId: {
3131
3906
  type: "string",
3132
3907
  pattern: "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$"
@@ -3155,6 +3930,105 @@ var openapi_default = {
3155
3930
  minLength: 1,
3156
3931
  maxLength: 300
3157
3932
  },
3933
+ modelPolicy: {
3934
+ type: "object",
3935
+ properties: {
3936
+ version: {
3937
+ type: "number",
3938
+ enum: [
3939
+ 1
3940
+ ],
3941
+ default: 1
3942
+ },
3943
+ roles: {
3944
+ type: "object",
3945
+ properties: {
3946
+ subagent: {
3947
+ type: "string",
3948
+ minLength: 1,
3949
+ maxLength: 300,
3950
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3951
+ },
3952
+ fast: {
3953
+ type: "string",
3954
+ minLength: 1,
3955
+ maxLength: 300,
3956
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3957
+ },
3958
+ planning: {
3959
+ type: "string",
3960
+ minLength: 1,
3961
+ maxLength: 300,
3962
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3963
+ },
3964
+ review: {
3965
+ type: "string",
3966
+ minLength: 1,
3967
+ maxLength: 300,
3968
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3969
+ },
3970
+ summary: {
3971
+ type: "string",
3972
+ minLength: 1,
3973
+ maxLength: 300,
3974
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3975
+ },
3976
+ compaction: {
3977
+ type: "string",
3978
+ minLength: 1,
3979
+ maxLength: 300,
3980
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3981
+ },
3982
+ weak: {
3983
+ type: "string",
3984
+ minLength: 1,
3985
+ maxLength: 300,
3986
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3987
+ },
3988
+ editor: {
3989
+ type: "string",
3990
+ minLength: 1,
3991
+ maxLength: 300,
3992
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
3993
+ }
3994
+ },
3995
+ additionalProperties: false
3996
+ },
3997
+ allowedModels: {
3998
+ type: "array",
3999
+ items: {
4000
+ type: "string",
4001
+ minLength: 1,
4002
+ maxLength: 300,
4003
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
4004
+ },
4005
+ maxItems: 500
4006
+ },
4007
+ aliases: {
4008
+ type: "object",
4009
+ additionalProperties: {
4010
+ type: "string",
4011
+ minLength: 1,
4012
+ maxLength: 300,
4013
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
4014
+ }
4015
+ },
4016
+ fallbacks: {
4017
+ type: "object",
4018
+ additionalProperties: {
4019
+ type: "array",
4020
+ items: {
4021
+ type: "string",
4022
+ minLength: 1,
4023
+ maxLength: 300,
4024
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
4025
+ },
4026
+ maxItems: 20
4027
+ }
4028
+ }
4029
+ },
4030
+ additionalProperties: false
4031
+ },
3158
4032
  planToken: {
3159
4033
  type: "string",
3160
4034
  pattern: "^[a-f0-9]{64}$"
@@ -3202,6 +4076,85 @@ var openapi_default = {
3202
4076
  },
3203
4077
  exitCode: {
3204
4078
  type: "integer"
4079
+ },
4080
+ routingEvents: {
4081
+ type: "array",
4082
+ items: {
4083
+ type: "object",
4084
+ properties: {
4085
+ at: {
4086
+ type: "string",
4087
+ format: "date-time"
4088
+ },
4089
+ requestId: {
4090
+ type: "string",
4091
+ pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
4092
+ },
4093
+ requestedModel: {
4094
+ type: "string",
4095
+ minLength: 1,
4096
+ maxLength: 300,
4097
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
4098
+ },
4099
+ resolvedModel: {
4100
+ type: "string",
4101
+ minLength: 1,
4102
+ maxLength: 300,
4103
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
4104
+ },
4105
+ reportedModel: {
4106
+ type: "string",
4107
+ minLength: 1,
4108
+ maxLength: 300,
4109
+ pattern: "^[^\\u0000-\\u001f\\u007f]+$"
4110
+ },
4111
+ decision: {
4112
+ type: "string",
4113
+ enum: [
4114
+ "allow",
4115
+ "alias",
4116
+ "reject",
4117
+ "fallback"
4118
+ ]
4119
+ },
4120
+ role: {
4121
+ type: "string",
4122
+ enum: [
4123
+ "main",
4124
+ "subagent",
4125
+ "fast",
4126
+ "planning",
4127
+ "review",
4128
+ "summary",
4129
+ "compaction",
4130
+ "weak",
4131
+ "editor"
4132
+ ]
4133
+ },
4134
+ reason: {
4135
+ type: "string",
4136
+ pattern: "^[a-z][a-z0-9_]{0,63}$"
4137
+ },
4138
+ upstreamStatus: {
4139
+ type: "integer",
4140
+ minimum: 100,
4141
+ maximum: 599
4142
+ }
4143
+ },
4144
+ required: [
4145
+ "at",
4146
+ "requestId",
4147
+ "requestedModel",
4148
+ "decision"
4149
+ ],
4150
+ additionalProperties: false
4151
+ },
4152
+ maxItems: 1000
4153
+ },
4154
+ routingEventsDropped: {
4155
+ type: "integer",
4156
+ minimum: 0,
4157
+ maximum: 1e6
3205
4158
  }
3206
4159
  },
3207
4160
  required: [
@@ -3328,8 +4281,8 @@ var openapi_default = {
3328
4281
  };
3329
4282
 
3330
4283
  // src/service.ts
3331
- var snapshot = (profile, provider, catalog) => createHash("sha256").update(JSON.stringify([profile, provider, { models: catalog.models, source: catalog.source }])).digest("hex");
3332
- var hash = (s) => createHash("sha256").update(s).digest();
4284
+ var snapshot = (profile, provider, catalog) => createHash2("sha256").update(JSON.stringify([profile, provider, { models: catalog.models, source: catalog.source }])).digest("hex");
4285
+ var hash = (s) => createHash2("sha256").update(s).digest();
3333
4286
  function createHandler(store, apiKey, providerEnv = process.env, resolveCredential) {
3334
4287
  if (!apiKey || apiKey.length < 24)
3335
4288
  throw new Fault(500, "auth_config", "Set HASNA_SWITCHER_API_KEY to a random token of at least 24 characters.");
@@ -3360,23 +4313,23 @@ function createHandler(store, apiKey, providerEnv = process.env, resolveCredenti
3360
4313
  if (parts[0] !== "v1")
3361
4314
  throw new Fault(404, "not_found", "Route was not found.");
3362
4315
  const resource = parts[1];
3363
- const id = parts[2];
3364
- if (id)
3365
- parse(idSchema, id);
3366
- const page = () => parse(z2.object({
3367
- limit: z2.coerce.number().int().min(1).max(1000).default(100),
3368
- offset: z2.coerce.number().int().min(0).max(1e6).default(0),
3369
- search: z2.string().max(200).default("")
4316
+ const id2 = parts[2];
4317
+ if (id2)
4318
+ parse(idSchema, id2);
4319
+ const page = () => parse(z3.object({
4320
+ limit: z3.coerce.number().int().min(1).max(1000).default(100),
4321
+ offset: z3.coerce.number().int().min(0).max(1e6).default(0),
4322
+ search: z3.string().max(200).default("")
3370
4323
  }).strict(), Object.fromEntries(url.searchParams));
3371
4324
  if (request.method === "GET") {
3372
4325
  if (resource === "provider-presets" && parts.length <= 3)
3373
- return json(id ? getProviderPreset(id) : { data: providerPresets });
4326
+ return json(id2 ? getProviderPreset(id2) : { data: providerPresets });
3374
4327
  if (["providers", "profiles", "runs"].includes(resource) && parts.length <= 3) {
3375
4328
  const kind = resource;
3376
- return json(id ? await store.get(kind, id) : await store.list(kind, page()));
4329
+ return json(id2 ? await store.get(kind, id2) : await store.list(kind, page()));
3377
4330
  }
3378
- if (resource === "providers" && id && parts[3] === "models" && parts.length === 4) {
3379
- const catalog = await store.get("catalogs", id);
4331
+ if (resource === "providers" && id2 && parts[3] === "models" && parts.length === 4) {
4332
+ const catalog = await store.get("catalogs", id2);
3380
4333
  const p = page();
3381
4334
  const filtered = catalog.models.filter((m) => [m.id, m.name].some((s) => s.toLowerCase().includes(p.search.toLowerCase())));
3382
4335
  return json({ ...catalog, models: undefined, data: filtered.slice(p.offset, p.offset + p.limit).map((m) => ({ ...m, codingEligible: codingEligible(m) })), total: filtered.length, ...p });
@@ -3407,50 +4360,50 @@ function createHandler(store, apiKey, providerEnv = process.env, resolveCredenti
3407
4360
  };
3408
4361
  const replay = await store.replay(key, fingerprint);
3409
4362
  if (replay.found)
3410
- return json(replay.value, request.method === "POST" && ["providers", "profiles", "runs"].includes(resource) && !id ? 201 : 200);
4363
+ return json(replay.value, request.method === "POST" && ["providers", "profiles", "runs"].includes(resource) && !id2 ? 201 : 200);
3411
4364
  let refreshed;
3412
- if (resource === "providers" && id && parts[3] === "refresh" && parts.length === 4 && request.method === "POST") {
3413
- parse(z2.object({}).strict(), body);
3414
- const provider = await store.get("providers", id);
4365
+ if (resource === "providers" && id2 && parts[3] === "refresh" && parts.length === 4 && request.method === "POST") {
4366
+ parse(z3.object({}).strict(), body);
4367
+ const provider = await store.get("providers", id2);
3415
4368
  refreshed = { provider, catalog: await discover(provider, providerEnv, resolveCredential) };
3416
4369
  }
3417
4370
  const result = await store.mutate(key, fingerprint, async (db) => {
3418
4371
  if ((resource === "providers" || resource === "profiles") && parts.length <= 3) {
3419
- if (request.method === "DELETE" && id)
3420
- return store.remove(resource, id, version(), db);
3421
- if (request.method === "POST" && !id || request.method === "PUT" && id) {
4372
+ if (request.method === "DELETE" && id2)
4373
+ return store.remove(resource, id2, version(), db);
4374
+ if (request.method === "POST" && !id2 || request.method === "PUT" && id2) {
3422
4375
  const value = resource === "providers" ? parse(providerInputSchema, body) : parse(profileInputSchema, body);
3423
- if (id && value.id !== id)
4376
+ if (id2 && value.id !== id2)
3424
4377
  throw new Fault(400, "id_mismatch", "Path and body IDs must match.");
3425
4378
  if (resource === "profiles") {
3426
4379
  const profile = value;
3427
4380
  const provider = await store.get("providers", profile.providerId, db);
3428
4381
  validateHarnessProvider(profile.harness, provider);
3429
4382
  }
3430
- const saved = await store.put(resource, value, id ? version() : undefined, db);
3431
- if (resource === "providers" && id)
3432
- await db.unsafe("DELETE FROM switcher_catalogs WHERE id = $1", [id]);
4383
+ const saved = await store.put(resource, value, id2 ? version() : undefined, db);
4384
+ if (resource === "providers" && id2)
4385
+ await db.unsafe("DELETE FROM switcher_catalogs WHERE id = $1", [id2]);
3433
4386
  return saved;
3434
4387
  }
3435
4388
  }
3436
- if (resource === "providers" && id && parts[3] === "refresh" && parts.length === 4 && request.method === "POST") {
4389
+ if (resource === "providers" && id2 && parts[3] === "refresh" && parts.length === 4 && request.method === "POST") {
3437
4390
  if (store.engine === "postgresql")
3438
- await db.unsafe("SELECT id FROM switcher_providers WHERE id = $1 FOR SHARE", [id]);
3439
- const provider = await store.get("providers", id, db);
4391
+ await db.unsafe("SELECT id FROM switcher_providers WHERE id = $1 FOR SHARE", [id2]);
4392
+ const provider = await store.get("providers", id2, db);
3440
4393
  if (!refreshed || provider.version !== refreshed.provider.version)
3441
4394
  throw new Fault(409, "provider_changed", "Provider changed during discovery; refresh again.");
3442
4395
  const catalog = refreshed.catalog;
3443
4396
  let old;
3444
4397
  try {
3445
- old = await store.get("catalogs", id, db);
4398
+ old = await store.get("catalogs", id2, db);
3446
4399
  } catch (e) {
3447
4400
  if (!(e instanceof Fault && e.status === 404))
3448
4401
  throw e;
3449
4402
  }
3450
- return store.put("catalogs", { id, ...catalog }, old?.version, db);
4403
+ return store.put("catalogs", { id: id2, ...catalog }, old?.version, db);
3451
4404
  }
3452
- if (resource === "launch-plans" && !id && request.method === "POST") {
3453
- const { profileId } = parse(z2.object({ profileId: idSchema }).strict(), body);
4405
+ if (resource === "launch-plans" && !id2 && request.method === "POST") {
4406
+ const { profileId } = parse(z3.object({ profileId: idSchema }).strict(), body);
3454
4407
  const profile = await store.get("profiles", profileId, db);
3455
4408
  const provider = await store.get("providers", profile.providerId, db);
3456
4409
  validateHarnessProvider(profile.harness, provider);
@@ -3467,6 +4420,7 @@ function createHandler(store, apiKey, providerEnv = process.env, resolveCredenti
3467
4420
  throw new Fault(422, "model_missing", "Selected model is not in the provider catalog.");
3468
4421
  if (!harnessEligible(selected, profile.harness))
3469
4422
  throw new Fault(422, "model_ineligible", "Selected model is unavailable or explicitly lacks a required generation method, text output or tool support.");
4423
+ compileModelPolicy(profile.model, catalog.models.filter((model) => harnessEligible(model, profile.harness)), profile.modelPolicy);
3470
4424
  const warnings = [];
3471
4425
  if (profile.harness !== "aider" && !selected.supportedParameters)
3472
4426
  warnings.push("Provider does not declare tool capabilities; execution compatibility is unverified.");
@@ -3476,7 +4430,9 @@ function createHandler(store, apiKey, providerEnv = process.env, resolveCredenti
3476
4430
  warnings.push("Catalog snapshot is older than five minutes; refresh before launching.");
3477
4431
  return { profile, provider, catalog, warnings, planToken: snapshot(profile, provider, catalog) };
3478
4432
  }
3479
- if (resource === "runs" && !id && request.method === "POST") {
4433
+ if (resource === "runs" && !id2 && request.method === "POST") {
4434
+ if (body?.modelPolicyVersion !== 1)
4435
+ throw new Fault(409, "launcher_upgrade_required", "This API requires a launcher with automatic model policy version 1; upgrade the Switcher CLI/SDK.");
3480
4436
  const input = parse(runInputSchema, body);
3481
4437
  if (store.engine === "postgresql")
3482
4438
  await db.unsafe("SELECT id FROM switcher_profiles WHERE id = $1 FOR SHARE", [input.profileId]);
@@ -3494,20 +4450,20 @@ function createHandler(store, apiKey, providerEnv = process.env, resolveCredenti
3494
4450
  throw new Fault(409, "plan_changed", "Catalog changed; request a fresh launch plan.");
3495
4451
  throw error;
3496
4452
  }
3497
- if (profile.harness !== input.harness || profile.model !== input.model || snapshot(profile, provider, catalog) !== input.planToken)
3498
- throw new Fault(409, "plan_changed", "Provider, profile or catalog changed; request a fresh launch plan.");
3499
- return store.put("runs", { ...input, providerId: provider.id, providerVersion: provider.version, profileVersion: profile.version, id: crypto.randomUUID(), status: "running", startedAt: new Date().toISOString() }, undefined, db);
4453
+ if (profile.harness !== input.harness || profile.model !== input.model || canonicalPolicyJSON(profile.modelPolicy ?? null) !== canonicalPolicyJSON(input.modelPolicy ?? profile.modelPolicy ?? null) || snapshot(profile, provider, catalog) !== input.planToken)
4454
+ throw new Fault(409, "plan_changed", "Provider, profile, model policy or catalog changed; request a fresh launch plan.");
4455
+ return store.put("runs", { ...input, modelPolicy: profile.modelPolicy, providerId: provider.id, providerVersion: provider.version, profileVersion: profile.version, id: crypto.randomUUID(), status: "running", startedAt: new Date().toISOString() }, undefined, db);
3500
4456
  }
3501
- if (resource === "runs" && id && request.method === "PATCH" && parts.length === 3) {
4457
+ if (resource === "runs" && id2 && request.method === "PATCH" && parts.length === 3) {
3502
4458
  const input = parse(runUpdateSchema, body);
3503
- const run = await store.get("runs", id, db);
4459
+ const run = await store.get("runs", id2, db);
3504
4460
  if (run.status !== "running")
3505
4461
  throw new Fault(409, "run_finished", "Run has already finished.");
3506
4462
  return store.put("runs", { ...run, ...input, endedAt: new Date().toISOString() }, version(), db);
3507
4463
  }
3508
4464
  throw new Fault(404, "not_found", "Route was not found.");
3509
4465
  });
3510
- return json(result, request.method === "POST" && ["providers", "profiles", "runs"].includes(resource) && !id ? 201 : 200);
4466
+ return json(result, request.method === "POST" && ["providers", "profiles", "runs"].includes(resource) && !id2 ? 201 : 200);
3511
4467
  } catch (error) {
3512
4468
  const safe = error instanceof Fault ? error : new Fault(500, "internal_error", "Request failed.");
3513
4469
  return json({ error: { code: safe.code, message: safe.message, requestId } }, safe.status);