@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
package/dist/sdk.js CHANGED
@@ -1,10 +1,56 @@
1
1
  // src/domain.ts
2
+ import { z as z2 } from "zod";
3
+
4
+ // src/model-policy-schema.ts
2
5
  import { z } from "zod";
3
- var harnessSchema = z.enum(["claude", "codex", "grok", "opencode2", "pi"]);
4
- var protocolSchema = z.enum(["anthropic-messages", "openai-responses", "openai-chat"]);
5
- var idSchema = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/);
6
- var label = z.string().min(1).max(200);
7
- var envRef = z.string().regex(/^SWITCHER_PROVIDER_[A-Z0-9_]+$/);
6
+ var policyModelIdSchema = z.string().min(1).max(300).regex(/^[^\u0000-\u001f\u007f]+$/);
7
+ var modelPolicyRoleSchema = z.enum(["subagent", "fast", "planning", "review", "summary", "compaction", "weak", "editor"]);
8
+ var boundedModelList = z.array(policyModelIdSchema).max(500);
9
+ var aliasSchema = z.string().regex(/^[A-Za-z0-9._/-]{1,120}$/).refine((v) => !["__proto__", "prototype", "constructor"].includes(v));
10
+ var boundedModelMap = z.record(aliasSchema, policyModelIdSchema).superRefine((value, ctx) => {
11
+ if (Object.keys(value).length > 200)
12
+ ctx.addIssue({ code: "custom", message: "Model policy maps may contain at most 200 entries." });
13
+ });
14
+ var fallbacksSchema = z.record(policyModelIdSchema, z.array(policyModelIdSchema).max(20)).superRefine((value, ctx) => {
15
+ if (Object.keys(value).length > 200)
16
+ ctx.addIssue({ code: "custom", message: "Model policy fallback maps may contain at most 200 entries." });
17
+ });
18
+ var modelPolicySchema = z.object({
19
+ version: z.literal(1).default(1),
20
+ roles: z.object({
21
+ subagent: policyModelIdSchema.optional(),
22
+ fast: policyModelIdSchema.optional(),
23
+ planning: policyModelIdSchema.optional(),
24
+ review: policyModelIdSchema.optional(),
25
+ summary: policyModelIdSchema.optional(),
26
+ compaction: policyModelIdSchema.optional(),
27
+ weak: policyModelIdSchema.optional(),
28
+ editor: policyModelIdSchema.optional()
29
+ }).strict().optional(),
30
+ allowedModels: boundedModelList.optional(),
31
+ aliases: boundedModelMap.optional(),
32
+ fallbacks: fallbacksSchema.optional()
33
+ }).strict();
34
+ var routingDecisionSchema = z.enum(["allow", "alias", "reject", "fallback"]);
35
+ var routingEventRoleSchema = z.enum(["main", ...modelPolicyRoleSchema.options]);
36
+ var routingEventSchema = z.object({
37
+ at: z.string().datetime({ offset: true }),
38
+ requestId: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/),
39
+ requestedModel: policyModelIdSchema,
40
+ resolvedModel: policyModelIdSchema.optional(),
41
+ reportedModel: policyModelIdSchema.optional(),
42
+ decision: routingDecisionSchema,
43
+ role: routingEventRoleSchema.optional(),
44
+ reason: z.string().regex(/^[a-z][a-z0-9_]{0,63}$/).optional(),
45
+ upstreamStatus: z.number().int().min(100).max(599).optional()
46
+ }).strict();
47
+ var routingEventsSchema = z.array(routingEventSchema).max(1000);
48
+ // src/domain.ts
49
+ var harnessSchema = z2.enum(["claude", "codex", "grok", "opencode", "opencode2", "pi", "omp", "dsh", "cline", "hermes", "prime-agent", "gemini", "aider", "kilo"]);
50
+ var protocolSchema = z2.enum(["anthropic-messages", "openai-responses", "openai-chat", "gemini-generate-content"]);
51
+ var idSchema = z2.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/);
52
+ var label = z2.string().min(1).max(200);
53
+ var envRef = z2.string().regex(/^SWITCHER_PROVIDER_[A-Z0-9_]+$/);
8
54
  function endpoint(value) {
9
55
  let url;
10
56
  try {
@@ -17,73 +63,79 @@ function endpoint(value) {
17
63
  throw new Fault(400, "invalid_url", "URL must use HTTPS, contain no credentials/query/fragment, or use HTTP on loopback.");
18
64
  return url.href.replace(/\/+$/, "");
19
65
  }
20
- var urlSchema = z.string().max(2000).superRefine((v, ctx) => {
66
+ var urlSchema = z2.string().max(2000).superRefine((v, ctx) => {
21
67
  try {
22
68
  endpoint(v);
23
69
  } catch {
24
70
  ctx.addIssue({ code: "custom", message: "Invalid endpoint URL" });
25
71
  }
26
72
  }).transform(endpoint);
27
- var modelSchema = z.object({
28
- id: z.string().min(1).max(300),
73
+ var modelSchema = z2.object({
74
+ id: z2.string().min(1).max(300),
29
75
  name: label,
30
- description: z.string().max(8000).optional(),
31
- available: z.boolean().optional(),
32
- contextWindow: z.number().int().positive().optional(),
33
- maxOutputTokens: z.number().int().positive().optional(),
34
- inputModalities: z.array(z.string().max(50)).max(20).optional(),
35
- outputModalities: z.array(z.string().max(50)).max(20).optional(),
36
- supportedParameters: z.array(z.string().max(100)).max(100).optional()
76
+ description: z2.string().max(8000).optional(),
77
+ available: z2.boolean().optional(),
78
+ contextWindow: z2.number().int().positive().optional(),
79
+ maxOutputTokens: z2.number().int().positive().optional(),
80
+ inputModalities: z2.array(z2.string().max(50)).max(20).optional(),
81
+ outputModalities: z2.array(z2.string().max(50)).max(20).optional(),
82
+ supportedParameters: z2.array(z2.string().max(100)).max(100).optional(),
83
+ supportedGenerationMethods: z2.array(z2.string().min(1).max(100)).max(100).optional()
37
84
  }).strict();
38
- var providerInputSchema = z.object({
85
+ var providerInputSchema = z2.object({
39
86
  id: idSchema,
40
87
  name: label,
41
88
  baseUrl: urlSchema,
42
89
  protocol: protocolSchema,
43
90
  credentialEnv: envRef.optional(),
44
- authStyle: z.enum(["bearer", "x-api-key"]).default("bearer"),
91
+ authStyle: z2.enum(["bearer", "x-api-key", "api-key"]).default("bearer"),
45
92
  catalogBaseUrl: urlSchema.optional(),
46
- catalogFormat: z.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "none"]).optional(),
47
- catalogAuthStyle: z.enum(["bearer", "x-api-key", "none"]).optional(),
93
+ catalogFormat: z2.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "gemini", "none"]).optional(),
94
+ catalogAuthStyle: z2.enum(["bearer", "x-api-key", "api-key", "none"]).optional(),
48
95
  catalogCredentialEnv: envRef.optional(),
49
- catalogAccountId: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/).optional(),
50
- modelsPath: z.string().regex(/^[a-zA-Z0-9_/-]+$/).max(200).default("models"),
51
- manualModels: z.array(modelSchema).max(1e4).default([])
96
+ catalogAccountId: z2.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/).optional(),
97
+ modelsPath: z2.string().regex(/^[a-zA-Z0-9_/-]+$/).max(200).default("models"),
98
+ manualModels: z2.array(modelSchema).max(1e4).default([])
52
99
  }).strict().refine((p) => !p.modelsPath.split("/").includes("..") && !p.modelsPath.startsWith("/"), "modelsPath must be relative");
53
- var providerPresetSchema = z.object({
100
+ var providerPresetSchema = z2.object({
54
101
  id: idSchema,
55
102
  name: label,
56
103
  credentialEnv: envRef.optional(),
57
- credentialAliases: z.array(z.string().regex(/^[A-Z][A-Z0-9_]+$/)),
58
- protocols: z.array(z.object({
104
+ credentialAliases: z2.array(z2.string().regex(/^[A-Z][A-Z0-9_]+$/)),
105
+ protocols: z2.array(z2.object({
59
106
  protocol: protocolSchema,
60
107
  baseUrl: urlSchema.optional(),
61
- authStyle: z.enum(["bearer", "x-api-key"]),
108
+ authStyle: z2.enum(["bearer", "x-api-key", "api-key"]),
62
109
  catalogBaseUrl: urlSchema.optional(),
63
- catalogFormat: z.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "none"]),
64
- catalogAuthStyle: z.enum(["bearer", "x-api-key", "none"]).optional(),
65
- modelsPath: z.string(),
66
- notes: z.array(z.string())
110
+ catalogFormat: z2.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "gemini", "none"]),
111
+ catalogAuthStyle: z2.enum(["bearer", "x-api-key", "api-key", "none"]).optional(),
112
+ modelsPath: z2.string(),
113
+ notes: z2.array(z2.string())
67
114
  }).strict()).min(1),
68
- sources: z.array(z.string().url()),
69
- verification: z.literal("documented")
115
+ sources: z2.array(z2.string().url()),
116
+ verification: z2.literal("documented")
70
117
  }).strict();
71
- var profileInputSchema = z.object({
118
+ var profileInputSchema = z2.object({
72
119
  id: idSchema,
73
120
  name: label,
74
121
  providerId: idSchema,
75
122
  harness: harnessSchema,
76
- model: z.string().min(1).max(300)
123
+ model: z2.string().min(1).max(300),
124
+ modelPolicy: modelPolicySchema.optional()
77
125
  }).strict();
78
- var runInputSchema = z.object({
126
+ var runInputSchema = z2.object({
127
+ modelPolicyVersion: z2.literal(1),
79
128
  profileId: idSchema,
80
129
  harness: harnessSchema,
81
- model: z.string().min(1).max(300),
82
- planToken: z.string().regex(/^[a-f0-9]{64}$/)
130
+ model: z2.string().min(1).max(300),
131
+ modelPolicy: modelPolicySchema.optional(),
132
+ planToken: z2.string().regex(/^[a-f0-9]{64}$/)
83
133
  }).strict();
84
- var runUpdateSchema = z.object({
85
- status: z.enum(["exited", "failed", "interrupted"]),
86
- exitCode: z.number().int().min(0).max(255)
134
+ var runUpdateSchema = z2.object({
135
+ status: z2.enum(["exited", "failed", "interrupted"]),
136
+ exitCode: z2.number().int().min(0).max(255),
137
+ routingEvents: routingEventsSchema.optional(),
138
+ routingEventsDropped: z2.number().int().min(0).max(1e6).optional()
87
139
  }).strict();
88
140
 
89
141
  class Fault extends Error {
@@ -102,7 +154,15 @@ function parse(schema, value) {
102
154
  return result.data;
103
155
  }
104
156
  function compatible(harness, protocol) {
105
- return harness === "claude" ? protocol === "anthropic-messages" : harness === "codex" ? protocol === "openai-responses" : true;
157
+ if (harness === "claude")
158
+ return protocol === "anthropic-messages";
159
+ if (harness === "codex")
160
+ return protocol === "openai-responses";
161
+ if (harness === "gemini")
162
+ return protocol === "gemini-generate-content";
163
+ if (protocol === "gemini-generate-content")
164
+ return false;
165
+ return true;
106
166
  }
107
167
 
108
168
  // src/http.ts
@@ -168,7 +228,18 @@ var providerPresets = [
168
228
  ], ["https://api-docs.deepseek.com/guides/anthropic_api", "https://api-docs.deepseek.com/api/list-models"], "DEEPSEEK_API_KEY"),
169
229
  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"),
170
230
  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"),
231
+ preset("gemini", "Google Gemini", [route("gemini-generate-content", "https://generativelanguage.googleapis.com/v1beta", {
232
+ authStyle: "x-api-key",
233
+ catalogBaseUrl: "https://generativelanguage.googleapis.com/v1beta",
234
+ catalogFormat: "gemini",
235
+ catalogAuthStyle: "x-api-key",
236
+ notes: ["Gemini CLI uses the native generateContent wire with x-goog-api-key authentication; model IDs are returned as models/{id}."]
237
+ }), 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"),
171
238
  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"),
239
+ preset("azure-openai", "Azure OpenAI (v1)", [
240
+ 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."] }),
241
+ 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."] })
242
+ ], ["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"),
172
243
  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"),
173
244
  preset("ollama", "Ollama", ["openai-chat", "openai-responses"].map((protocol) => route(protocol, "http://127.0.0.1:11434/v1", {
174
245
  catalogBaseUrl: "http://127.0.0.1:11434",
@@ -229,6 +300,8 @@ function providerFromPreset(presetId, options = {}) {
229
300
  const baseUrl = options.baseUrl ?? selected.baseUrl;
230
301
  if (!baseUrl)
231
302
  throw new Fault(400, "endpoint_required", "This preset requires an explicit --url for its inference endpoint.");
303
+ if (presetId === "azure-openai" && !/\/openai\/v1$/.test(endpoint(baseUrl)))
304
+ throw new Fault(400, "invalid_url", "Azure OpenAI v1 requires an explicit endpoint ending in /openai/v1; deployment and api-version URLs are unsupported.");
232
305
  if (options.baseUrl && selected.baseUrl && new URL(endpoint(options.baseUrl)).origin !== new URL(selected.baseUrl).origin && preset2.credentialEnv && !options.credentialEnv)
233
306
  throw new Fault(422, "credential_authority", "An endpoint on another origin requires an explicit --credential-env reference.");
234
307
  const suffix = selected.protocol === "anthropic-messages" ? "messages" : selected.protocol === "openai-responses" ? "responses" : "chat";
@@ -265,6 +338,26 @@ class SwitcherError extends Error {
265
338
  this.requestId = requestId;
266
339
  }
267
340
  }
341
+ function apiError(status, data, apiKey) {
342
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
343
+ const error = object(data) && object(data.error) ? data.error : {};
344
+ const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
345
+ const base64 = Buffer.from(apiKey, "utf8").toString("base64");
346
+ const literal = [apiKey, JSON.stringify(apiKey).slice(1, -1), base64, base64.replace(/=+$/, ""), Buffer.from(apiKey, "utf8").toString("base64url")];
347
+ const encoded = [encodeURIComponent(apiKey), new URLSearchParams({ key: apiKey }).toString().slice(4)];
348
+ const patterns = [...new Set(literal)].sort((a, b) => b.length - a.length).map(escape);
349
+ for (const value of encoded) {
350
+ patterns.unshift(escape(value).replace(/%[0-9A-F]{2}/g, (part) => part.replace(/[A-F]/g, (letter) => `[${letter}${letter.toLowerCase()}]`)));
351
+ }
352
+ const reflected = new RegExp(patterns.join("|"), "g");
353
+ const redact = (value) => value.replace(reflected, "[REDACTED]");
354
+ const identifier = (value, pattern) => typeof value === "string" && pattern.test(value) && redact(value) === value ? value : undefined;
355
+ const code = identifier(error.code, /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/) ?? "api_error";
356
+ const requestId = identifier(error.requestId, /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/);
357
+ const fallback = `Switcher API returned HTTP ${status}.`;
358
+ const message = typeof error.message === "string" && error.message.length <= 4096 ? redact(error.message).replace(/[\x00-\x1f\x7f-\x9f]/g, " ").trim().slice(0, 2048) || fallback : fallback;
359
+ return new SwitcherError(status, code, message, requestId);
360
+ }
268
361
 
269
362
  class SwitcherClient {
270
363
  options;
@@ -301,7 +394,7 @@ class SwitcherClient {
301
394
  throw new SwitcherError(response.status, "invalid_response", "Switcher API returned invalid JSON.");
302
395
  }
303
396
  if (!response.ok)
304
- throw new SwitcherError(response.status, data?.error?.code ?? "api_error", data?.error?.message ?? `Switcher API returned HTTP ${response.status}.`, data?.error?.requestId);
397
+ throw apiError(response.status, data, apiKey);
305
398
  return data;
306
399
  }
307
400
  query(options = {}) {
@@ -368,7 +461,7 @@ class SwitcherClient {
368
461
  return this.request("GET", `/v1/runs/${encodeURIComponent(id)}`);
369
462
  }
370
463
  createRun(input, idempotencyKey) {
371
- return this.request("POST", "/v1/runs", input, { idempotencyKey });
464
+ return this.request("POST", "/v1/runs", { ...input, modelPolicyVersion: 1 }, { idempotencyKey });
372
465
  }
373
466
  finishRun(id, version, input, idempotencyKey) {
374
467
  return this.request("PATCH", `/v1/runs/${encodeURIComponent(id)}`, input, { version, idempotencyKey });