@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.
- package/README.md +146 -7
- package/dist/aider-args.d.ts +7 -0
- package/dist/aider-config.d.ts +6 -0
- package/dist/auth.d.ts +3 -0
- package/dist/cli/index.js +7548 -2558
- package/dist/cli.d.ts +2 -0
- package/dist/cline-backend.d.ts +7 -0
- package/dist/codex-model-policy.d.ts +65 -0
- package/dist/credentials.d.ts +5 -5
- package/dist/direct-launch.d.ts +3 -3
- package/dist/domain.d.ts +320 -50
- package/dist/dsh-args.d.ts +5 -0
- package/dist/gemini-bridge.d.ts +6 -0
- package/dist/gemini-config.d.ts +12 -0
- package/dist/gemini-model-policy.d.ts +56 -0
- package/dist/generated/api.d.ts +220 -21
- package/dist/harness-arguments.d.ts +1 -0
- package/dist/harness-installation.d.ts +19 -0
- package/dist/harness-types.d.ts +10 -0
- package/dist/harnesses.d.ts +5 -2
- package/dist/hermes-backend.d.ts +19 -0
- package/dist/hermes-model-policy.d.ts +30 -0
- package/dist/index.js +136 -43
- package/dist/inference-gateway.d.ts +24 -0
- package/dist/kilo-config.d.ts +13 -0
- package/dist/kilo.d.ts +5 -0
- package/dist/launcher.d.ts +12 -6
- package/dist/mcp/index.js +139 -55
- package/dist/model-policy-schema.d.ts +137 -0
- package/dist/model-policy.d.ts +31 -0
- package/dist/native-model-policy.d.ts +23 -0
- package/dist/omp-backend.d.ts +7 -0
- package/dist/opencode-model-policy.d.ts +33 -0
- package/dist/opencode2-config.d.ts +3 -3
- package/dist/ori-backend.d.ts +2 -2
- package/dist/ori-model-policy.d.ts +8 -0
- package/dist/presets.d.ts +11 -10
- package/dist/sdk.d.ts +279 -38
- package/dist/sdk.js +136 -43
- package/dist/serve/index.js +1306 -125
- package/docs/MODEL-POLICY.md +82 -0
- package/openapi.json +973 -14
- 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
|
|
4
|
-
var
|
|
5
|
-
var
|
|
6
|
-
var
|
|
7
|
-
var
|
|
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 =
|
|
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 =
|
|
28
|
-
id:
|
|
73
|
+
var modelSchema = z2.object({
|
|
74
|
+
id: z2.string().min(1).max(300),
|
|
29
75
|
name: label,
|
|
30
|
-
description:
|
|
31
|
-
available:
|
|
32
|
-
contextWindow:
|
|
33
|
-
maxOutputTokens:
|
|
34
|
-
inputModalities:
|
|
35
|
-
outputModalities:
|
|
36
|
-
supportedParameters:
|
|
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 =
|
|
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:
|
|
91
|
+
authStyle: z2.enum(["bearer", "x-api-key", "api-key"]).default("bearer"),
|
|
45
92
|
catalogBaseUrl: urlSchema.optional(),
|
|
46
|
-
catalogFormat:
|
|
47
|
-
catalogAuthStyle:
|
|
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:
|
|
50
|
-
modelsPath:
|
|
51
|
-
manualModels:
|
|
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 =
|
|
100
|
+
var providerPresetSchema = z2.object({
|
|
54
101
|
id: idSchema,
|
|
55
102
|
name: label,
|
|
56
103
|
credentialEnv: envRef.optional(),
|
|
57
|
-
credentialAliases:
|
|
58
|
-
protocols:
|
|
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:
|
|
108
|
+
authStyle: z2.enum(["bearer", "x-api-key", "api-key"]),
|
|
62
109
|
catalogBaseUrl: urlSchema.optional(),
|
|
63
|
-
catalogFormat:
|
|
64
|
-
catalogAuthStyle:
|
|
65
|
-
modelsPath:
|
|
66
|
-
notes:
|
|
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:
|
|
69
|
-
verification:
|
|
115
|
+
sources: z2.array(z2.string().url()),
|
|
116
|
+
verification: z2.literal("documented")
|
|
70
117
|
}).strict();
|
|
71
|
-
var profileInputSchema =
|
|
118
|
+
var profileInputSchema = z2.object({
|
|
72
119
|
id: idSchema,
|
|
73
120
|
name: label,
|
|
74
121
|
providerId: idSchema,
|
|
75
122
|
harness: harnessSchema,
|
|
76
|
-
model:
|
|
123
|
+
model: z2.string().min(1).max(300),
|
|
124
|
+
modelPolicy: modelPolicySchema.optional()
|
|
77
125
|
}).strict();
|
|
78
|
-
var runInputSchema =
|
|
126
|
+
var runInputSchema = z2.object({
|
|
127
|
+
modelPolicyVersion: z2.literal(1),
|
|
79
128
|
profileId: idSchema,
|
|
80
129
|
harness: harnessSchema,
|
|
81
|
-
model:
|
|
82
|
-
|
|
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 =
|
|
85
|
-
status:
|
|
86
|
-
exitCode:
|
|
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
|
-
|
|
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
|
|
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 });
|