@hasna/switcher 0.1.1 → 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.
Files changed (43) hide show
  1. package/README.md +146 -7
  2. package/dist/aider-args.d.ts +7 -0
  3. package/dist/aider-config.d.ts +6 -0
  4. package/dist/auth.d.ts +3 -0
  5. package/dist/cli/index.js +7548 -2558
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cline-backend.d.ts +7 -0
  8. package/dist/codex-model-policy.d.ts +65 -0
  9. package/dist/credentials.d.ts +5 -5
  10. package/dist/direct-launch.d.ts +3 -3
  11. package/dist/domain.d.ts +320 -50
  12. package/dist/dsh-args.d.ts +5 -0
  13. package/dist/gemini-bridge.d.ts +6 -0
  14. package/dist/gemini-config.d.ts +12 -0
  15. package/dist/gemini-model-policy.d.ts +56 -0
  16. package/dist/generated/api.d.ts +220 -21
  17. package/dist/harness-arguments.d.ts +1 -0
  18. package/dist/harness-installation.d.ts +19 -0
  19. package/dist/harness-types.d.ts +10 -0
  20. package/dist/harnesses.d.ts +5 -2
  21. package/dist/hermes-backend.d.ts +19 -0
  22. package/dist/hermes-model-policy.d.ts +30 -0
  23. package/dist/index.js +136 -43
  24. package/dist/inference-gateway.d.ts +24 -0
  25. package/dist/kilo-config.d.ts +13 -0
  26. package/dist/kilo.d.ts +5 -0
  27. package/dist/launcher.d.ts +12 -6
  28. package/dist/mcp/index.js +139 -55
  29. package/dist/model-policy-schema.d.ts +137 -0
  30. package/dist/model-policy.d.ts +31 -0
  31. package/dist/native-model-policy.d.ts +23 -0
  32. package/dist/omp-backend.d.ts +7 -0
  33. package/dist/opencode-model-policy.d.ts +33 -0
  34. package/dist/opencode2-config.d.ts +3 -3
  35. package/dist/ori-backend.d.ts +2 -2
  36. package/dist/ori-model-policy.d.ts +8 -0
  37. package/dist/presets.d.ts +11 -10
  38. package/dist/sdk.d.ts +279 -38
  39. package/dist/sdk.js +136 -43
  40. package/dist/serve/index.js +1306 -125
  41. package/docs/MODEL-POLICY.md +82 -0
  42. package/openapi.json +973 -14
  43. package/package.json +14 -3
@@ -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.1";
16
- var harnessSchema = z.enum(["claude", "codex", "grok", "opencode2", "pi"]);
17
- var protocolSchema = z.enum(["anthropic-messages", "openai-responses", "openai-chat"]);
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,73 +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()
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()
50
101
  }).strict();
