@hasna/switcher 0.1.0 → 0.1.2
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 +210 -13
- 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/catalog.d.ts +3 -2
- package/dist/cli/index.js +8558 -337
- package/dist/cline-backend.d.ts +7 -0
- package/dist/credentials.d.ts +178 -0
- package/dist/direct-launch.d.ts +6 -0
- package/dist/domain.d.ts +142 -19
- 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/generated/api.d.ts +163 -11
- package/dist/grok-args.d.ts +1 -0
- package/dist/harness-arguments.d.ts +6 -0
- package/dist/harness-environment.d.ts +1 -0
- package/dist/harness-installation.d.ts +19 -0
- package/dist/harness-process.d.ts +11 -0
- package/dist/harness-types.d.ts +2 -0
- package/dist/harnesses.d.ts +36 -2
- package/dist/hermes-backend.d.ts +19 -0
- package/dist/index.js +209 -6
- package/dist/kilo-config.d.ts +13 -0
- package/dist/kilo.d.ts +5 -0
- package/dist/launcher.d.ts +39 -3
- package/dist/mcp/index.js +159 -7
- package/dist/omp-backend.d.ts +7 -0
- package/dist/opencode2-config.d.ts +60 -0
- package/dist/ori-backend.d.ts +89 -0
- package/dist/presets.d.ts +64 -0
- package/dist/runtime.d.ts +20 -0
- package/dist/sdk.d.ts +95 -19
- package/dist/sdk.js +209 -6
- package/dist/serve/index.js +909 -98
- package/dist/server.d.ts +16 -0
- package/dist/service.d.ts +2 -1
- package/dist/terminal-descriptors.d.ts +5 -0
- package/hasna.contract.json +1 -1
- package/openapi.json +450 -12
- package/package.json +19 -4
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/domain.ts
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
var harnessSchema = z.enum(["claude", "codex", "grok", "opencode2"]);
|
|
4
|
-
var protocolSchema = z.enum(["anthropic-messages", "openai-responses", "openai-chat"]);
|
|
3
|
+
var harnessSchema = z.enum(["claude", "codex", "grok", "opencode", "opencode2", "pi", "omp", "dsh", "cline", "hermes", "prime-agent", "gemini", "aider", "kilo"]);
|
|
4
|
+
var protocolSchema = z.enum(["anthropic-messages", "openai-responses", "openai-chat", "gemini-generate-content"]);
|
|
5
5
|
var idSchema = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/);
|
|
6
6
|
var label = z.string().min(1).max(200);
|
|
7
7
|
var envRef = z.string().regex(/^SWITCHER_PROVIDER_[A-Z0-9_]+$/);
|
|
@@ -28,11 +28,13 @@ var modelSchema = z.object({
|
|
|
28
28
|
id: z.string().min(1).max(300),
|
|
29
29
|
name: label,
|
|
30
30
|
description: z.string().max(8000).optional(),
|
|
31
|
+
available: z.boolean().optional(),
|
|
31
32
|
contextWindow: z.number().int().positive().optional(),
|
|
32
33
|
maxOutputTokens: z.number().int().positive().optional(),
|
|
33
34
|
inputModalities: z.array(z.string().max(50)).max(20).optional(),
|
|
34
35
|
outputModalities: z.array(z.string().max(50)).max(20).optional(),
|
|
35
|
-
supportedParameters: z.array(z.string().max(100)).max(100).optional()
|
|
36
|
+
supportedParameters: z.array(z.string().max(100)).max(100).optional(),
|
|
37
|
+
supportedGenerationMethods: z.array(z.string().min(1).max(100)).max(100).optional()
|
|
36
38
|
}).strict();
|
|
37
39
|
var providerInputSchema = z.object({
|
|
38
40
|
id: idSchema,
|
|
@@ -40,10 +42,33 @@ var providerInputSchema = z.object({
|
|
|
40
42
|
baseUrl: urlSchema,
|
|
41
43
|
protocol: protocolSchema,
|
|
42
44
|
credentialEnv: envRef.optional(),
|
|
43
|
-
authStyle: z.enum(["bearer", "x-api-key"]).default("bearer"),
|
|
45
|
+
authStyle: z.enum(["bearer", "x-api-key", "api-key"]).default("bearer"),
|
|
46
|
+
catalogBaseUrl: urlSchema.optional(),
|
|
47
|
+
catalogFormat: z.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "gemini", "none"]).optional(),
|
|
48
|
+
catalogAuthStyle: z.enum(["bearer", "x-api-key", "api-key", "none"]).optional(),
|
|
49
|
+
catalogCredentialEnv: envRef.optional(),
|
|
50
|
+
catalogAccountId: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/).optional(),
|
|
44
51
|
modelsPath: z.string().regex(/^[a-zA-Z0-9_/-]+$/).max(200).default("models"),
|
|
45
52
|
manualModels: z.array(modelSchema).max(1e4).default([])
|
|
46
53
|
}).strict().refine((p) => !p.modelsPath.split("/").includes("..") && !p.modelsPath.startsWith("/"), "modelsPath must be relative");
|
|
54
|
+
var providerPresetSchema = z.object({
|
|
55
|
+
id: idSchema,
|
|
56
|
+
name: label,
|
|
57
|
+
credentialEnv: envRef.optional(),
|
|
58
|
+
credentialAliases: z.array(z.string().regex(/^[A-Z][A-Z0-9_]+$/)),
|
|
59
|
+
protocols: z.array(z.object({
|
|
60
|
+
protocol: protocolSchema,
|
|
61
|
+
baseUrl: urlSchema.optional(),
|
|
62
|
+
authStyle: z.enum(["bearer", "x-api-key", "api-key"]),
|
|
63
|
+
catalogBaseUrl: urlSchema.optional(),
|
|
64
|
+
catalogFormat: z.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "gemini", "none"]),
|
|
65
|
+
catalogAuthStyle: z.enum(["bearer", "x-api-key", "api-key", "none"]).optional(),
|
|
66
|
+
modelsPath: z.string(),
|
|
67
|
+
notes: z.array(z.string())
|
|
68
|
+
}).strict()).min(1),
|
|
69
|
+
sources: z.array(z.string().url()),
|
|
70
|
+
verification: z.literal("documented")
|
|
71
|
+
}).strict();
|
|
47
72
|
var profileInputSchema = z.object({
|
|
48
73
|
id: idSchema,
|
|
49
74
|
name: label,
|
|
@@ -71,6 +96,23 @@ class Fault extends Error {
|
|
|
71
96
|
this.code = code;
|
|
72
97
|
}
|
|
73
98
|
}
|
|
99
|
+
function parse(schema, value) {
|
|
100
|
+
const result = schema.safeParse(value);
|
|
101
|
+
if (!result.success)
|
|
102
|
+
throw new Fault(400, "invalid_request", result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; "));
|
|
103
|
+
return result.data;
|
|
104
|
+
}
|
|
105
|
+
function compatible(harness, protocol) {
|
|
106
|
+
if (harness === "claude")
|
|
107
|
+
return protocol === "anthropic-messages";
|
|
108
|
+
if (harness === "codex")
|
|
109
|
+
return protocol === "openai-responses";
|
|
110
|
+
if (harness === "gemini")
|
|
111
|
+
return protocol === "gemini-generate-content";
|
|
112
|
+
if (protocol === "gemini-generate-content")
|
|
113
|
+
return false;
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
74
116
|
|
|
75
117
|
// src/http.ts
|
|
76
118
|
var MAX_BYTES = 16 * 1024 * 1024;
|
|
@@ -109,6 +151,131 @@ async function boundedJson(response, maxBytes = MAX_BYTES) {
|
|
|
109
151
|
// src/sdk.ts
|
|
110
152
|
import { resolveCredential } from "@hasna/contracts/client";
|
|
111
153
|
|
|
154
|
+
// src/presets.ts
|
|
155
|
+
var route = (protocol, baseUrl, options = {}) => ({
|
|
156
|
+
protocol,
|
|
157
|
+
baseUrl,
|
|
158
|
+
authStyle: "bearer",
|
|
159
|
+
catalogFormat: "openai",
|
|
160
|
+
modelsPath: "models",
|
|
161
|
+
notes: [],
|
|
162
|
+
...options
|
|
163
|
+
});
|
|
164
|
+
var preset = (id, name, protocols, sources, alias) => parse(providerPresetSchema, {
|
|
165
|
+
id,
|
|
166
|
+
name,
|
|
167
|
+
protocols,
|
|
168
|
+
sources,
|
|
169
|
+
credentialAliases: alias ? [alias] : [],
|
|
170
|
+
credentialEnv: alias ? `SWITCHER_PROVIDER_${id.toUpperCase().replace(/-/g, "_")}` : undefined,
|
|
171
|
+
verification: "documented"
|
|
172
|
+
});
|
|
173
|
+
var providerPresets = [
|
|
174
|
+
preset("deepseek", "DeepSeek", [
|
|
175
|
+
route("openai-chat", "https://api.deepseek.com", { catalogBaseUrl: "https://api.deepseek.com" }),
|
|
176
|
+
route("anthropic-messages", "https://api.deepseek.com/anthropic/v1", { catalogBaseUrl: "https://api.deepseek.com" })
|
|
177
|
+
], ["https://api-docs.deepseek.com/guides/anthropic_api", "https://api-docs.deepseek.com/api/list-models"], "DEEPSEEK_API_KEY"),
|
|
178
|
+
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"),
|
|
179
|
+
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"),
|
|
180
|
+
preset("gemini", "Google Gemini", [route("gemini-generate-content", "https://generativelanguage.googleapis.com/v1beta", {
|
|
181
|
+
authStyle: "x-api-key",
|
|
182
|
+
catalogBaseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
|
183
|
+
catalogFormat: "gemini",
|
|
184
|
+
catalogAuthStyle: "x-api-key",
|
|
185
|
+
notes: ["Gemini CLI uses the native generateContent wire with x-goog-api-key authentication; model IDs are returned as models/{id}."]
|
|
186
|
+
}), 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"),
|
|
187
|
+
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"),
|
|
188
|
+
preset("azure-openai", "Azure OpenAI (v1)", [
|
|
189
|
+
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."] }),
|
|
190
|
+
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."] })
|
|
191
|
+
], ["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"),
|
|
192
|
+
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"),
|
|
193
|
+
preset("ollama", "Ollama", ["openai-chat", "openai-responses"].map((protocol) => route(protocol, "http://127.0.0.1:11434/v1", {
|
|
194
|
+
catalogBaseUrl: "http://127.0.0.1:11434",
|
|
195
|
+
modelsPath: "api/tags",
|
|
196
|
+
catalogFormat: "ollama",
|
|
197
|
+
catalogAuthStyle: "none",
|
|
198
|
+
notes: protocol === "openai-responses" ? ["Requires Ollama 0.13.3 or newer; only stateless Responses are supported."] : []
|
|
199
|
+
})), ["https://docs.ollama.com/api/openai-compatibility", "https://docs.ollama.com/api/tags"]),
|
|
200
|
+
preset("lmstudio", "LM Studio", ["openai-chat", "openai-responses", "anthropic-messages"].map((protocol) => route(protocol, "http://127.0.0.1:1234/v1", {
|
|
201
|
+
notes: ["Server authentication is optional. Use --credential-env when authentication is enabled."]
|
|
202
|
+
})), ["https://lmstudio.ai/docs/developer/rest"]),
|
|
203
|
+
preset("vllm", "vLLM (operator endpoint)", [
|
|
204
|
+
route("openai-chat", undefined, { notes: ["Pass the operator's OpenAI-compatible URL, normally ending in /v1. vLLM exposes Chat Completions at /chat/completions and GET /models under that prefix; configure --credential-env only when the server was started with --api-key or VLLM_API_KEY."] }),
|
|
205
|
+
route("openai-responses", undefined, { notes: ["Pass the operator's OpenAI-compatible URL, normally ending in /v1. Responses is available for supported text-generation models at /responses; configure --credential-env only when the server was started with --api-key or VLLM_API_KEY."] }),
|
|
206
|
+
route("anthropic-messages", undefined, { notes: ["Pass the operator's URL, normally ending in /v1. vLLM exposes the Anthropic Messages API at /messages for supported deployments. Chat templates and the configured tool parser determine whether streaming and tool calls work for a served model; configure --credential-env only when the server was started with --api-key or VLLM_API_KEY."] })
|
|
207
|
+
], ["https://docs.vllm.ai/en/latest/serving/online_serving/openai_compatible_server/", "https://github.com/vllm-project/vllm/blob/main/docs/serving/online_serving/README.md"]),
|
|
208
|
+
preset("litellm", "LiteLLM Proxy (operator endpoint)", [
|
|
209
|
+
route("openai-chat", undefined, { notes: ["Pass the proxy's documented OpenAI-compatible base URL exactly; the official quick start uses the root server URL, while a deployment may add a prefix such as /v1. LiteLLM serves Chat Completions and GET /models relative to that URL; use --credential-env for the proxy's configured master key or other bearer token."] }),
|
|
210
|
+
route("openai-responses", undefined, { notes: ["Pass the proxy's documented OpenAI-compatible base URL exactly; LiteLLM documents the Responses API under the same proxy root or deployment prefix. Use --credential-env for the proxy's configured master key or other bearer token."] }),
|
|
211
|
+
route("anthropic-messages", undefined, { notes: ["Pass the complete inference prefix ending in /v1, including any deployment prefix. LiteLLM registers /v1/messages; Switcher appends /messages to the stored prefix and discovers /models there. This is a gateway adapter: streaming and tool behavior depend on the configured upstream model and route, so verify those capabilities independently. Use --credential-env for the proxy's configured master key or other bearer token."] })
|
|
212
|
+
], ["https://docs.litellm.ai/", "https://docs.litellm.ai/docs/proxy/quick_start", "https://github.com/BerriAI/litellm/blob/main/litellm/proxy/anthropic_endpoints/endpoints.py"]),
|
|
213
|
+
preset("groq", "Groq", [route("openai-chat", "https://api.groq.com/openai/v1"), route("openai-responses", "https://api.groq.com/openai/v1", { notes: ["Responses is an upstream beta API."] })], ["https://console.groq.com/docs/api-reference"], "GROQ_API_KEY"),
|
|
214
|
+
preset("cerebras", "Cerebras", [route("openai-chat", "https://api.cerebras.ai/v1")], ["https://inference-docs.cerebras.ai/api-reference/chat-completions"], "CEREBRAS_API_KEY"),
|
|
215
|
+
preset("mistral", "Mistral", [route("openai-chat", "https://api.mistral.ai/v1", { catalogFormat: "mistral" })], ["https://docs.mistral.ai/api/endpoint/chat", "https://docs.mistral.ai/api/endpoint/models"], "MISTRAL_API_KEY"),
|
|
216
|
+
preset("together", "Together AI", [route("openai-chat", "https://api.together.ai/v1", { catalogFormat: "together" })], ["https://docs.together.ai/docs/inference/openai-compatibility", "https://docs.together.ai/reference/models"], "TOGETHER_API_KEY"),
|
|
217
|
+
preset("fireworks", "Fireworks AI", [
|
|
218
|
+
route("openai-chat", "https://api.fireworks.ai/inference/v1", { catalogFormat: "fireworks", notes: ["Model discovery uses GET /v1/accounts/{account_id}/models; provide --catalog-account-id or --catalog-url."] }),
|
|
219
|
+
route("openai-responses", "https://api.fireworks.ai/inference/v1", { catalogFormat: "fireworks", notes: ["Model discovery uses GET /v1/accounts/{account_id}/models; provide --catalog-account-id or --catalog-url."] }),
|
|
220
|
+
route("anthropic-messages", "https://api.fireworks.ai/inference/v1", { catalogFormat: "fireworks", notes: ["Model discovery uses GET /v1/accounts/{account_id}/models; provide --catalog-account-id or --catalog-url."] })
|
|
221
|
+
], ["https://docs.fireworks.ai/getting-started/quickstart", "https://docs.fireworks.ai/tools-sdks/python-client/api-reference", "https://docs.fireworks.ai/api-reference/anthropic-messages", "https://docs.fireworks.ai/api-reference/post-chatcompletions", "https://docs.fireworks.ai/api-reference/list-models"], "FIREWORKS_API_KEY"),
|
|
222
|
+
preset("moonshot", "Moonshot AI (Kimi)", [route("openai-chat", "https://api.moonshot.ai/v1", { catalogBaseUrl: "https://api.moonshot.ai/v1" })], ["https://platform.kimi.ai/docs/api/chat", "https://platform.kimi.ai/docs/api/list-models"], "MOONSHOT_API_KEY"),
|
|
223
|
+
preset("dashscope", "Alibaba Cloud Model Studio (Qwen)", [route("openai-chat", "https://dashscope-us.aliyuncs.com/compatible-mode/v1", {
|
|
224
|
+
catalogFormat: "none",
|
|
225
|
+
notes: ["Inference keys and endpoints are region/workspace-specific. Model discovery uses GET /api/v1/models on a documented region or workspace catalog URL; pass --catalog-url and --catalog-format dashscope."]
|
|
226
|
+
})], ["https://help.aliyun.com/en/model-studio/base-url", "https://help.aliyun.com/en/model-studio/compatibility-of-openai-with-dashscope", "https://help.aliyun.com/en/model-studio/list-models"], "DASHSCOPE_API_KEY"),
|
|
227
|
+
preset("zai", "Z.AI", [route("openai-chat", "https://api.z.ai/api/paas/v4", {
|
|
228
|
+
catalogFormat: "none",
|
|
229
|
+
notes: ["The published API reference documents inference endpoints but no model-list endpoint; use manual models or provide an explicit catalog URL and parser."]
|
|
230
|
+
})], ["https://docs.z.ai/api-reference/introduction", "https://docs.z.ai/devpack/quick-start"], "ZAI_API_KEY"),
|
|
231
|
+
preset("minimax", "MiniMax", [
|
|
232
|
+
route("openai-chat", "https://api.minimax.cn/v1", { catalogBaseUrl: "https://api.minimax.cn/v1", notes: ["The Open Platform contract uses api.minimax.cn and Bearer auth. Token Plan documentation uses api.minimaxi.com; select that authority explicitly with --url and matching auth/credential settings."] }),
|
|
233
|
+
route("anthropic-messages", "https://api.minimax.cn/anthropic/v1", { authStyle: "x-api-key", catalogBaseUrl: "https://api.minimax.cn/anthropic/v1", catalogAuthStyle: "x-api-key", notes: ["The Open Platform contract uses api.minimax.cn/anthropic/v1 and X-Api-Key. Token Plan documentation uses api.minimaxi.com/anthropic; select that authority explicitly with --url and matching auth/credential settings."] })
|
|
234
|
+
], ["https://platform.minimaxi.com/docs/api-reference/text-chat-anthropic", "https://platform.minimaxi.com/docs/api-reference/models/anthropic/list-models", "https://platform.minimaxi.com/docs/api-reference/models/openai/list-models", "https://platform.minimaxi.com/docs/token-plan/other-tools"], "MINIMAX_API_KEY"),
|
|
235
|
+
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"),
|
|
236
|
+
...["anthropic-messages", "openai-responses", "openai-chat"].map((protocol) => preset(`generic-${protocol}`, `Custom ${protocol}`, [route(protocol)], []))
|
|
237
|
+
];
|
|
238
|
+
function getProviderPreset(id) {
|
|
239
|
+
const entry = providerPresets.find((p) => p.id === id);
|
|
240
|
+
if (!entry)
|
|
241
|
+
throw new Fault(404, "preset_not_found", "Unknown provider preset. Use switcher providers presets to list available presets.");
|
|
242
|
+
return structuredClone(entry);
|
|
243
|
+
}
|
|
244
|
+
function providerFromPreset(presetId, options = {}) {
|
|
245
|
+
const preset2 = getProviderPreset(presetId);
|
|
246
|
+
const selected = preset2.protocols.find((p) => (!options.protocol || p.protocol === options.protocol) && (!options.harness || compatible(options.harness, p.protocol)));
|
|
247
|
+
if (!selected)
|
|
248
|
+
throw new Fault(422, "protocol_mismatch", "This provider preset has no native protocol compatible with the requested harness. Choose an explicitly compatible gateway.");
|
|
249
|
+
const baseUrl = options.baseUrl ?? selected.baseUrl;
|
|
250
|
+
if (!baseUrl)
|
|
251
|
+
throw new Fault(400, "endpoint_required", "This preset requires an explicit --url for its inference endpoint.");
|
|
252
|
+
if (presetId === "azure-openai" && !/\/openai\/v1$/.test(endpoint(baseUrl)))
|
|
253
|
+
throw new Fault(400, "invalid_url", "Azure OpenAI v1 requires an explicit endpoint ending in /openai/v1; deployment and api-version URLs are unsupported.");
|
|
254
|
+
if (options.baseUrl && selected.baseUrl && new URL(endpoint(options.baseUrl)).origin !== new URL(selected.baseUrl).origin && preset2.credentialEnv && !options.credentialEnv)
|
|
255
|
+
throw new Fault(422, "credential_authority", "An endpoint on another origin requires an explicit --credential-env reference.");
|
|
256
|
+
const suffix = selected.protocol === "anthropic-messages" ? "messages" : selected.protocol === "openai-responses" ? "responses" : "chat";
|
|
257
|
+
if (presetId === "fireworks" && !options.catalogBaseUrl && !options.catalogAccountId)
|
|
258
|
+
throw new Fault(400, "catalog_account_required", "Fireworks model discovery requires --catalog-account-id or an explicit --catalog-url.");
|
|
259
|
+
if (selected.catalogFormat === "none" && options.catalogFormat && !options.catalogBaseUrl)
|
|
260
|
+
throw new Fault(400, "catalog_url_required", "This preset requires an explicit --catalog-url when enabling a catalog parser.");
|
|
261
|
+
const catalogBaseUrl = options.catalogBaseUrl ?? (presetId === "fireworks" && options.catalogAccountId ? `https://api.fireworks.ai/v1/accounts/${encodeURIComponent(options.catalogAccountId)}` : options.baseUrl ? undefined : selected.catalogBaseUrl);
|
|
262
|
+
return parse(providerInputSchema, {
|
|
263
|
+
id: options.id ?? `${preset2.id}-${suffix}`,
|
|
264
|
+
name: preset2.name,
|
|
265
|
+
baseUrl,
|
|
266
|
+
protocol: selected.protocol,
|
|
267
|
+
credentialEnv: options.credentialEnv ?? preset2.credentialEnv,
|
|
268
|
+
authStyle: options.authStyle ?? selected.authStyle,
|
|
269
|
+
catalogBaseUrl,
|
|
270
|
+
catalogCredentialEnv: options.catalogCredentialEnv,
|
|
271
|
+
catalogAuthStyle: options.catalogAuthStyle ?? selected.catalogAuthStyle,
|
|
272
|
+
catalogFormat: options.catalogFormat ?? selected.catalogFormat,
|
|
273
|
+
catalogAccountId: options.catalogAccountId,
|
|
274
|
+
modelsPath: options.modelsPath ?? selected.modelsPath
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/sdk.ts
|
|
112
279
|
class SwitcherError extends Error {
|
|
113
280
|
status;
|
|
114
281
|
code;
|
|
@@ -120,6 +287,26 @@ class SwitcherError extends Error {
|
|
|
120
287
|
this.requestId = requestId;
|
|
121
288
|
}
|
|
122
289
|
}
|
|
290
|
+
function apiError(status, data, apiKey) {
|
|
291
|
+
const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
292
|
+
const error = object(data) && object(data.error) ? data.error : {};
|
|
293
|
+
const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
294
|
+
const base64 = Buffer.from(apiKey, "utf8").toString("base64");
|
|
295
|
+
const literal = [apiKey, JSON.stringify(apiKey).slice(1, -1), base64, base64.replace(/=+$/, ""), Buffer.from(apiKey, "utf8").toString("base64url")];
|
|
296
|
+
const encoded = [encodeURIComponent(apiKey), new URLSearchParams({ key: apiKey }).toString().slice(4)];
|
|
297
|
+
const patterns = [...new Set(literal)].sort((a, b) => b.length - a.length).map(escape);
|
|
298
|
+
for (const value of encoded) {
|
|
299
|
+
patterns.unshift(escape(value).replace(/%[0-9A-F]{2}/g, (part) => part.replace(/[A-F]/g, (letter) => `[${letter}${letter.toLowerCase()}]`)));
|
|
300
|
+
}
|
|
301
|
+
const reflected = new RegExp(patterns.join("|"), "g");
|
|
302
|
+
const redact = (value) => value.replace(reflected, "[REDACTED]");
|
|
303
|
+
const identifier = (value, pattern) => typeof value === "string" && pattern.test(value) && redact(value) === value ? value : undefined;
|
|
304
|
+
const code = identifier(error.code, /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/) ?? "api_error";
|
|
305
|
+
const requestId = identifier(error.requestId, /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/);
|
|
306
|
+
const fallback = `Switcher API returned HTTP ${status}.`;
|
|
307
|
+
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;
|
|
308
|
+
return new SwitcherError(status, code, message, requestId);
|
|
309
|
+
}
|
|
123
310
|
|
|
124
311
|
class SwitcherClient {
|
|
125
312
|
options;
|
|
@@ -131,7 +318,7 @@ class SwitcherClient {
|
|
|
131
318
|
this.options = { ...options };
|
|
132
319
|
}
|
|
133
320
|
async request(method, path, body, options = {}) {
|
|
134
|
-
if (!/^\/v1\/[a-zA-Z0-9/?&=._%+-]+$/.test(path) || path.includes(".."))
|
|
321
|
+
if (!/^\/v1\/[a-zA-Z0-9/?&=._%+-]+$/.test(path) && !["/health", "/ready", "/version"].includes(path) || path.includes(".."))
|
|
135
322
|
throw new Error("Invalid API path.");
|
|
136
323
|
const apiKey = typeof this.options.apiKey === "function" ? this.options.apiKey() : this.options.apiKey;
|
|
137
324
|
if (!apiKey || /[\r\n]/.test(apiKey))
|
|
@@ -156,12 +343,27 @@ class SwitcherClient {
|
|
|
156
343
|
throw new SwitcherError(response.status, "invalid_response", "Switcher API returned invalid JSON.");
|
|
157
344
|
}
|
|
158
345
|
if (!response.ok)
|
|
159
|
-
throw
|
|
346
|
+
throw apiError(response.status, data, apiKey);
|
|
160
347
|
return data;
|
|
161
348
|
}
|
|
162
349
|
query(options = {}) {
|
|
163
350
|
return new URLSearchParams(Object.entries(options).filter(([, v]) => v !== undefined).map(([k, v]) => [k, String(v)])).toString();
|
|
164
351
|
}
|
|
352
|
+
health() {
|
|
353
|
+
return this.request("GET", "/health");
|
|
354
|
+
}
|
|
355
|
+
ready() {
|
|
356
|
+
return this.request("GET", "/ready");
|
|
357
|
+
}
|
|
358
|
+
version() {
|
|
359
|
+
return this.request("GET", "/version");
|
|
360
|
+
}
|
|
361
|
+
listProviderPresets() {
|
|
362
|
+
return this.request("GET", "/v1/provider-presets");
|
|
363
|
+
}
|
|
364
|
+
getProviderPreset(id) {
|
|
365
|
+
return this.request("GET", `/v1/provider-presets/${encodeURIComponent(id)}`);
|
|
366
|
+
}
|
|
165
367
|
listProviders(options = {}) {
|
|
166
368
|
return this.request("GET", `/v1/providers?${this.query(options)}`);
|
|
167
369
|
}
|
|
@@ -221,6 +423,7 @@ function clientFromEnv(env = process.env) {
|
|
|
221
423
|
return new SwitcherClient({ baseUrl: env.HASNA_SWITCHER_API_URL, apiKey: credential });
|
|
222
424
|
}
|
|
223
425
|
export {
|
|
426
|
+
providerFromPreset,
|
|
224
427
|
clientFromEnv,
|
|
225
428
|
SwitcherError,
|
|
226
429
|
SwitcherClient
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type ObjectValue = Record<string, unknown>;
|
|
2
|
+
/** Read native policies as data; native config inspection itself may write project files. */
|
|
3
|
+
export declare function kiloSettings(cwd: string, stateDir: string, env?: NodeJS.ProcessEnv, options?: {
|
|
4
|
+
writeInstructions?: boolean;
|
|
5
|
+
}): Promise<{
|
|
6
|
+
config: ObjectValue;
|
|
7
|
+
global: ObjectValue;
|
|
8
|
+
instructions: string[];
|
|
9
|
+
sourcePaths: string[];
|
|
10
|
+
}>;
|
|
11
|
+
/** Kilo substitutes variables before parsing JSON; only the generated key reference is active. */
|
|
12
|
+
export declare function kiloConfigText(config: Record<string, unknown>, providerID?: string, keyEnv?: string): string;
|
|
13
|
+
export {};
|
package/dist/kilo.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { HarnessLaunchInput, PreparedLaunch } from "./harness-types";
|
|
2
|
+
export declare function validateKiloArgs(args: string[]): void;
|
|
3
|
+
/** Validate native policy and instruction inputs without credentials or generated files. */
|
|
4
|
+
export declare function validateKiloConfiguration(cwd: string, args?: string[]): Promise<void>;
|
|
5
|
+
export declare function prepareKilo(input: HarnessLaunchInput, providerBaseUrl: string): Promise<PreparedLaunch>;
|
package/dist/launcher.d.ts
CHANGED
|
@@ -1,9 +1,45 @@
|
|
|
1
1
|
import { SwitcherClient } from "./sdk";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { type LaunchPlan, type ProviderInput } from "./domain";
|
|
3
|
+
import { type OriContract, type OriLaunchPlan } from "./ori-backend";
|
|
4
|
+
export { childEnvironment } from "./harness-environment";
|
|
5
|
+
export type LaunchBackend = "direct" | "ori";
|
|
6
|
+
export type LaunchOptions = {
|
|
7
|
+
backend?: LaunchBackend;
|
|
8
|
+
oriExecutable?: string;
|
|
4
9
|
cwd?: string;
|
|
5
10
|
executable?: string;
|
|
6
11
|
stateDir?: string;
|
|
7
12
|
args?: string[];
|
|
8
13
|
timeoutMs?: number;
|
|
9
|
-
|
|
14
|
+
refresh?: boolean;
|
|
15
|
+
credentialEnv?: NodeJS.ProcessEnv;
|
|
16
|
+
resolveCredential?: (provider: ProviderInput) => Promise<string | undefined>;
|
|
17
|
+
};
|
|
18
|
+
type OriPreparationOptions = Pick<LaunchOptions, "oriExecutable" | "args" | "resolveCredential" | "credentialEnv"> & {
|
|
19
|
+
stateDir?: string;
|
|
20
|
+
cwd?: string;
|
|
21
|
+
};
|
|
22
|
+
type OriSupportedHarness = Exclude<LaunchPlan["profile"]["harness"], "omp" | "cline" | "hermes" | "prime-agent" | "gemini" | "aider" | "opencode" | "kilo">;
|
|
23
|
+
export declare function validateOriForPlan(plan: LaunchPlan, options?: Pick<OriPreparationOptions, "oriExecutable" | "args" | "credentialEnv" | "cwd">): Promise<{
|
|
24
|
+
contract: OriContract;
|
|
25
|
+
request: ReturnType<typeof buildOriRequest>;
|
|
26
|
+
warnings: string[];
|
|
27
|
+
}>;
|
|
28
|
+
declare function buildOriRequest(plan: LaunchPlan, target: OriSupportedHarness, catalogPath: string | undefined, args: string[]): {
|
|
29
|
+
readonly target: OriSupportedHarness;
|
|
30
|
+
readonly provider: string;
|
|
31
|
+
readonly providerBaseUrl: string;
|
|
32
|
+
readonly protocol: "anthropic-messages" | "openai-responses" | "openai-chat" | "gemini-generate-content";
|
|
33
|
+
readonly model: string;
|
|
34
|
+
readonly catalog: {
|
|
35
|
+
readonly codexModelCatalogPath?: string | undefined;
|
|
36
|
+
readonly source: "switcher-openrouter";
|
|
37
|
+
readonly modelIds: string[];
|
|
38
|
+
};
|
|
39
|
+
readonly args: string[];
|
|
40
|
+
};
|
|
41
|
+
export declare function prepareOriForPlan(plan: LaunchPlan, options?: OriPreparationOptions): Promise<{
|
|
42
|
+
contract: OriContract;
|
|
43
|
+
prepared: OriLaunchPlan;
|
|
44
|
+
}>;
|
|
45
|
+
export declare function launch(client: SwitcherClient, profileId: string, options?: LaunchOptions): Promise<number>;
|
package/dist/mcp/index.js
CHANGED
|
@@ -8,9 +8,9 @@ import { z as z2 } from "zod";
|
|
|
8
8
|
|
|
9
9
|
// src/domain.ts
|
|
10
10
|
import { z } from "zod";
|
|
11
|
-
var VERSION = "0.1.
|
|
12
|
-
var harnessSchema = z.enum(["claude", "codex", "grok", "opencode2"]);
|
|
13
|
-
var protocolSchema = z.enum(["anthropic-messages", "openai-responses", "openai-chat"]);
|
|
11
|
+
var VERSION = "0.1.2";
|
|
12
|
+
var harnessSchema = z.enum(["claude", "codex", "grok", "opencode", "opencode2", "pi", "omp", "dsh", "cline", "hermes", "prime-agent", "gemini", "aider", "kilo"]);
|
|
13
|
+
var protocolSchema = z.enum(["anthropic-messages", "openai-responses", "openai-chat", "gemini-generate-content"]);
|
|
14
14
|
var idSchema = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/);
|
|
15
15
|
var label = z.string().min(1).max(200);
|
|
16
16
|
var envRef = z.string().regex(/^SWITCHER_PROVIDER_[A-Z0-9_]+$/);
|
|
@@ -37,11 +37,13 @@ var modelSchema = z.object({
|
|
|
37
37
|
id: z.string().min(1).max(300),
|
|
38
38
|
name: label,
|
|
39
39
|
description: z.string().max(8000).optional(),
|
|
40
|
+
available: z.boolean().optional(),
|
|
40
41
|
contextWindow: z.number().int().positive().optional(),
|
|
41
42
|
maxOutputTokens: z.number().int().positive().optional(),
|
|
42
43
|
inputModalities: z.array(z.string().max(50)).max(20).optional(),
|
|
43
44
|
outputModalities: z.array(z.string().max(50)).max(20).optional(),
|
|
44
|
-
supportedParameters: z.array(z.string().max(100)).max(100).optional()
|
|
45
|
+
supportedParameters: z.array(z.string().max(100)).max(100).optional(),
|
|
46
|
+
supportedGenerationMethods: z.array(z.string().min(1).max(100)).max(100).optional()
|
|
45
47
|
}).strict();
|
|
46
48
|
var providerInputSchema = z.object({
|
|
47
49
|
id: idSchema,
|
|
@@ -49,10 +51,33 @@ var providerInputSchema = z.object({
|
|
|
49
51
|
baseUrl: urlSchema,
|
|
50
52
|
protocol: protocolSchema,
|
|
51
53
|
credentialEnv: envRef.optional(),
|
|
52
|
-
authStyle: z.enum(["bearer", "x-api-key"]).default("bearer"),
|
|
54
|
+
authStyle: z.enum(["bearer", "x-api-key", "api-key"]).default("bearer"),
|
|
55
|
+
catalogBaseUrl: urlSchema.optional(),
|
|
56
|
+
catalogFormat: z.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "gemini", "none"]).optional(),
|
|
57
|
+
catalogAuthStyle: z.enum(["bearer", "x-api-key", "api-key", "none"]).optional(),
|
|
58
|
+
catalogCredentialEnv: envRef.optional(),
|
|
59
|
+
catalogAccountId: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/).optional(),
|
|
53
60
|
modelsPath: z.string().regex(/^[a-zA-Z0-9_/-]+$/).max(200).default("models"),
|
|
54
61
|
manualModels: z.array(modelSchema).max(1e4).default([])
|
|
55
62
|
}).strict().refine((p) => !p.modelsPath.split("/").includes("..") && !p.modelsPath.startsWith("/"), "modelsPath must be relative");
|
|
63
|
+
var providerPresetSchema = z.object({
|
|
64
|
+
id: idSchema,
|
|
65
|
+
name: label,
|
|
66
|
+
credentialEnv: envRef.optional(),
|
|
67
|
+
credentialAliases: z.array(z.string().regex(/^[A-Z][A-Z0-9_]+$/)),
|
|
68
|
+
protocols: z.array(z.object({
|
|
69
|
+
protocol: protocolSchema,
|
|
70
|
+
baseUrl: urlSchema.optional(),
|
|
71
|
+
authStyle: z.enum(["bearer", "x-api-key", "api-key"]),
|
|
72
|
+
catalogBaseUrl: urlSchema.optional(),
|
|
73
|
+
catalogFormat: z.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "gemini", "none"]),
|
|
74
|
+
catalogAuthStyle: z.enum(["bearer", "x-api-key", "api-key", "none"]).optional(),
|
|
75
|
+
modelsPath: z.string(),
|
|
76
|
+
notes: z.array(z.string())
|
|
77
|
+
}).strict()).min(1),
|
|
78
|
+
sources: z.array(z.string().url()),
|
|
79
|
+
verification: z.literal("documented")
|
|
80
|
+
}).strict();
|
|
56
81
|
var profileInputSchema = z.object({
|
|
57
82
|
id: idSchema,
|
|
58
83
|
name: label,
|
|
@@ -80,6 +105,12 @@ class Fault extends Error {
|
|
|
80
105
|
this.code = code;
|
|
81
106
|
}
|
|
82
107
|
}
|
|
108
|
+
function parse(schema, value) {
|
|
109
|
+
const result = schema.safeParse(value);
|
|
110
|
+
if (!result.success)
|
|
111
|
+
throw new Fault(400, "invalid_request", result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; "));
|
|
112
|
+
return result.data;
|
|
113
|
+
}
|
|
83
114
|
|
|
84
115
|
// src/http.ts
|
|
85
116
|
var MAX_BYTES = 16 * 1024 * 1024;
|
|
@@ -118,6 +149,92 @@ async function boundedJson(response, maxBytes = MAX_BYTES) {
|
|
|
118
149
|
// src/sdk.ts
|
|
119
150
|
import { resolveCredential } from "@hasna/contracts/client";
|
|
120
151
|
|
|
152
|
+
// src/presets.ts
|
|
153
|
+
var route = (protocol, baseUrl, options = {}) => ({
|
|
154
|
+
protocol,
|
|
155
|
+
baseUrl,
|
|
156
|
+
authStyle: "bearer",
|
|
157
|
+
catalogFormat: "openai",
|
|
158
|
+
modelsPath: "models",
|
|
159
|
+
notes: [],
|
|
160
|
+
...options
|
|
161
|
+
});
|
|
162
|
+
var preset = (id, name, protocols, sources, alias) => parse(providerPresetSchema, {
|
|
163
|
+
id,
|
|
164
|
+
name,
|
|
165
|
+
protocols,
|
|
166
|
+
sources,
|
|
167
|
+
credentialAliases: alias ? [alias] : [],
|
|
168
|
+
credentialEnv: alias ? `SWITCHER_PROVIDER_${id.toUpperCase().replace(/-/g, "_")}` : undefined,
|
|
169
|
+
verification: "documented"
|
|
170
|
+
});
|
|
171
|
+
var providerPresets = [
|
|
172
|
+
preset("deepseek", "DeepSeek", [
|
|
173
|
+
route("openai-chat", "https://api.deepseek.com", { catalogBaseUrl: "https://api.deepseek.com" }),
|
|
174
|
+
route("anthropic-messages", "https://api.deepseek.com/anthropic/v1", { catalogBaseUrl: "https://api.deepseek.com" })
|
|
175
|
+
], ["https://api-docs.deepseek.com/guides/anthropic_api", "https://api-docs.deepseek.com/api/list-models"], "DEEPSEEK_API_KEY"),
|
|
176
|
+
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"),
|
|
177
|
+
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"),
|
|
178
|
+
preset("gemini", "Google Gemini", [route("gemini-generate-content", "https://generativelanguage.googleapis.com/v1beta", {
|
|
179
|
+
authStyle: "x-api-key",
|
|
180
|
+
catalogBaseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
|
181
|
+
catalogFormat: "gemini",
|
|
182
|
+
catalogAuthStyle: "x-api-key",
|
|
183
|
+
notes: ["Gemini CLI uses the native generateContent wire with x-goog-api-key authentication; model IDs are returned as models/{id}."]
|
|
184
|
+
}), 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"),
|
|
185
|
+
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"),
|
|
186
|
+
preset("azure-openai", "Azure OpenAI (v1)", [
|
|
187
|
+
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."] }),
|
|
188
|
+
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."] })
|
|
189
|
+
], ["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"),
|
|
190
|
+
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"),
|
|
191
|
+
preset("ollama", "Ollama", ["openai-chat", "openai-responses"].map((protocol) => route(protocol, "http://127.0.0.1:11434/v1", {
|
|
192
|
+
catalogBaseUrl: "http://127.0.0.1:11434",
|
|
193
|
+
modelsPath: "api/tags",
|
|
194
|
+
catalogFormat: "ollama",
|
|
195
|
+
catalogAuthStyle: "none",
|
|
196
|
+
notes: protocol === "openai-responses" ? ["Requires Ollama 0.13.3 or newer; only stateless Responses are supported."] : []
|
|
197
|
+
})), ["https://docs.ollama.com/api/openai-compatibility", "https://docs.ollama.com/api/tags"]),
|
|
198
|
+
preset("lmstudio", "LM Studio", ["openai-chat", "openai-responses", "anthropic-messages"].map((protocol) => route(protocol, "http://127.0.0.1:1234/v1", {
|
|
199
|
+
notes: ["Server authentication is optional. Use --credential-env when authentication is enabled."]
|
|
200
|
+
})), ["https://lmstudio.ai/docs/developer/rest"]),
|
|
201
|
+
preset("vllm", "vLLM (operator endpoint)", [
|
|
202
|
+
route("openai-chat", undefined, { notes: ["Pass the operator's OpenAI-compatible URL, normally ending in /v1. vLLM exposes Chat Completions at /chat/completions and GET /models under that prefix; configure --credential-env only when the server was started with --api-key or VLLM_API_KEY."] }),
|
|
203
|
+
route("openai-responses", undefined, { notes: ["Pass the operator's OpenAI-compatible URL, normally ending in /v1. Responses is available for supported text-generation models at /responses; configure --credential-env only when the server was started with --api-key or VLLM_API_KEY."] }),
|
|
204
|
+
route("anthropic-messages", undefined, { notes: ["Pass the operator's URL, normally ending in /v1. vLLM exposes the Anthropic Messages API at /messages for supported deployments. Chat templates and the configured tool parser determine whether streaming and tool calls work for a served model; configure --credential-env only when the server was started with --api-key or VLLM_API_KEY."] })
|
|
205
|
+
], ["https://docs.vllm.ai/en/latest/serving/online_serving/openai_compatible_server/", "https://github.com/vllm-project/vllm/blob/main/docs/serving/online_serving/README.md"]),
|
|
206
|
+
preset("litellm", "LiteLLM Proxy (operator endpoint)", [
|
|
207
|
+
route("openai-chat", undefined, { notes: ["Pass the proxy's documented OpenAI-compatible base URL exactly; the official quick start uses the root server URL, while a deployment may add a prefix such as /v1. LiteLLM serves Chat Completions and GET /models relative to that URL; use --credential-env for the proxy's configured master key or other bearer token."] }),
|
|
208
|
+
route("openai-responses", undefined, { notes: ["Pass the proxy's documented OpenAI-compatible base URL exactly; LiteLLM documents the Responses API under the same proxy root or deployment prefix. Use --credential-env for the proxy's configured master key or other bearer token."] }),
|
|
209
|
+
route("anthropic-messages", undefined, { notes: ["Pass the complete inference prefix ending in /v1, including any deployment prefix. LiteLLM registers /v1/messages; Switcher appends /messages to the stored prefix and discovers /models there. This is a gateway adapter: streaming and tool behavior depend on the configured upstream model and route, so verify those capabilities independently. Use --credential-env for the proxy's configured master key or other bearer token."] })
|
|
210
|
+
], ["https://docs.litellm.ai/", "https://docs.litellm.ai/docs/proxy/quick_start", "https://github.com/BerriAI/litellm/blob/main/litellm/proxy/anthropic_endpoints/endpoints.py"]),
|
|
211
|
+
preset("groq", "Groq", [route("openai-chat", "https://api.groq.com/openai/v1"), route("openai-responses", "https://api.groq.com/openai/v1", { notes: ["Responses is an upstream beta API."] })], ["https://console.groq.com/docs/api-reference"], "GROQ_API_KEY"),
|
|
212
|
+
preset("cerebras", "Cerebras", [route("openai-chat", "https://api.cerebras.ai/v1")], ["https://inference-docs.cerebras.ai/api-reference/chat-completions"], "CEREBRAS_API_KEY"),
|
|
213
|
+
preset("mistral", "Mistral", [route("openai-chat", "https://api.mistral.ai/v1", { catalogFormat: "mistral" })], ["https://docs.mistral.ai/api/endpoint/chat", "https://docs.mistral.ai/api/endpoint/models"], "MISTRAL_API_KEY"),
|
|
214
|
+
preset("together", "Together AI", [route("openai-chat", "https://api.together.ai/v1", { catalogFormat: "together" })], ["https://docs.together.ai/docs/inference/openai-compatibility", "https://docs.together.ai/reference/models"], "TOGETHER_API_KEY"),
|
|
215
|
+
preset("fireworks", "Fireworks AI", [
|
|
216
|
+
route("openai-chat", "https://api.fireworks.ai/inference/v1", { catalogFormat: "fireworks", notes: ["Model discovery uses GET /v1/accounts/{account_id}/models; provide --catalog-account-id or --catalog-url."] }),
|
|
217
|
+
route("openai-responses", "https://api.fireworks.ai/inference/v1", { catalogFormat: "fireworks", notes: ["Model discovery uses GET /v1/accounts/{account_id}/models; provide --catalog-account-id or --catalog-url."] }),
|
|
218
|
+
route("anthropic-messages", "https://api.fireworks.ai/inference/v1", { catalogFormat: "fireworks", notes: ["Model discovery uses GET /v1/accounts/{account_id}/models; provide --catalog-account-id or --catalog-url."] })
|
|
219
|
+
], ["https://docs.fireworks.ai/getting-started/quickstart", "https://docs.fireworks.ai/tools-sdks/python-client/api-reference", "https://docs.fireworks.ai/api-reference/anthropic-messages", "https://docs.fireworks.ai/api-reference/post-chatcompletions", "https://docs.fireworks.ai/api-reference/list-models"], "FIREWORKS_API_KEY"),
|
|
220
|
+
preset("moonshot", "Moonshot AI (Kimi)", [route("openai-chat", "https://api.moonshot.ai/v1", { catalogBaseUrl: "https://api.moonshot.ai/v1" })], ["https://platform.kimi.ai/docs/api/chat", "https://platform.kimi.ai/docs/api/list-models"], "MOONSHOT_API_KEY"),
|
|
221
|
+
preset("dashscope", "Alibaba Cloud Model Studio (Qwen)", [route("openai-chat", "https://dashscope-us.aliyuncs.com/compatible-mode/v1", {
|
|
222
|
+
catalogFormat: "none",
|
|
223
|
+
notes: ["Inference keys and endpoints are region/workspace-specific. Model discovery uses GET /api/v1/models on a documented region or workspace catalog URL; pass --catalog-url and --catalog-format dashscope."]
|
|
224
|
+
})], ["https://help.aliyun.com/en/model-studio/base-url", "https://help.aliyun.com/en/model-studio/compatibility-of-openai-with-dashscope", "https://help.aliyun.com/en/model-studio/list-models"], "DASHSCOPE_API_KEY"),
|
|
225
|
+
preset("zai", "Z.AI", [route("openai-chat", "https://api.z.ai/api/paas/v4", {
|
|
226
|
+
catalogFormat: "none",
|
|
227
|
+
notes: ["The published API reference documents inference endpoints but no model-list endpoint; use manual models or provide an explicit catalog URL and parser."]
|
|
228
|
+
})], ["https://docs.z.ai/api-reference/introduction", "https://docs.z.ai/devpack/quick-start"], "ZAI_API_KEY"),
|
|
229
|
+
preset("minimax", "MiniMax", [
|
|
230
|
+
route("openai-chat", "https://api.minimax.cn/v1", { catalogBaseUrl: "https://api.minimax.cn/v1", notes: ["The Open Platform contract uses api.minimax.cn and Bearer auth. Token Plan documentation uses api.minimaxi.com; select that authority explicitly with --url and matching auth/credential settings."] }),
|
|
231
|
+
route("anthropic-messages", "https://api.minimax.cn/anthropic/v1", { authStyle: "x-api-key", catalogBaseUrl: "https://api.minimax.cn/anthropic/v1", catalogAuthStyle: "x-api-key", notes: ["The Open Platform contract uses api.minimax.cn/anthropic/v1 and X-Api-Key. Token Plan documentation uses api.minimaxi.com/anthropic; select that authority explicitly with --url and matching auth/credential settings."] })
|
|
232
|
+
], ["https://platform.minimaxi.com/docs/api-reference/text-chat-anthropic", "https://platform.minimaxi.com/docs/api-reference/models/anthropic/list-models", "https://platform.minimaxi.com/docs/api-reference/models/openai/list-models", "https://platform.minimaxi.com/docs/token-plan/other-tools"], "MINIMAX_API_KEY"),
|
|
233
|
+
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"),
|
|
234
|
+
...["anthropic-messages", "openai-responses", "openai-chat"].map((protocol) => preset(`generic-${protocol}`, `Custom ${protocol}`, [route(protocol)], []))
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
// src/sdk.ts
|
|
121
238
|
class SwitcherError extends Error {
|
|
122
239
|
status;
|
|
123
240
|
code;
|
|
@@ -129,6 +246,26 @@ class SwitcherError extends Error {
|
|
|
129
246
|
this.requestId = requestId;
|
|
130
247
|
}
|
|
131
248
|
}
|
|
249
|
+
function apiError(status, data, apiKey) {
|
|
250
|
+
const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
251
|
+
const error = object(data) && object(data.error) ? data.error : {};
|
|
252
|
+
const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
253
|
+
const base64 = Buffer.from(apiKey, "utf8").toString("base64");
|
|
254
|
+
const literal = [apiKey, JSON.stringify(apiKey).slice(1, -1), base64, base64.replace(/=+$/, ""), Buffer.from(apiKey, "utf8").toString("base64url")];
|
|
255
|
+
const encoded = [encodeURIComponent(apiKey), new URLSearchParams({ key: apiKey }).toString().slice(4)];
|
|
256
|
+
const patterns = [...new Set(literal)].sort((a, b) => b.length - a.length).map(escape);
|
|
257
|
+
for (const value of encoded) {
|
|
258
|
+
patterns.unshift(escape(value).replace(/%[0-9A-F]{2}/g, (part) => part.replace(/[A-F]/g, (letter) => `[${letter}${letter.toLowerCase()}]`)));
|
|
259
|
+
}
|
|
260
|
+
const reflected = new RegExp(patterns.join("|"), "g");
|
|
261
|
+
const redact = (value) => value.replace(reflected, "[REDACTED]");
|
|
262
|
+
const identifier = (value, pattern) => typeof value === "string" && pattern.test(value) && redact(value) === value ? value : undefined;
|
|
263
|
+
const code = identifier(error.code, /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/) ?? "api_error";
|
|
264
|
+
const requestId = identifier(error.requestId, /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/);
|
|
265
|
+
const fallback = `Switcher API returned HTTP ${status}.`;
|
|
266
|
+
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;
|
|
267
|
+
return new SwitcherError(status, code, message, requestId);
|
|
268
|
+
}
|
|
132
269
|
|
|
133
270
|
class SwitcherClient {
|
|
134
271
|
options;
|
|
@@ -140,7 +277,7 @@ class SwitcherClient {
|
|
|
140
277
|
this.options = { ...options };
|
|
141
278
|
}
|
|
142
279
|
async request(method, path, body, options = {}) {
|
|
143
|
-
if (!/^\/v1\/[a-zA-Z0-9/?&=._%+-]+$/.test(path) || path.includes(".."))
|
|
280
|
+
if (!/^\/v1\/[a-zA-Z0-9/?&=._%+-]+$/.test(path) && !["/health", "/ready", "/version"].includes(path) || path.includes(".."))
|
|
144
281
|
throw new Error("Invalid API path.");
|
|
145
282
|
const apiKey = typeof this.options.apiKey === "function" ? this.options.apiKey() : this.options.apiKey;
|
|
146
283
|
if (!apiKey || /[\r\n]/.test(apiKey))
|
|
@@ -165,12 +302,27 @@ class SwitcherClient {
|
|
|
165
302
|
throw new SwitcherError(response.status, "invalid_response", "Switcher API returned invalid JSON.");
|
|
166
303
|
}
|
|
167
304
|
if (!response.ok)
|
|
168
|
-
throw
|
|
305
|
+
throw apiError(response.status, data, apiKey);
|
|
169
306
|
return data;
|
|
170
307
|
}
|
|
171
308
|
query(options = {}) {
|
|
172
309
|
return new URLSearchParams(Object.entries(options).filter(([, v]) => v !== undefined).map(([k, v]) => [k, String(v)])).toString();
|
|
173
310
|
}
|
|
311
|
+
health() {
|
|
312
|
+
return this.request("GET", "/health");
|
|
313
|
+
}
|
|
314
|
+
ready() {
|
|
315
|
+
return this.request("GET", "/ready");
|
|
316
|
+
}
|
|
317
|
+
version() {
|
|
318
|
+
return this.request("GET", "/version");
|
|
319
|
+
}
|
|
320
|
+
listProviderPresets() {
|
|
321
|
+
return this.request("GET", "/v1/provider-presets");
|
|
322
|
+
}
|
|
323
|
+
getProviderPreset(id) {
|
|
324
|
+
return this.request("GET", `/v1/provider-presets/${encodeURIComponent(id)}`);
|
|
325
|
+
}
|
|
174
326
|
listProviders(options = {}) {
|
|
175
327
|
return this.request("GET", `/v1/providers?${this.query(options)}`);
|
|
176
328
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { HarnessLaunchInput, PreparedLaunch } from "./harness-types";
|
|
2
|
+
/**
|
|
3
|
+
* Prepare an isolated OMP configuration. OMP reads models.yml as JSON-valid
|
|
4
|
+
* YAML, and resolves the apiKey/header references from the child environment.
|
|
5
|
+
* The credential therefore never reaches the generated files or command line.
|
|
6
|
+
*/
|
|
7
|
+
export declare function prepareOmpLaunch(input: HarnessLaunchInput): Promise<PreparedLaunch>;
|