51
- var providerInputSchema = z.object({
102
+ var providerInputSchema = z2.object({
52
103
  id: idSchema,
53
104
  name: label,
54
105
  baseUrl: urlSchema,
55
106
  protocol: protocolSchema,
56
107
  credentialEnv: envRef.optional(),
57
- authStyle: z.enum(["bearer", "x-api-key"]).default("bearer"),
108
+ authStyle: z2.enum(["bearer", "x-api-key", "api-key"]).default("bearer"),
58
109
  catalogBaseUrl: urlSchema.optional(),
59
- catalogFormat: z.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "none"]).optional(),
60
- catalogAuthStyle: z.enum(["bearer", "x-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(),
61
112
  catalogCredentialEnv: envRef.optional(),
62
- catalogAccountId: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/).optional(),
63
- modelsPath: z.string().regex(/^[a-zA-Z0-9_/-]+$/).max(200).default("models"),
64
- 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([])
65
116
  }).strict().refine((p) => !p.modelsPath.split("/").includes("..") && !p.modelsPath.startsWith("/"), "modelsPath must be relative");
66
- var providerPresetSchema = z.object({
117
+ var providerPresetSchema = z2.object({
67
118
  id: idSchema,
68
119
  name: label,
69
120
  credentialEnv: envRef.optional(),
70
- credentialAliases: z.array(z.string().regex(/^[A-Z][A-Z0-9_]+$/)),
71
- protocols: z.array(z.object({
121
+ credentialAliases: z2.array(z2.string().regex(/^[A-Z][A-Z0-9_]+$/)),
122
+ protocols: z2.array(z2.object({
72
123
  protocol: protocolSchema,
73
124
  baseUrl: urlSchema.optional(),
74
- authStyle: z.enum(["bearer", "x-api-key"]),
125
+ authStyle: z2.enum(["bearer", "x-api-key", "api-key"]),
75
126
  catalogBaseUrl: urlSchema.optional(),
76
- catalogFormat: z.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "none"]),
77
- catalogAuthStyle: z.enum(["bearer", "x-api-key", "none"]).optional(),
78
- modelsPath: z.string(),
79
- 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())
80
131
  }).strict()).min(1),
81
- sources: z.array(z.string().url()),
82
- verification: z.literal("documented")
132
+ sources: z2.array(z2.string().url()),
133
+ verification: z2.literal("documented")
83
134
  }).strict();
84
- var profileInputSchema = z.object({
135
+ var profileInputSchema = z2.object({
85
136
  id: idSchema,
86
137
  name: label,
87
138
  providerId: idSchema,
88
139
  harness: harnessSchema,
89
- model: z.string().min(1).max(300)
140
+ model: z2.string().min(1).max(300),
141
+ modelPolicy: modelPolicySchema.optional()
90
142
  }).strict();
91
- var runInputSchema = z.object({
143
+ var runInputSchema = z2.object({
144
+ modelPolicyVersion: z2.literal(1),
92
145
  profileId: idSchema,
93
146
  harness: harnessSchema,
94
- model: z.string().min(1).max(300),
95
- 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}$/)
96
150
  }).strict();
97
- var runUpdateSchema = z.object({
98
- status: z.enum(["exited", "failed", "interrupted"]),
99
- 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()
100
156
  }).strict();
101
157
 
102
158
  class Fault extends Error {
@@ -115,10 +171,27 @@ function parse(schema, value) {
115
171
  return result.data;
116
172
  }
117
173
  function compatible(harness, protocol) {
118
- return harness === "claude" ? protocol === "anthropic-messages" : harness === "codex" ? protocol === "openai-responses" : true;
174
+ if (harness === "claude")
175
+ return protocol === "anthropic-messages";
176
+ if (harness === "codex")
177
+ return protocol === "openai-responses";
178
+ if (harness === "gemini")
179
+ return protocol === "gemini-generate-content";
180
+ if (protocol === "gemini-generate-content")
181
+ return false;
182
+ return true;
183
+ }
184
+ function validateHarnessProvider(harness, provider) {
185
+ if (!compatible(harness, provider.protocol))
186
+ throw new Fault(422, "protocol_mismatch", "Harness does not support this provider protocol.");
187
+ if (harness === "gemini" && provider.authStyle !== "x-api-key")
188
+ throw new Fault(422, "auth_mismatch", "Gemini CLI requires x-api-key authentication for its native generateContent protocol.");
119
189
  }
120
190
  function codingEligible(model) {
121
- return model.available !== false && (!model.outputModalities || model.outputModalities.includes("text")) && (!model.supportedParameters || model.supportedParameters.includes("tools"));
191
+ return model.available !== false && (!model.supportedGenerationMethods || model.supportedGenerationMethods.includes("generateContent")) && (!model.outputModalities || model.outputModalities.includes("text")) && (!model.supportedParameters || model.supportedParameters.includes("tools"));
192
+ }
193
+ function harnessEligible(model, harness) {
194
+ return harness === "aider" ? model.available !== false && (!model.supportedGenerationMethods || model.supportedGenerationMethods.includes("generateContent")) && (!model.inputModalities || model.inputModalities.includes("text")) && (!model.outputModalities || model.outputModalities.includes("text")) : codingEligible(model);
122
195
  }
123
196
 
124
197
  // src/store.ts
@@ -280,8 +353,8 @@ class Store {
280
353
  }
281
354
 
282
355
  // src/service.ts
283
- import { createHash, timingSafeEqual } from "crypto";
284
- import { z as z2 } from "zod";
356
+ import { createHash as createHash2, timingSafeEqual } from "crypto";
357
+ import { z as z3 } from "zod";
285
358
 
286
359
  // src/http.ts
287
360
  var MAX_BYTES = 16 * 1024 * 1024;
@@ -317,6 +390,13 @@ async function boundedJson(response, maxBytes = MAX_BYTES) {
317
390
  }
318
391
  }
319
392
 
393
+ // src/auth.ts
394
+ function authHeader(style, credential) {
395
+ if (/[^\x20-\x7e]/.test(credential))
396
+ throw new Error("Provider credential contains invalid header characters.");
397
+ return style === "bearer" ? ["authorization", `Bearer ${credential}`] : [style, credential];
398
+ }
399
+
320
400
  // src/catalog.ts
321
401
  var positive = (v) => typeof v === "number" && Number.isInteger(v) && v > 0 ? v : undefined;
322
402
  var strings = (v) => Array.isArray(v) && v.every((i) => typeof i === "string") ? v : undefined;
@@ -327,6 +407,64 @@ var modalities = (v) => {
327
407
  throw new Fault(502, "invalid_catalog", "Provider returned malformed modality metadata.");
328
408
  return v.map((i) => i.toLowerCase());
329
409
  };
410
+ var CATALOG_REQUEST_TIMEOUT_MS = 20000;
411
+ var CATALOG_REFRESH_DEADLINE_MS = 60000;
412
+ var CATALOG_MAX_RETRIES = 2;
413
+ var TRANSIENT_CATALOG_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
414
+ function retryAfterMs(value) {
415
+ if (!value)
416
+ return;
417
+ const trimmed = value.trim();
418
+ if (/^\d+$/.test(trimmed)) {
419
+ try {
420
+ const milliseconds = BigInt(trimmed) * 1000n;
421
+ if (milliseconds > BigInt(Number.MAX_SAFE_INTEGER))
422
+ return Number.POSITIVE_INFINITY;
423
+ return Number(milliseconds);
424
+ } catch {
425
+ return Number.POSITIVE_INFINITY;
426
+ }
427
+ }
428
+ const date = Date.parse(trimmed);
429
+ return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
430
+ }
431
+ function catalogDeadlineFault() {
432
+ return new Fault(502, "provider_unavailable", "Provider catalog refresh exceeded its bounded deadline.");
433
+ }
434
+ async function waitForCatalogRetry(delayMs, deadline) {
435
+ if (Date.now() + delayMs > deadline)
436
+ throw catalogDeadlineFault();
437
+ if (delayMs > 0)
438
+ await new Promise((resolve2) => setTimeout(resolve2, delayMs));
439
+ }
440
+ async function fetchCatalogPage(url, headers, deadline) {
441
+ for (let retry = 0;; retry++) {
442
+ const remaining = deadline - Date.now();
443
+ if (remaining <= 0)
444
+ throw catalogDeadlineFault();
445
+ let response;
446
+ try {
447
+ response = await fetch(url, {
448
+ headers,
449
+ redirect: "manual",
450
+ signal: AbortSignal.timeout(Math.max(1, Math.min(CATALOG_REQUEST_TIMEOUT_MS, remaining)))
451
+ });
452
+ } catch {
453
+ if (retry >= CATALOG_MAX_RETRIES)
454
+ throw new Fault(502, "provider_unavailable", "Provider catalog request failed.");
455
+ await waitForCatalogRetry(100 * 2 ** retry, deadline);
456
+ continue;
457
+ }
458
+ if (response.ok)
459
+ return response;
460
+ const retryable = TRANSIENT_CATALOG_STATUSES.has(response.status);
461
+ await response.body?.cancel().catch(() => {});
462
+ if (!retryable || retry >= CATALOG_MAX_RETRIES)
463
+ throw new Fault(502, "provider_rejected", `Provider catalog returned HTTP ${response.status}.`);
464
+ const delay = retryAfterMs(response.headers.get("retry-after"));
465
+ await waitForCatalogRetry(delay ?? 100 * 2 ** retry, deadline);
466
+ }
467
+ }
330
468
  async function discover(provider, env = process.env, resolveCredential) {
331
469
  const refreshedAt = new Date().toISOString();
332
470
  if (provider.manualModels.length)
@@ -352,7 +490,8 @@ async function discover(provider, env = process.env, resolveCredential) {
352
490
  throw new Fault(422, "credential_missing", "Provider credential environment variable is not available on the server.");
353
491
  if (/[\r\n]/.test(credential))
354
492
  throw new Fault(422, "credential_invalid", "Catalog credential contains invalid header characters.");
355
- headers[authStyle === "x-api-key" ? "x-api-key" : "authorization"] = authStyle === "x-api-key" ? credential : `Bearer ${credential}`;
493
+ const [header, value] = authHeader(authStyle, credential);
494
+ headers[provider.catalogFormat === "gemini" && header === "x-api-key" ? "x-goog-api-key" : header] = value;
356
495
  }
357
496
  if (provider.protocol === "anthropic-messages" && url.hostname !== "openrouter.ai")
358
497
  headers["anthropic-version"] = "2023-06-01";
@@ -361,18 +500,10 @@ async function discover(provider, env = process.env, resolveCredential) {
361
500
  const models = new Map;
362
501
  const seenCursors = new Set;
363
502
  const seenPages = new Set([url.href]);
503
+ const deadline = Date.now() + CATALOG_REFRESH_DEADLINE_MS;
364
504
  let fireworksTotal;
365
505
  for (let page = 0;page < 100; page++) {
366
- let response;
367
- try {
368
- response = await fetch(url, { headers, redirect: "manual", signal: AbortSignal.timeout(20000) });
369
- } catch {
370
- throw new Fault(502, "provider_unavailable", "Provider catalog request failed.");
371
- }
372
- if (!response.ok) {
373
- await response.body?.cancel();
374
- throw new Fault(502, "provider_rejected", `Provider catalog returned HTTP ${response.status}.`);
375
- }
506
+ const response = await fetchCatalogPage(url, headers, deadline);
376
507
  const data = await boundedJson(response);
377
508
  if (provider.catalogFormat === "fireworks" && data?.totalSize !== undefined) {
378
509
  if (typeof data.totalSize !== "number" || !Number.isInteger(data.totalSize) || data.totalSize < 0)
@@ -381,11 +512,11 @@ async function discover(provider, env = process.env, resolveCredential) {
381
512
  throw new Fault(502, "incomplete_catalog", "Provider catalog count changed during pagination; retry the refresh.");
382
513
  fireworksTotal = data.totalSize;
383
514
  }
384
- const rows = provider.catalogFormat === "together" ? data : provider.catalogFormat === "ollama" ? data?.models : provider.catalogFormat === "fireworks" ? data?.models : provider.catalogFormat === "dashscope" ? data?.output?.models : data?.data;
515
+ const rows = provider.catalogFormat === "together" ? data : provider.catalogFormat === "ollama" ? data?.models : provider.catalogFormat === "fireworks" || provider.catalogFormat === "gemini" ? data?.models : provider.catalogFormat === "dashscope" ? data?.output?.models : data?.data;
385
516
  if (!Array.isArray(rows))
386
517
  throw new Fault(502, "invalid_catalog", "Expected a provider catalog with a model array matching its configured format.");
387
518
  for (const row of rows) {
388
- const id = provider.catalogFormat === "ollama" ? row?.model ?? row?.name : provider.catalogFormat === "fireworks" ? row?.name : provider.catalogFormat === "dashscope" ? row?.model : row?.id;
519
+ const id = provider.catalogFormat === "ollama" ? row?.model ?? row?.name : provider.catalogFormat === "fireworks" ? row?.name : provider.catalogFormat === "gemini" ? typeof row?.name === "string" ? row.name.replace(/^models\//, "") : undefined : provider.catalogFormat === "dashscope" ? row?.model : row?.id;
389
520
  if (typeof id !== "string")
390
521
  throw new Fault(502, "invalid_catalog", "Catalog entry is missing a model ID.");
391
522
  const candidate = {
@@ -393,11 +524,12 @@ async function discover(provider, env = process.env, resolveCredential) {
393
524
  name: row.displayName ?? row.name ?? row.display_name ?? id,
394
525
  available: provider.catalogFormat === "mistral" && typeof row.archived === "boolean" ? !row.archived : undefined,
395
526
  description: typeof row.description === "string" ? row.description.slice(0, 8000) : undefined,
396
- contextWindow: positive(row.context_length ?? row.context_window ?? row.contextLength ?? row.model_info?.context_window ?? (provider.catalogFormat === "mistral" ? row.max_context_length : undefined)),
397
- maxOutputTokens: positive(row.top_provider?.max_completion_tokens ?? row.max_output_tokens ?? row.model_info?.max_output_tokens),
527
+ contextWindow: positive(row.context_length ?? row.context_window ?? row.contextLength ?? row.inputTokenLimit ?? row.model_info?.context_window ?? (provider.catalogFormat === "mistral" ? row.max_context_length : undefined)),
528
+ maxOutputTokens: positive(row.top_provider?.max_completion_tokens ?? row.max_output_tokens ?? row.outputTokenLimit ?? row.model_info?.max_output_tokens),
398
529
  inputModalities: strings(row.architecture?.input_modalities ?? row.input_modalities) ?? modalities(row.inference_metadata?.request_modality),
399
530
  outputModalities: strings(row.architecture?.output_modalities ?? row.output_modalities) ?? modalities(row.inference_metadata?.response_modality),
400
- supportedParameters: strings(row.supported_parameters)
531
+ supportedParameters: strings(row.supported_parameters),
532
+ ...provider.catalogFormat === "gemini" && row.supportedGenerationMethods !== undefined ? { supportedGenerationMethods: row.supportedGenerationMethods } : {}
401
533
  };
402
534
  if (provider.catalogFormat === "mistral") {
403
535
  const capabilities = row.capabilities;
@@ -468,6 +600,21 @@ async function discover(provider, env = process.env, resolveCredential) {
468
600
  throw new Fault(502, "incomplete_catalog", "Provider catalog count does not match the collected models; retry the refresh.");
469
601
  return { models: [...models.values()], source: "remote", refreshedAt };
470
602
  }
603
+ if (provider.catalogFormat === "gemini") {
604
+ const nextPageToken = data.nextPageToken;
605
+ if (nextPageToken !== undefined && nextPageToken !== null) {
606
+ if (typeof nextPageToken !== "string" || nextPageToken.length > 2000)
607
+ throw new Fault(502, "invalid_catalog", "Gemini catalog pagination token is malformed.");
608
+ if (nextPageToken) {
609
+ if (seenCursors.has(nextPageToken))
610
+ throw new Fault(502, "invalid_catalog", "Provider catalog pagination did not advance.");
611
+ seenCursors.add(nextPageToken);
612
+ url.searchParams.set("pageToken", nextPageToken);
613
+ continue;
614
+ }
615
+ }
616
+ return { models: [...models.values()], source: "remote", refreshedAt };
617
+ }
471
618
  if (provider.catalogFormat === "dashscope") {
472
619
  const output = data.output;
473
620
  const total = output?.total;
@@ -503,6 +650,69 @@ async function discover(provider, env = process.env, resolveCredential) {
503
650
  throw new Fault(502, "catalog_too_large", "Provider catalog pagination exceeded 100 pages.");
504
651
  }
505
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
+
506
716
  // src/presets.ts
507
717
  var route = (protocol, baseUrl, options = {}) => ({
508
718
  protocol,
@@ -513,13 +723,13 @@ var route = (protocol, baseUrl, options = {}) => ({
513
723
  notes: [],
514
724
  ...options
515
725
  });
516
- var preset = (id, name, protocols, sources, alias) => parse(providerPresetSchema, {
517
- id,
726
+ var preset = (id2, name, protocols, sources, alias) => parse(providerPresetSchema, {
727
+ id: id2,
518
728
  name,
519
729
  protocols,
520
730
  sources,
521
731
  credentialAliases: alias ? [alias] : [],
522
- credentialEnv: alias ? `SWITCHER_PROVIDER_${id.toUpperCase().replace(/-/g, "_")}` : undefined,
732
+ credentialEnv: alias ? `SWITCHER_PROVIDER_${id2.toUpperCase().replace(/-/g, "_")}` : undefined,
523
733
  verification: "documented"
524
734
  });
525
735
  var providerPresets = [
@@ -529,7 +739,18 @@ var providerPresets = [
529
739
  ], ["https://api-docs.deepseek.com/guides/anthropic_api", "https://api-docs.deepseek.com/api/list-models"], "DEEPSEEK_API_KEY"),
530
740
  preset("openrouter", "OpenRouter", ["openai-chat", "openai-responses", "anthropic-messages"].map((protocol) => route(protocol, "https://openrouter.ai/api/v1", { catalogAuthStyle: "none" })), ["https://openrouter.ai/docs/api/api-reference/models/list-all-models-and-their-properties", "https://openrouter.ai/docs/guides/overview"], "OPENROUTER_API_KEY"),
531
741
  preset("anthropic", "Anthropic", [route("anthropic-messages", "https://api.anthropic.com/v1", { authStyle: "x-api-key" })], ["https://platform.claude.com/docs/en/api/overview", "https://platform.claude.com/docs/en/api/models/list"], "ANTHROPIC_API_KEY"),
742
+ preset("gemini", "Google Gemini", [route("gemini-generate-content", "https://generativelanguage.googleapis.com/v1beta", {
743
+ authStyle: "x-api-key",
744
+ catalogBaseUrl: "https://generativelanguage.googleapis.com/v1beta",
745
+ catalogFormat: "gemini",
746
+ catalogAuthStyle: "x-api-key",
747
+ notes: ["Gemini CLI uses the native generateContent wire with x-goog-api-key authentication; model IDs are returned as models/{id}."]
748
+ }), route("openai-chat", "https://generativelanguage.googleapis.com/v1beta/openai")], ["https://ai.google.dev/api", "https://ai.google.dev/api/models", "https://github.com/google-gemini/gemini-cli", "https://ai.google.dev/gemini-api/docs/openai"], "GEMINI_API_KEY"),
532
749
  preset("openai", "OpenAI", [route("openai-responses", "https://api.openai.com/v1"), route("openai-chat", "https://api.openai.com/v1")], ["https://platform.openai.com/docs/api-reference/introduction", "https://platform.openai.com/docs/api-reference/models/list"], "OPENAI_API_KEY"),
750
+ preset("azure-openai", "Azure OpenAI (v1)", [
751
+ route("openai-responses", undefined, { authStyle: "api-key", catalogFormat: "none", notes: ["Pass the Azure OpenAI v1 resource endpoint ending in /openai/v1. The request model is your deployment name. Azure's model-definition list is not a deployment catalog, so configure manual deployment models or an explicit deployment catalog parser; Switcher does not synthesize deployment paths or api-version query parameters."] }),
752
+ route("openai-chat", undefined, { authStyle: "api-key", catalogFormat: "none", notes: ["Pass the Azure OpenAI v1 resource endpoint ending in /openai/v1. Chat Completions is POST /chat/completions and accepts the literal api-key header. The request model is your deployment name; configure manual deployment models or an explicit deployment catalog parser because GET /models does not establish deployment names."] })
753
+ ], ["https://learn.microsoft.com/en-us/rest/api/aifoundry/azureopenai/models", "https://learn.microsoft.com/en-us/rest/api/microsoft-foundry/azureopenai/chat", "https://learn.microsoft.com/en-us/rest/api/aifoundry/azureopenai/responses"], "AZURE_OPENAI_API_KEY"),
533
754
  preset("xai", "xAI", ["openai-chat", "openai-responses", "anthropic-messages"].map((protocol) => route(protocol, "https://api.x.ai/v1")), ["https://api.x.ai/docs/", "https://docs.x.ai/developers/model-capabilities/text/generate-text"], "XAI_API_KEY"),
534
755
  preset("ollama", "Ollama", ["openai-chat", "openai-responses"].map((protocol) => route(protocol, "http://127.0.0.1:11434/v1", {
535
756
  catalogBaseUrl: "http://127.0.0.1:11434",
@@ -576,8 +797,8 @@ var providerPresets = [
576
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"),
577
798
  ...["anthropic-messages", "openai-responses", "openai-chat"].map((protocol) => preset(`generic-${protocol}`, `Custom ${protocol}`, [route(protocol)], []))
578
799
  ];
579
- function getProviderPreset(id) {
580
- const entry = providerPresets.find((p) => p.id === id);
800
+ function getProviderPreset(id2) {
801
+ const entry = providerPresets.find((p) => p.id === id2);
581
802
  if (!entry)
582
803
  throw new Fault(404, "preset_not_found", "Unknown provider preset. Use switcher providers presets to list available presets.");
583
804
  return structuredClone(entry);
@@ -587,7 +808,7 @@ var openapi_default = {
587
808
  openapi: "3.0.3",
588
809
  info: {
589
810
  title: "Switcher API",
590
- version: "0.1.1",
811
+ version: "0.1.3",
591
812
  description: "Authenticated provider/profile/catalog control plane. Launches run locally; the API never returns provider credentials."
592
813
  },
593
814
  security: [
@@ -1801,7 +2022,8 @@ var openapi_default = {
1801
2022
  enum: [
1802
2023
  "anthropic-messages",
1803
2024
  "openai-responses",
1804
- "openai-chat"
2025
+ "openai-chat",
2026
+ "gemini-generate-content"
1805
2027
  ]
1806
2028
  },
1807
2029
  baseUrl: {
@@ -1812,7 +2034,8 @@ var openapi_default = {
1812
2034
  type: "string",
1813
2035
  enum: [
1814
2036
  "bearer",
1815
- "x-api-key"
2037
+ "x-api-key",
2038
+ "api-key"
1816
2039
  ]
1817
2040
  },
1818
2041
  catalogBaseUrl: {
@@ -1828,6 +2051,7 @@ var openapi_default = {
1828
2051
  "together",
1829
2052
  "fireworks",
1830
2053
  "dashscope",
2054
+ "gemini",
1831
2055
  "none"
1832
2056
  ]
1833
2057
  },
@@ -1836,6 +2060,7 @@ var openapi_default = {
1836
2060
  enum: [
1837
2061
  "bearer",
1838
2062
  "x-api-key",
2063
+ "api-key",
1839
2064
  "none"
1840
2065
  ]
1841
2066
  },
@@ -1905,7 +2130,8 @@ var openapi_default = {
1905
2130
  enum: [
1906
2131
  "anthropic-messages",
1907
2132
  "openai-responses",
1908
- "openai-chat"
2133
+ "openai-chat",
2134
+ "gemini-generate-content"
1909
2135
  ]
1910
2136
  },
1911
2137
  credentialEnv: {
@@ -1916,7 +2142,8 @@ var openapi_default = {
1916
2142
  type: "string",
1917
2143
  enum: [
1918
2144
  "bearer",
1919
- "x-api-key"
2145
+ "x-api-key",
2146
+ "api-key"
1920
2147
  ],
1921
2148
  default: "bearer"
1922
2149
  },
@@ -1933,6 +2160,7 @@ var openapi_default = {
1933
2160
  "together",
1934
2161
  "fireworks",
1935
2162
  "dashscope",
2163
+ "gemini",
1936
2164
  "none"
1937
2165
  ]
1938
2166
  },
@@ -1941,6 +2169,7 @@ var openapi_default = {
1941
2169
  enum: [
1942
2170
  "bearer",
1943
2171
  "x-api-key",
2172
+ "api-key",
1944
2173
  "none"
1945
2174
  ]
1946
2175
  },
@@ -2013,6 +2242,15 @@ var openapi_default = {
2013
2242
  maxLength: 100
2014
2243
  },
2015
2244
  maxItems: 100
2245
+ },
2246
+ supportedGenerationMethods: {
2247
+ type: "array",
2248
+ items: {
2249
+ type: "string",
2250
+ minLength: 1,
2251
+ maxLength: 100
2252
+ },
2253
+ maxItems: 100
2016
2254
  }
2017
2255
  },
2018
2256
  required: [
@@ -2054,7 +2292,8 @@ var openapi_default = {
2054
2292
  enum: [
2055
2293
  "anthropic-messages",
2056
2294
  "openai-responses",
2057
- "openai-chat"
2295
+ "openai-chat",
2296
+ "gemini-generate-content"
2058
2297
  ]
2059
2298
  },
2060
2299
  credentialEnv: {
@@ -2065,7 +2304,8 @@ var openapi_default = {
2065
2304
  type: "string",
2066
2305
  enum: [
2067
2306
  "bearer",
2068
- "x-api-key"
2307
+ "x-api-key",
2308
+ "api-key"
2069
2309
  ],
2070
2310
  default: "bearer"
2071
2311
  },
@@ -2082,6 +2322,7 @@ var openapi_default = {
2082
2322
  "together",
2083
2323
  "fireworks",
2084
2324
  "dashscope",
2325
+ "gemini",
2085
2326
  "none"
2086
2327
  ]
2087
2328
  },
@@ -2090,6 +2331,7 @@ var openapi_default = {
2090
2331
  enum: [
2091
2332
  "bearer",
2092
2333
  "x-api-key",
2334
+ "api-key",
2093
2335
  "none"
2094
2336
  ]
2095
2337
  },
@@ -2162,6 +2404,15 @@ var openapi_default = {
2162
2404
  maxLength: 100
2163
2405
  },
2164
2406
  maxItems: 100
2407
+ },
2408
+ supportedGenerationMethods: {
2409
+ type: "array",
2410
+ items: {
2411
+ type: "string",
2412
+ minLength: 1,
2413
+ maxLength: 100
2414
+ },
2415
+ maxItems: 100
2165
2416
  }
2166
2417
  },
2167
2418
  required: [
@@ -2214,14 +2465,122 @@ var openapi_default = {
2214
2465
  "claude",
2215
2466
  "codex",
2216
2467
  "grok",
2468
+ "opencode",
2217
2469
  "opencode2",
2218
- "pi"
2470
+ "pi",
2471
+ "omp",
2472
+ "dsh",
2473
+ "cline",
2474
+ "hermes",
2475
+ "prime-agent",
2476
+ "gemini",
2477
+ "aider",
2478
+ "kilo"
2219
2479
  ]
2220
2480
  },
2221
2481
  model: {
2222
2482
  type: "string",
2223
2483
  minLength: 1,
2224
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
2225
2584
  }
2226
2585
  },
2227
2586
  required: [
@@ -2255,8 +2614,17 @@ var openapi_default = {
2255
2614
  "claude",
2256
2615
  "codex",
2257
2616
  "grok",
2617
+ "opencode",
2258
2618
  "opencode2",
2259
- "pi"
2619
+ "pi",
2620
+ "omp",
2621
+ "dsh",
2622
+ "cline",
2623
+ "hermes",
2624
+ "prime-agent",
2625
+ "gemini",
2626
+ "aider",
2627
+ "kilo"
2260
2628
  ]
2261
2629
  },
2262
2630
  model: {
@@ -2264,6 +2632,105 @@ var openapi_default = {
2264
2632
  minLength: 1,
2265
2633
  maxLength: 300
2266
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
+ },
2267
2734
  version: {
2268
2735
  type: "integer",
2269
2736
  exclusiveMinimum: true,
@@ -2337,6 +2804,15 @@ var openapi_default = {
2337
2804
  maxLength: 100
2338
2805
  },
2339
2806
  maxItems: 100
2807
+ },
2808
+ supportedGenerationMethods: {
2809
+ type: "array",
2810
+ items: {
2811
+ type: "string",
2812
+ minLength: 1,
2813
+ maxLength: 100
2814
+ },
2815
+ maxItems: 100
2340
2816
  }
2341
2817
  },
2342
2818
  required: [
@@ -2345,6 +2821,175 @@ var openapi_default = {
2345
2821
  ],
2346
2822
  additionalProperties: false
2347
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
+ },
2348
2993
  ModelPage: {
2349
2994
  type: "object",
2350
2995
  properties: {
@@ -2404,6 +3049,15 @@ var openapi_default = {
2404
3049
  },
2405
3050
  maxItems: 100
2406
3051
  },
3052
+ supportedGenerationMethods: {
3053
+ type: "array",
3054
+ items: {
3055
+ type: "string",
3056
+ minLength: 1,
3057
+ maxLength: 100
3058
+ },
3059
+ maxItems: 100
3060
+ },
2407
3061
  codingEligible: {
2408
3062
  type: "boolean"
2409
3063
  }
@@ -2504,6 +3158,15 @@ var openapi_default = {
2504
3158
  maxLength: 100
2505
3159
  },
2506
3160
  maxItems: 100
3161
+ },
3162
+ supportedGenerationMethods: {
3163
+ type: "array",
3164
+ items: {
3165
+ type: "string",
3166
+ minLength: 1,
3167
+ maxLength: 100
3168
+ },
3169
+ maxItems: 100
2507
3170
  }
2508
3171
  },
2509
3172
  required: [
@@ -2555,7 +3218,8 @@ var openapi_default = {
2555
3218
  enum: [
2556
3219
  "anthropic-messages",
2557
3220
  "openai-responses",
2558
- "openai-chat"
3221
+ "openai-chat",
3222
+ "gemini-generate-content"
2559
3223
  ]
2560
3224
  },
2561
3225
  credentialEnv: {
@@ -2566,7 +3230,8 @@ var openapi_default = {
2566
3230
  type: "string",
2567
3231
  enum: [
2568
3232
  "bearer",
2569
- "x-api-key"
3233
+ "x-api-key",
3234
+ "api-key"
2570
3235
  ],
2571
3236
  default: "bearer"
2572
3237
  },
@@ -2583,6 +3248,7 @@ var openapi_default = {
2583
3248
  "together",
2584
3249
  "fireworks",
2585
3250
  "dashscope",
3251
+ "gemini",
2586
3252
  "none"
2587
3253
  ]
2588
3254
  },
@@ -2591,6 +3257,7 @@ var openapi_default = {
2591
3257
  enum: [
2592
3258
  "bearer",
2593
3259
  "x-api-key",
3260
+ "api-key",
2594
3261
  "none"
2595
3262
  ]
2596
3263
  },
@@ -2663,6 +3330,15 @@ var openapi_default = {
2663
3330
  maxLength: 100
2664
3331
  },
2665
3332
  maxItems: 100
3333
+ },
3334
+ supportedGenerationMethods: {
3335
+ type: "array",
3336
+ items: {
3337
+ type: "string",
3338
+ minLength: 1,
3339
+ maxLength: 100
3340
+ },
3341
+ maxItems: 100
2666
3342
  }
2667
3343
  },
2668
3344
  required: [
@@ -2715,8 +3391,17 @@ var openapi_default = {
2715
3391
  "claude",
2716
3392
  "codex",
2717
3393
  "grok",
3394
+ "opencode",
2718
3395
  "opencode2",
2719
- "pi"
3396
+ "pi",
3397
+ "omp",
3398
+ "dsh",
3399
+ "cline",
3400
+ "hermes",
3401
+ "prime-agent",
3402
+ "gemini",
3403
+ "aider",
3404
+ "kilo"
2720
3405
  ]
2721
3406
  },
2722
3407
  model: {
@@ -2724,6 +3409,105 @@ var openapi_default = {
2724
3409
  minLength: 1,
2725
3410
  maxLength: 300
2726
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
+ },
2727
3511
  version: {
2728
3512
  type: "integer",
2729
3513
  exclusiveMinimum: true,
@@ -2802,6 +3586,15 @@ var openapi_default = {
2802
3586
  maxLength: 100
2803
3587
  },
2804
3588
  maxItems: 100
3589
+ },
3590
+ supportedGenerationMethods: {
3591
+ type: "array",
3592
+ items: {
3593
+ type: "string",
3594
+ minLength: 1,
3595
+ maxLength: 100
3596
+ },
3597
+ maxItems: 100
2805
3598
  }
2806
3599
  },
2807
3600
  required: [
@@ -2851,6 +3644,12 @@ var openapi_default = {
2851
3644
  RunInput: {
2852
3645
  type: "object",
2853
3646
  properties: {
3647
+ modelPolicyVersion: {
3648
+ type: "number",
3649
+ enum: [
3650
+ 1
3651
+ ]
3652
+ },
2854
3653
  profileId: {
2855
3654
  type: "string",
2856
3655
  pattern: "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$"
@@ -2861,8 +3660,17 @@ var openapi_default = {
2861
3660
  "claude",
2862
3661
  "codex",
2863
3662
  "grok",
3663
+ "opencode",
2864
3664
  "opencode2",
2865
- "pi"
3665
+ "pi",
3666
+ "omp",
3667
+ "dsh",
3668
+ "cline",
3669
+ "hermes",
3670
+ "prime-agent",
3671
+ "gemini",
3672
+ "aider",
3673
+ "kilo"
2866
3674
  ]
2867
3675
  },
2868
3676
  model: {
@@ -2870,12 +3678,112 @@ var openapi_default = {
2870
3678
  minLength: 1,
2871
3679
  maxLength: 300
2872
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
+ },
2873
3780
  planToken: {
2874
3781
  type: "string",
2875
3782
  pattern: "^[a-f0-9]{64}$"
2876
3783
  }
2877
3784
  },
2878
3785
  required: [
3786
+ "modelPolicyVersion",
2879
3787
  "profileId",
2880
3788
  "harness",
2881
3789
  "model",
@@ -2898,6 +3806,85 @@ var openapi_default = {
2898
3806
  type: "integer",
2899
3807
  minimum: 0,
2900
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
2901
3888
  }
2902
3889
  },
2903
3890
  required: [
@@ -2909,6 +3896,12 @@ var openapi_default = {
2909
3896
  Run: {
2910
3897
  type: "object",
2911
3898
  properties: {
3899
+ modelPolicyVersion: {
3900
+ type: "number",
3901
+ enum: [
3902
+ 1
3903
+ ]
3904
+ },
2912
3905
  profileId: {
2913
3906
  type: "string",
2914
3907
  pattern: "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$"
@@ -2919,8 +3912,17 @@ var openapi_default = {
2919
3912
  "claude",
2920
3913
  "codex",
2921
3914
  "grok",
3915
+ "opencode",
2922
3916
  "opencode2",
2923
- "pi"
3917
+ "pi",
3918
+ "omp",
3919
+ "dsh",
3920
+ "cline",
3921
+ "hermes",
3922
+ "prime-agent",
3923
+ "gemini",
3924
+ "aider",
3925
+ "kilo"
2924
3926
  ]
2925
3927
  },
2926
3928
  model: {
@@ -2928,6 +3930,105 @@ var openapi_default = {
2928
3930
  minLength: 1,
2929
3931
  maxLength: 300
2930
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
+ },
2931
4032
  planToken: {
2932
4033
  type: "string",
2933
4034
  pattern: "^[a-f0-9]{64}$"
@@ -2975,6 +4076,85 @@ var openapi_default = {
2975
4076
  },
2976
4077
  exitCode: {
2977
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
2978
4158
  }
2979
4159
  },
2980
4160
  required: [
@@ -3101,8 +4281,8 @@ var openapi_default = {
3101
4281
  };
3102
4282
 
3103
4283
  // src/service.ts
3104
- var snapshot = (profile, provider, catalog) => createHash("sha256").update(JSON.stringify([profile, provider, { models: catalog.models, source: catalog.source }])).digest("hex");
3105
- 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();
3106
4286
  function createHandler(store, apiKey, providerEnv = process.env, resolveCredential) {
3107
4287
  if (!apiKey || apiKey.length < 24)
3108
4288
  throw new Fault(500, "auth_config", "Set HASNA_SWITCHER_API_KEY to a random token of at least 24 characters.");
@@ -3133,23 +4313,23 @@ function createHandler(store, apiKey, providerEnv = process.env, resolveCredenti
3133
4313
  if (parts[0] !== "v1")
3134
4314
  throw new Fault(404, "not_found", "Route was not found.");
3135
4315
  const resource = parts[1];
3136
- const id = parts[2];
3137
- if (id)
3138
- parse(idSchema, id);
3139
- const page = () => parse(z2.object({
3140
- limit: z2.coerce.number().int().min(1).max(1000).default(100),
3141
- offset: z2.coerce.number().int().min(0).max(1e6).default(0),
3142
- 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("")
3143
4323
  }).strict(), Object.fromEntries(url.searchParams));
3144
4324
  if (request.method === "GET") {
3145
4325
  if (resource === "provider-presets" && parts.length <= 3)
3146
- return json(id ? getProviderPreset(id) : { data: providerPresets });
4326
+ return json(id2 ? getProviderPreset(id2) : { data: providerPresets });
3147
4327
  if (["providers", "profiles", "runs"].includes(resource) && parts.length <= 3) {
3148
4328
  const kind = resource;
3149
- 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()));
3150
4330
  }
3151
- if (resource === "providers" && id && parts[3] === "models" && parts.length === 4) {
3152
- 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);
3153
4333
  const p = page();
3154
4334
  const filtered = catalog.models.filter((m) => [m.id, m.name].some((s) => s.toLowerCase().includes(p.search.toLowerCase())));
3155
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 });
@@ -3180,55 +4360,53 @@ function createHandler(store, apiKey, providerEnv = process.env, resolveCredenti
3180
4360
  };
3181
4361
  const replay = await store.replay(key, fingerprint);
3182
4362
  if (replay.found)
3183
- 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);
3184
4364
  let refreshed;
3185
- if (resource === "providers" && id && parts[3] === "refresh" && parts.length === 4 && request.method === "POST") {
3186
- parse(z2.object({}).strict(), body);
3187
- 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);
3188
4368
  refreshed = { provider, catalog: await discover(provider, providerEnv, resolveCredential) };
3189
4369
  }
3190
4370
  const result = await store.mutate(key, fingerprint, async (db) => {
3191
4371
  if ((resource === "providers" || resource === "profiles") && parts.length <= 3) {
3192
- if (request.method === "DELETE" && id)
3193
- return store.remove(resource, id, version(), db);
3194
- 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) {
3195
4375
  const value = resource === "providers" ? parse(providerInputSchema, body) : parse(profileInputSchema, body);
3196
- if (id && value.id !== id)
4376
+ if (id2 && value.id !== id2)
3197
4377
  throw new Fault(400, "id_mismatch", "Path and body IDs must match.");
3198
4378
  if (resource === "profiles") {
3199
4379
  const profile = value;
3200
4380
  const provider = await store.get("providers", profile.providerId, db);
3201
- if (!compatible(profile.harness, provider.protocol))
3202
- throw new Fault(422, "protocol_mismatch", "Harness does not support this provider protocol.");
4381
+ validateHarnessProvider(profile.harness, provider);
3203
4382
  }
3204
- const saved = await store.put(resource, value, id ? version() : undefined, db);
3205
- if (resource === "providers" && id)
3206
- 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]);
3207
4386
  return saved;
3208
4387
  }
3209
4388
  }
3210
- 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") {
3211
4390
  if (store.engine === "postgresql")
3212
- await db.unsafe("SELECT id FROM switcher_providers WHERE id = $1 FOR SHARE", [id]);
3213
- 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);
3214
4393
  if (!refreshed || provider.version !== refreshed.provider.version)
3215
4394
  throw new Fault(409, "provider_changed", "Provider changed during discovery; refresh again.");
3216
4395
  const catalog = refreshed.catalog;
3217
4396
  let old;
3218
4397
  try {
3219
- old = await store.get("catalogs", id, db);
4398
+ old = await store.get("catalogs", id2, db);
3220
4399
  } catch (e) {
3221
4400
  if (!(e instanceof Fault && e.status === 404))
3222
4401
  throw e;
3223
4402
  }
3224
- return store.put("catalogs", { id, ...catalog }, old?.version, db);
4403
+ return store.put("catalogs", { id: id2, ...catalog }, old?.version, db);
3225
4404
  }
3226
- if (resource === "launch-plans" && !id && request.method === "POST") {
3227
- 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);
3228
4407
  const profile = await store.get("profiles", profileId, db);
3229
4408
  const provider = await store.get("providers", profile.providerId, db);
3230
- if (!compatible(profile.harness, provider.protocol))
3231
- throw new Fault(422, "protocol_mismatch", "Harness does not support this provider protocol.");
4409
+ validateHarnessProvider(profile.harness, provider);
3232
4410
  let catalog;
3233
4411
  try {
3234
4412
  catalog = await store.get("catalogs", provider.id, db);
@@ -3240,10 +4418,11 @@ function createHandler(store, apiKey, providerEnv = process.env, resolveCredenti
3240
4418
  const selected = catalog.models.find((m) => m.id === profile.model);
3241
4419
  if (!selected)
3242
4420
  throw new Fault(422, "model_missing", "Selected model is not in the provider catalog.");
3243
- if (!codingEligible(selected))
3244
- throw new Fault(422, "model_ineligible", "Selected model explicitly lacks text output or tool support.");
4421
+ if (!harnessEligible(selected, profile.harness))
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);
3245
4424
  const warnings = [];
3246
- if (!selected.supportedParameters)
4425
+ if (profile.harness !== "aider" && !selected.supportedParameters)
3247
4426
  warnings.push("Provider does not declare tool capabilities; execution compatibility is unverified.");
3248
4427
  if (profile.harness === "claude" && !/claude/i.test(profile.model))
3249
4428
  warnings.push("Anthropic does not support non-Claude models in Claude Code; this combination is experimental.");
@@ -3251,7 +4430,9 @@ function createHandler(store, apiKey, providerEnv = process.env, resolveCredenti
3251
4430
  warnings.push("Catalog snapshot is older than five minutes; refresh before launching.");
3252
4431
  return { profile, provider, catalog, warnings, planToken: snapshot(profile, provider, catalog) };
3253
4432
  }
3254
- 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.");
3255
4436
  const input = parse(runInputSchema, body);
3256
4437
  if (store.engine === "postgresql")
3257
4438
  await db.unsafe("SELECT id FROM switcher_profiles WHERE id = $1 FOR SHARE", [input.profileId]);
@@ -3269,20 +4450,20 @@ function createHandler(store, apiKey, providerEnv = process.env, resolveCredenti
3269
4450
  throw new Fault(409, "plan_changed", "Catalog changed; request a fresh launch plan.");
3270
4451
  throw error;
3271
4452
  }
3272
- if (profile.harness !== input.harness || profile.model !== input.model || snapshot(profile, provider, catalog) !== input.planToken)
3273
- throw new Fault(409, "plan_changed", "Provider, profile or catalog changed; request a fresh launch plan.");
3274
- 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);
3275
4456
  }
3276
- if (resource === "runs" && id && request.method === "PATCH" && parts.length === 3) {
4457
+ if (resource === "runs" && id2 && request.method === "PATCH" && parts.length === 3) {
3277
4458
  const input = parse(runUpdateSchema, body);
3278
- const run = await store.get("runs", id, db);
4459
+ const run = await store.get("runs", id2, db);
3279
4460
  if (run.status !== "running")
3280
4461
  throw new Fault(409, "run_finished", "Run has already finished.");
3281
4462
  return store.put("runs", { ...run, ...input, endedAt: new Date().toISOString() }, version(), db);
3282
4463
  }
3283
4464
  throw new Fault(404, "not_found", "Route was not found.");
3284
4465
  });
3285
- 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);
3286
4467
  } catch (error) {
3287
4468
  const safe = error instanceof Fault ? error : new Fault(500, "internal_error", "Request failed.");
3288
4469
  return json({ error: { code: safe.code, message: safe.message, requestId } }, safe.status);