@hasna/switcher 0.1.1 → 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 +142 -6
- 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 +6481 -3510
- package/dist/cline-backend.d.ts +7 -0
- package/dist/credentials.d.ts +5 -5
- package/dist/direct-launch.d.ts +1 -1
- package/dist/domain.d.ts +62 -49
- 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 +28 -21
- package/dist/harness-arguments.d.ts +1 -0
- package/dist/harness-installation.d.ts +19 -0
- package/dist/harness-types.d.ts +1 -0
- package/dist/harnesses.d.ts +5 -2
- package/dist/hermes-backend.d.ts +19 -0
- package/dist/index.js +53 -11
- package/dist/kilo-config.d.ts +13 -0
- package/dist/kilo.d.ts +5 -0
- package/dist/launcher.d.ts +4 -3
- package/dist/mcp/index.js +43 -11
- package/dist/omp-backend.d.ts +7 -0
- package/dist/opencode2-config.d.ts +3 -3
- package/dist/ori-backend.d.ts +2 -2
- package/dist/presets.d.ts +11 -10
- package/dist/sdk.d.ts +41 -33
- package/dist/sdk.js +53 -11
- package/dist/serve/index.js +274 -49
- package/openapi.json +138 -14
- package/package.json +12 -2
package/dist/serve/index.js
CHANGED
|
@@ -12,9 +12,9 @@ import { dirname, resolve } from "path";
|
|
|
12
12
|
|
|
13
13
|
// src/domain.ts
|
|
14
14
|
import { z } from "zod";
|
|
15
|
-
var VERSION = "0.1.
|
|
16
|
-
var harnessSchema = z.enum(["claude", "codex", "grok", "opencode2", "pi"]);
|
|
17
|
-
var protocolSchema = z.enum(["anthropic-messages", "openai-responses", "openai-chat"]);
|
|
15
|
+
var VERSION = "0.1.2";
|
|
16
|
+
var harnessSchema = z.enum(["claude", "codex", "grok", "opencode", "opencode2", "pi", "omp", "dsh", "cline", "hermes", "prime-agent", "gemini", "aider", "kilo"]);
|
|
17
|
+
var protocolSchema = z.enum(["anthropic-messages", "openai-responses", "openai-chat", "gemini-generate-content"]);
|
|
18
18
|
var idSchema = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/);
|
|
19
19
|
var label = z.string().min(1).max(200);
|
|
20
20
|
var envRef = z.string().regex(/^SWITCHER_PROVIDER_[A-Z0-9_]+$/);
|
|
@@ -46,7 +46,8 @@ var modelSchema = z.object({
|
|
|
46
46
|
maxOutputTokens: z.number().int().positive().optional(),
|
|
47
47
|
inputModalities: z.array(z.string().max(50)).max(20).optional(),
|
|
48
48
|
outputModalities: z.array(z.string().max(50)).max(20).optional(),
|
|
49
|
-
supportedParameters: z.array(z.string().max(100)).max(100).optional()
|
|
49
|
+
supportedParameters: z.array(z.string().max(100)).max(100).optional(),
|
|
50
|
+
supportedGenerationMethods: z.array(z.string().min(1).max(100)).max(100).optional()
|
|
50
51
|
}).strict();
|
|
51
52
|
var providerInputSchema = z.object({
|
|
52
53
|
id: idSchema,
|
|
@@ -54,10 +55,10 @@ var providerInputSchema = z.object({
|
|
|
54
55
|
baseUrl: urlSchema,
|
|
55
56
|
protocol: protocolSchema,
|
|
56
57
|
credentialEnv: envRef.optional(),
|
|
57
|
-
authStyle: z.enum(["bearer", "x-api-key"]).default("bearer"),
|
|
58
|
+
authStyle: z.enum(["bearer", "x-api-key", "api-key"]).default("bearer"),
|
|
58
59
|
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(),
|
|
60
|
+
catalogFormat: z.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "gemini", "none"]).optional(),
|
|
61
|
+
catalogAuthStyle: z.enum(["bearer", "x-api-key", "api-key", "none"]).optional(),
|
|
61
62
|
catalogCredentialEnv: envRef.optional(),
|
|
62
63
|
catalogAccountId: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/).optional(),
|
|
63
64
|
modelsPath: z.string().regex(/^[a-zA-Z0-9_/-]+$/).max(200).default("models"),
|
|
@@ -71,10 +72,10 @@ var providerPresetSchema = z.object({
|
|
|
71
72
|
protocols: z.array(z.object({
|
|
72
73
|
protocol: protocolSchema,
|
|
73
74
|
baseUrl: urlSchema.optional(),
|
|
74
|
-
authStyle: z.enum(["bearer", "x-api-key"]),
|
|
75
|
+
authStyle: z.enum(["bearer", "x-api-key", "api-key"]),
|
|
75
76
|
catalogBaseUrl: urlSchema.optional(),
|
|
76
|
-
catalogFormat: z.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "none"]),
|
|
77
|
-
catalogAuthStyle: z.enum(["bearer", "x-api-key", "none"]).optional(),
|
|
77
|
+
catalogFormat: z.enum(["openai", "ollama", "mistral", "together", "fireworks", "dashscope", "gemini", "none"]),
|
|
78
|
+
catalogAuthStyle: z.enum(["bearer", "x-api-key", "api-key", "none"]).optional(),
|
|
78
79
|
modelsPath: z.string(),
|
|
79
80
|
notes: z.array(z.string())
|
|
80
81
|
}).strict()).min(1),
|
|
@@ -115,10 +116,27 @@ function parse(schema, value) {
|
|
|
115
116
|
return result.data;
|
|
116
117
|
}
|
|
117
118
|
function compatible(harness, protocol) {
|
|
118
|
-
|
|
119
|
+
if (harness === "claude")
|
|
120
|
+
return protocol === "anthropic-messages";
|
|
121
|
+
if (harness === "codex")
|
|
122
|
+
return protocol === "openai-responses";
|
|
123
|
+
if (harness === "gemini")
|
|
124
|
+
return protocol === "gemini-generate-content";
|
|
125
|
+
if (protocol === "gemini-generate-content")
|
|
126
|
+
return false;
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
function validateHarnessProvider(harness, provider) {
|
|
130
|
+
if (!compatible(harness, provider.protocol))
|
|
131
|
+
throw new Fault(422, "protocol_mismatch", "Harness does not support this provider protocol.");
|
|
132
|
+
if (harness === "gemini" && provider.authStyle !== "x-api-key")
|
|
133
|
+
throw new Fault(422, "auth_mismatch", "Gemini CLI requires x-api-key authentication for its native generateContent protocol.");
|
|
119
134
|
}
|
|
120
135
|
function codingEligible(model) {
|
|
121
|
-
return model.available !== false && (!model.outputModalities || model.outputModalities.includes("text")) && (!model.supportedParameters || model.supportedParameters.includes("tools"));
|
|
136
|
+
return model.available !== false && (!model.supportedGenerationMethods || model.supportedGenerationMethods.includes("generateContent")) && (!model.outputModalities || model.outputModalities.includes("text")) && (!model.supportedParameters || model.supportedParameters.includes("tools"));
|
|
137
|
+
}
|
|
138
|
+
function harnessEligible(model, harness) {
|
|
139
|
+
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
140
|
}
|
|
123
141
|
|
|
124
142
|
// src/store.ts
|
|
@@ -317,6 +335,13 @@ async function boundedJson(response, maxBytes = MAX_BYTES) {
|
|
|
317
335
|
}
|
|
318
336
|
}
|
|
319
337
|
|
|
338
|
+
// src/auth.ts
|
|
339
|
+
function authHeader(style, credential) {
|
|
340
|
+
if (/[^\x20-\x7e]/.test(credential))
|
|
341
|
+
throw new Error("Provider credential contains invalid header characters.");
|
|
342
|
+
return style === "bearer" ? ["authorization", `Bearer ${credential}`] : [style, credential];
|
|
343
|
+
}
|
|
344
|
+
|
|
320
345
|
// src/catalog.ts
|
|
321
346
|
var positive = (v) => typeof v === "number" && Number.isInteger(v) && v > 0 ? v : undefined;
|
|
322
347
|
var strings = (v) => Array.isArray(v) && v.every((i) => typeof i === "string") ? v : undefined;
|
|
@@ -327,6 +352,64 @@ var modalities = (v) => {
|
|
|
327
352
|
throw new Fault(502, "invalid_catalog", "Provider returned malformed modality metadata.");
|
|
328
353
|
return v.map((i) => i.toLowerCase());
|
|
329
354
|
};
|
|
355
|
+
var CATALOG_REQUEST_TIMEOUT_MS = 20000;
|
|
356
|
+
var CATALOG_REFRESH_DEADLINE_MS = 60000;
|
|
357
|
+
var CATALOG_MAX_RETRIES = 2;
|
|
358
|
+
var TRANSIENT_CATALOG_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
359
|
+
function retryAfterMs(value) {
|
|
360
|
+
if (!value)
|
|
361
|
+
return;
|
|
362
|
+
const trimmed = value.trim();
|
|
363
|
+
if (/^\d+$/.test(trimmed)) {
|
|
364
|
+
try {
|
|
365
|
+
const milliseconds = BigInt(trimmed) * 1000n;
|
|
366
|
+
if (milliseconds > BigInt(Number.MAX_SAFE_INTEGER))
|
|
367
|
+
return Number.POSITIVE_INFINITY;
|
|
368
|
+
return Number(milliseconds);
|
|
369
|
+
} catch {
|
|
370
|
+
return Number.POSITIVE_INFINITY;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const date = Date.parse(trimmed);
|
|
374
|
+
return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
|
|
375
|
+
}
|
|
376
|
+
function catalogDeadlineFault() {
|
|
377
|
+
return new Fault(502, "provider_unavailable", "Provider catalog refresh exceeded its bounded deadline.");
|
|
378
|
+
}
|
|
379
|
+
async function waitForCatalogRetry(delayMs, deadline) {
|
|
380
|
+
if (Date.now() + delayMs > deadline)
|
|
381
|
+
throw catalogDeadlineFault();
|
|
382
|
+
if (delayMs > 0)
|
|
383
|
+
await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
384
|
+
}
|
|
385
|
+
async function fetchCatalogPage(url, headers, deadline) {
|
|
386
|
+
for (let retry = 0;; retry++) {
|
|
387
|
+
const remaining = deadline - Date.now();
|
|
388
|
+
if (remaining <= 0)
|
|
389
|
+
throw catalogDeadlineFault();
|
|
390
|
+
let response;
|
|
391
|
+
try {
|
|
392
|
+
response = await fetch(url, {
|
|
393
|
+
headers,
|
|
394
|
+
redirect: "manual",
|
|
395
|
+
signal: AbortSignal.timeout(Math.max(1, Math.min(CATALOG_REQUEST_TIMEOUT_MS, remaining)))
|
|
396
|
+
});
|
|
397
|
+
} catch {
|
|
398
|
+
if (retry >= CATALOG_MAX_RETRIES)
|
|
399
|
+
throw new Fault(502, "provider_unavailable", "Provider catalog request failed.");
|
|
400
|
+
await waitForCatalogRetry(100 * 2 ** retry, deadline);
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (response.ok)
|
|
404
|
+
return response;
|
|
405
|
+
const retryable = TRANSIENT_CATALOG_STATUSES.has(response.status);
|
|
406
|
+
await response.body?.cancel().catch(() => {});
|
|
407
|
+
if (!retryable || retry >= CATALOG_MAX_RETRIES)
|
|
408
|
+
throw new Fault(502, "provider_rejected", `Provider catalog returned HTTP ${response.status}.`);
|
|
409
|
+
const delay = retryAfterMs(response.headers.get("retry-after"));
|
|
410
|
+
await waitForCatalogRetry(delay ?? 100 * 2 ** retry, deadline);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
330
413
|
async function discover(provider, env = process.env, resolveCredential) {
|
|
331
414
|
const refreshedAt = new Date().toISOString();
|
|
332
415
|
if (provider.manualModels.length)
|
|
@@ -352,7 +435,8 @@ async function discover(provider, env = process.env, resolveCredential) {
|
|
|
352
435
|
throw new Fault(422, "credential_missing", "Provider credential environment variable is not available on the server.");
|
|
353
436
|
if (/[\r\n]/.test(credential))
|
|
354
437
|
throw new Fault(422, "credential_invalid", "Catalog credential contains invalid header characters.");
|
|
355
|
-
|
|
438
|
+
const [header, value] = authHeader(authStyle, credential);
|
|
439
|
+
headers[provider.catalogFormat === "gemini" && header === "x-api-key" ? "x-goog-api-key" : header] = value;
|
|
356
440
|
}
|
|
357
441
|
if (provider.protocol === "anthropic-messages" && url.hostname !== "openrouter.ai")
|
|
358
442
|
headers["anthropic-version"] = "2023-06-01";
|
|
@@ -361,18 +445,10 @@ async function discover(provider, env = process.env, resolveCredential) {
|
|
|
361
445
|
const models = new Map;
|
|
362
446
|
const seenCursors = new Set;
|
|
363
447
|
const seenPages = new Set([url.href]);
|
|
448
|
+
const deadline = Date.now() + CATALOG_REFRESH_DEADLINE_MS;
|
|
364
449
|
let fireworksTotal;
|
|
365
450
|
for (let page = 0;page < 100; page++) {
|
|
366
|
-
|
|
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
|
-
}
|
|
451
|
+
const response = await fetchCatalogPage(url, headers, deadline);
|
|
376
452
|
const data = await boundedJson(response);
|
|
377
453
|
if (provider.catalogFormat === "fireworks" && data?.totalSize !== undefined) {
|
|
378
454
|
if (typeof data.totalSize !== "number" || !Number.isInteger(data.totalSize) || data.totalSize < 0)
|
|
@@ -381,11 +457,11 @@ async function discover(provider, env = process.env, resolveCredential) {
|
|
|
381
457
|
throw new Fault(502, "incomplete_catalog", "Provider catalog count changed during pagination; retry the refresh.");
|
|
382
458
|
fireworksTotal = data.totalSize;
|
|
383
459
|
}
|
|
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;
|
|
460
|
+
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
461
|
if (!Array.isArray(rows))
|
|
386
462
|
throw new Fault(502, "invalid_catalog", "Expected a provider catalog with a model array matching its configured format.");
|
|
387
463
|
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;
|
|
464
|
+
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
465
|
if (typeof id !== "string")
|
|
390
466
|
throw new Fault(502, "invalid_catalog", "Catalog entry is missing a model ID.");
|
|
391
467
|
const candidate = {
|
|
@@ -393,11 +469,12 @@ async function discover(provider, env = process.env, resolveCredential) {
|
|
|
393
469
|
name: row.displayName ?? row.name ?? row.display_name ?? id,
|
|
394
470
|
available: provider.catalogFormat === "mistral" && typeof row.archived === "boolean" ? !row.archived : undefined,
|
|
395
471
|
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),
|
|
472
|
+
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)),
|
|
473
|
+
maxOutputTokens: positive(row.top_provider?.max_completion_tokens ?? row.max_output_tokens ?? row.outputTokenLimit ?? row.model_info?.max_output_tokens),
|
|
398
474
|
inputModalities: strings(row.architecture?.input_modalities ?? row.input_modalities) ?? modalities(row.inference_metadata?.request_modality),
|
|
399
475
|
outputModalities: strings(row.architecture?.output_modalities ?? row.output_modalities) ?? modalities(row.inference_metadata?.response_modality),
|
|
400
|
-
supportedParameters: strings(row.supported_parameters)
|
|
476
|
+
supportedParameters: strings(row.supported_parameters),
|
|
477
|
+
...provider.catalogFormat === "gemini" && row.supportedGenerationMethods !== undefined ? { supportedGenerationMethods: row.supportedGenerationMethods } : {}
|
|
401
478
|
};
|
|
402
479
|
if (provider.catalogFormat === "mistral") {
|
|
403
480
|
const capabilities = row.capabilities;
|
|
@@ -468,6 +545,21 @@ async function discover(provider, env = process.env, resolveCredential) {
|
|
|
468
545
|
throw new Fault(502, "incomplete_catalog", "Provider catalog count does not match the collected models; retry the refresh.");
|
|
469
546
|
return { models: [...models.values()], source: "remote", refreshedAt };
|
|
470
547
|
}
|
|
548
|
+
if (provider.catalogFormat === "gemini") {
|
|
549
|
+
const nextPageToken = data.nextPageToken;
|
|
550
|
+
if (nextPageToken !== undefined && nextPageToken !== null) {
|
|
551
|
+
if (typeof nextPageToken !== "string" || nextPageToken.length > 2000)
|
|
552
|
+
throw new Fault(502, "invalid_catalog", "Gemini catalog pagination token is malformed.");
|
|
553
|
+
if (nextPageToken) {
|
|
554
|
+
if (seenCursors.has(nextPageToken))
|
|
555
|
+
throw new Fault(502, "invalid_catalog", "Provider catalog pagination did not advance.");
|
|
556
|
+
seenCursors.add(nextPageToken);
|
|
557
|
+
url.searchParams.set("pageToken", nextPageToken);
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return { models: [...models.values()], source: "remote", refreshedAt };
|
|
562
|
+
}
|
|
471
563
|
if (provider.catalogFormat === "dashscope") {
|
|
472
564
|
const output = data.output;
|
|
473
565
|
const total = output?.total;
|
|
@@ -529,7 +621,18 @@ var providerPresets = [
|
|
|
529
621
|
], ["https://api-docs.deepseek.com/guides/anthropic_api", "https://api-docs.deepseek.com/api/list-models"], "DEEPSEEK_API_KEY"),
|
|
530
622
|
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
623
|
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"),
|
|
624
|
+
preset("gemini", "Google Gemini", [route("gemini-generate-content", "https://generativelanguage.googleapis.com/v1beta", {
|
|
625
|
+
authStyle: "x-api-key",
|
|
626
|
+
catalogBaseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
|
627
|
+
catalogFormat: "gemini",
|
|
628
|
+
catalogAuthStyle: "x-api-key",
|
|
629
|
+
notes: ["Gemini CLI uses the native generateContent wire with x-goog-api-key authentication; model IDs are returned as models/{id}."]
|
|
630
|
+
}), 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
631
|
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"),
|
|
632
|
+
preset("azure-openai", "Azure OpenAI (v1)", [
|
|
633
|
+
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."] }),
|
|
634
|
+
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."] })
|
|
635
|
+
], ["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
636
|
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
637
|
preset("ollama", "Ollama", ["openai-chat", "openai-responses"].map((protocol) => route(protocol, "http://127.0.0.1:11434/v1", {
|
|
535
638
|
catalogBaseUrl: "http://127.0.0.1:11434",
|
|
@@ -587,7 +690,7 @@ var openapi_default = {
|
|
|
587
690
|
openapi: "3.0.3",
|
|
588
691
|
info: {
|
|
589
692
|
title: "Switcher API",
|
|
590
|
-
version: "0.1.
|
|
693
|
+
version: "0.1.2",
|
|
591
694
|
description: "Authenticated provider/profile/catalog control plane. Launches run locally; the API never returns provider credentials."
|
|
592
695
|
},
|
|
593
696
|
security: [
|
|
@@ -1801,7 +1904,8 @@ var openapi_default = {
|
|
|
1801
1904
|
enum: [
|
|
1802
1905
|
"anthropic-messages",
|
|
1803
1906
|
"openai-responses",
|
|
1804
|
-
"openai-chat"
|
|
1907
|
+
"openai-chat",
|
|
1908
|
+
"gemini-generate-content"
|
|
1805
1909
|
]
|
|
1806
1910
|
},
|
|
1807
1911
|
baseUrl: {
|
|
@@ -1812,7 +1916,8 @@ var openapi_default = {
|
|
|
1812
1916
|
type: "string",
|
|
1813
1917
|
enum: [
|
|
1814
1918
|
"bearer",
|
|
1815
|
-
"x-api-key"
|
|
1919
|
+
"x-api-key",
|
|
1920
|
+
"api-key"
|
|
1816
1921
|
]
|
|
1817
1922
|
},
|
|
1818
1923
|
catalogBaseUrl: {
|
|
@@ -1828,6 +1933,7 @@ var openapi_default = {
|
|
|
1828
1933
|
"together",
|
|
1829
1934
|
"fireworks",
|
|
1830
1935
|
"dashscope",
|
|
1936
|
+
"gemini",
|
|
1831
1937
|
"none"
|
|
1832
1938
|
]
|
|
1833
1939
|
},
|
|
@@ -1836,6 +1942,7 @@ var openapi_default = {
|
|
|
1836
1942
|
enum: [
|
|
1837
1943
|
"bearer",
|
|
1838
1944
|
"x-api-key",
|
|
1945
|
+
"api-key",
|
|
1839
1946
|
"none"
|
|
1840
1947
|
]
|
|
1841
1948
|
},
|
|
@@ -1905,7 +2012,8 @@ var openapi_default = {
|
|
|
1905
2012
|
enum: [
|
|
1906
2013
|
"anthropic-messages",
|
|
1907
2014
|
"openai-responses",
|
|
1908
|
-
"openai-chat"
|
|
2015
|
+
"openai-chat",
|
|
2016
|
+
"gemini-generate-content"
|
|
1909
2017
|
]
|
|
1910
2018
|
},
|
|
1911
2019
|
credentialEnv: {
|
|
@@ -1916,7 +2024,8 @@ var openapi_default = {
|
|
|
1916
2024
|
type: "string",
|
|
1917
2025
|
enum: [
|
|
1918
2026
|
"bearer",
|
|
1919
|
-
"x-api-key"
|
|
2027
|
+
"x-api-key",
|
|
2028
|
+
"api-key"
|
|
1920
2029
|
],
|
|
1921
2030
|
default: "bearer"
|
|
1922
2031
|
},
|
|
@@ -1933,6 +2042,7 @@ var openapi_default = {
|
|
|
1933
2042
|
"together",
|
|
1934
2043
|
"fireworks",
|
|
1935
2044
|
"dashscope",
|
|
2045
|
+
"gemini",
|
|
1936
2046
|
"none"
|
|
1937
2047
|
]
|
|
1938
2048
|
},
|
|
@@ -1941,6 +2051,7 @@ var openapi_default = {
|
|
|
1941
2051
|
enum: [
|
|
1942
2052
|
"bearer",
|
|
1943
2053
|
"x-api-key",
|
|
2054
|
+
"api-key",
|
|
1944
2055
|
"none"
|
|
1945
2056
|
]
|
|
1946
2057
|
},
|
|
@@ -2013,6 +2124,15 @@ var openapi_default = {
|
|
|
2013
2124
|
maxLength: 100
|
|
2014
2125
|
},
|
|
2015
2126
|
maxItems: 100
|
|
2127
|
+
},
|
|
2128
|
+
supportedGenerationMethods: {
|
|
2129
|
+
type: "array",
|
|
2130
|
+
items: {
|
|
2131
|
+
type: "string",
|
|
2132
|
+
minLength: 1,
|
|
2133
|
+
maxLength: 100
|
|
2134
|
+
},
|
|
2135
|
+
maxItems: 100
|
|
2016
2136
|
}
|
|
2017
2137
|
},
|
|
2018
2138
|
required: [
|
|
@@ -2054,7 +2174,8 @@ var openapi_default = {
|
|
|
2054
2174
|
enum: [
|
|
2055
2175
|
"anthropic-messages",
|
|
2056
2176
|
"openai-responses",
|
|
2057
|
-
"openai-chat"
|
|
2177
|
+
"openai-chat",
|
|
2178
|
+
"gemini-generate-content"
|
|
2058
2179
|
]
|
|
2059
2180
|
},
|
|
2060
2181
|
credentialEnv: {
|
|
@@ -2065,7 +2186,8 @@ var openapi_default = {
|
|
|
2065
2186
|
type: "string",
|
|
2066
2187
|
enum: [
|
|
2067
2188
|
"bearer",
|
|
2068
|
-
"x-api-key"
|
|
2189
|
+
"x-api-key",
|
|
2190
|
+
"api-key"
|
|
2069
2191
|
],
|
|
2070
2192
|
default: "bearer"
|
|
2071
2193
|
},
|
|
@@ -2082,6 +2204,7 @@ var openapi_default = {
|
|
|
2082
2204
|
"together",
|
|
2083
2205
|
"fireworks",
|
|
2084
2206
|
"dashscope",
|
|
2207
|
+
"gemini",
|
|
2085
2208
|
"none"
|
|
2086
2209
|
]
|
|
2087
2210
|
},
|
|
@@ -2090,6 +2213,7 @@ var openapi_default = {
|
|
|
2090
2213
|
enum: [
|
|
2091
2214
|
"bearer",
|
|
2092
2215
|
"x-api-key",
|
|
2216
|
+
"api-key",
|
|
2093
2217
|
"none"
|
|
2094
2218
|
]
|
|
2095
2219
|
},
|
|
@@ -2162,6 +2286,15 @@ var openapi_default = {
|
|
|
2162
2286
|
maxLength: 100
|
|
2163
2287
|
},
|
|
2164
2288
|
maxItems: 100
|
|
2289
|
+
},
|
|
2290
|
+
supportedGenerationMethods: {
|
|
2291
|
+
type: "array",
|
|
2292
|
+
items: {
|
|
2293
|
+
type: "string",
|
|
2294
|
+
minLength: 1,
|
|
2295
|
+
maxLength: 100
|
|
2296
|
+
},
|
|
2297
|
+
maxItems: 100
|
|
2165
2298
|
}
|
|
2166
2299
|
},
|
|
2167
2300
|
required: [
|
|
@@ -2214,8 +2347,17 @@ var openapi_default = {
|
|
|
2214
2347
|
"claude",
|
|
2215
2348
|
"codex",
|
|
2216
2349
|
"grok",
|
|
2350
|
+
"opencode",
|
|
2217
2351
|
"opencode2",
|
|
2218
|
-
"pi"
|
|
2352
|
+
"pi",
|
|
2353
|
+
"omp",
|
|
2354
|
+
"dsh",
|
|
2355
|
+
"cline",
|
|
2356
|
+
"hermes",
|
|
2357
|
+
"prime-agent",
|
|
2358
|
+
"gemini",
|
|
2359
|
+
"aider",
|
|
2360
|
+
"kilo"
|
|
2219
2361
|
]
|
|
2220
2362
|
},
|
|
2221
2363
|
model: {
|
|
@@ -2255,8 +2397,17 @@ var openapi_default = {
|
|
|
2255
2397
|
"claude",
|
|
2256
2398
|
"codex",
|
|
2257
2399
|
"grok",
|
|
2400
|
+
"opencode",
|
|
2258
2401
|
"opencode2",
|
|
2259
|
-
"pi"
|
|
2402
|
+
"pi",
|
|
2403
|
+
"omp",
|
|
2404
|
+
"dsh",
|
|
2405
|
+
"cline",
|
|
2406
|
+
"hermes",
|
|
2407
|
+
"prime-agent",
|
|
2408
|
+
"gemini",
|
|
2409
|
+
"aider",
|
|
2410
|
+
"kilo"
|
|
2260
2411
|
]
|
|
2261
2412
|
},
|
|
2262
2413
|
model: {
|
|
@@ -2337,6 +2488,15 @@ var openapi_default = {
|
|
|
2337
2488
|
maxLength: 100
|
|
2338
2489
|
},
|
|
2339
2490
|
maxItems: 100
|
|
2491
|
+
},
|
|
2492
|
+
supportedGenerationMethods: {
|
|
2493
|
+
type: "array",
|
|
2494
|
+
items: {
|
|
2495
|
+
type: "string",
|
|
2496
|
+
minLength: 1,
|
|
2497
|
+
maxLength: 100
|
|
2498
|
+
},
|
|
2499
|
+
maxItems: 100
|
|
2340
2500
|
}
|
|
2341
2501
|
},
|
|
2342
2502
|
required: [
|
|
@@ -2404,6 +2564,15 @@ var openapi_default = {
|
|
|
2404
2564
|
},
|
|
2405
2565
|
maxItems: 100
|
|
2406
2566
|
},
|
|
2567
|
+
supportedGenerationMethods: {
|
|
2568
|
+
type: "array",
|
|
2569
|
+
items: {
|
|
2570
|
+
type: "string",
|
|
2571
|
+
minLength: 1,
|
|
2572
|
+
maxLength: 100
|
|
2573
|
+
},
|
|
2574
|
+
maxItems: 100
|
|
2575
|
+
},
|
|
2407
2576
|
codingEligible: {
|
|
2408
2577
|
type: "boolean"
|
|
2409
2578
|
}
|
|
@@ -2504,6 +2673,15 @@ var openapi_default = {
|
|
|
2504
2673
|
maxLength: 100
|
|
2505
2674
|
},
|
|
2506
2675
|
maxItems: 100
|
|
2676
|
+
},
|
|
2677
|
+
supportedGenerationMethods: {
|
|
2678
|
+
type: "array",
|
|
2679
|
+
items: {
|
|
2680
|
+
type: "string",
|
|
2681
|
+
minLength: 1,
|
|
2682
|
+
maxLength: 100
|
|
2683
|
+
},
|
|
2684
|
+
maxItems: 100
|
|
2507
2685
|
}
|
|
2508
2686
|
},
|
|
2509
2687
|
required: [
|
|
@@ -2555,7 +2733,8 @@ var openapi_default = {
|
|
|
2555
2733
|
enum: [
|
|
2556
2734
|
"anthropic-messages",
|
|
2557
2735
|
"openai-responses",
|
|
2558
|
-
"openai-chat"
|
|
2736
|
+
"openai-chat",
|
|
2737
|
+
"gemini-generate-content"
|
|
2559
2738
|
]
|
|
2560
2739
|
},
|
|
2561
2740
|
credentialEnv: {
|
|
@@ -2566,7 +2745,8 @@ var openapi_default = {
|
|
|
2566
2745
|
type: "string",
|
|
2567
2746
|
enum: [
|
|
2568
2747
|
"bearer",
|
|
2569
|
-
"x-api-key"
|
|
2748
|
+
"x-api-key",
|
|
2749
|
+
"api-key"
|
|
2570
2750
|
],
|
|
2571
2751
|
default: "bearer"
|
|
2572
2752
|
},
|
|
@@ -2583,6 +2763,7 @@ var openapi_default = {
|
|
|
2583
2763
|
"together",
|
|
2584
2764
|
"fireworks",
|
|
2585
2765
|
"dashscope",
|
|
2766
|
+
"gemini",
|
|
2586
2767
|
"none"
|
|
2587
2768
|
]
|
|
2588
2769
|
},
|
|
@@ -2591,6 +2772,7 @@ var openapi_default = {
|
|
|
2591
2772
|
enum: [
|
|
2592
2773
|
"bearer",
|
|
2593
2774
|
"x-api-key",
|
|
2775
|
+
"api-key",
|
|
2594
2776
|
"none"
|
|
2595
2777
|
]
|
|
2596
2778
|
},
|
|
@@ -2663,6 +2845,15 @@ var openapi_default = {
|
|
|
2663
2845
|
maxLength: 100
|
|
2664
2846
|
},
|
|
2665
2847
|
maxItems: 100
|
|
2848
|
+
},
|
|
2849
|
+
supportedGenerationMethods: {
|
|
2850
|
+
type: "array",
|
|
2851
|
+
items: {
|
|
2852
|
+
type: "string",
|
|
2853
|
+
minLength: 1,
|
|
2854
|
+
maxLength: 100
|
|
2855
|
+
},
|
|
2856
|
+
maxItems: 100
|
|
2666
2857
|
}
|
|
2667
2858
|
},
|
|
2668
2859
|
required: [
|
|
@@ -2715,8 +2906,17 @@ var openapi_default = {
|
|
|
2715
2906
|
"claude",
|
|
2716
2907
|
"codex",
|
|
2717
2908
|
"grok",
|
|
2909
|
+
"opencode",
|
|
2718
2910
|
"opencode2",
|
|
2719
|
-
"pi"
|
|
2911
|
+
"pi",
|
|
2912
|
+
"omp",
|
|
2913
|
+
"dsh",
|
|
2914
|
+
"cline",
|
|
2915
|
+
"hermes",
|
|
2916
|
+
"prime-agent",
|
|
2917
|
+
"gemini",
|
|
2918
|
+
"aider",
|
|
2919
|
+
"kilo"
|
|
2720
2920
|
]
|
|
2721
2921
|
},
|
|
2722
2922
|
model: {
|
|
@@ -2802,6 +3002,15 @@ var openapi_default = {
|
|
|
2802
3002
|
maxLength: 100
|
|
2803
3003
|
},
|
|
2804
3004
|
maxItems: 100
|
|
3005
|
+
},
|
|
3006
|
+
supportedGenerationMethods: {
|
|
3007
|
+
type: "array",
|
|
3008
|
+
items: {
|
|
3009
|
+
type: "string",
|
|
3010
|
+
minLength: 1,
|
|
3011
|
+
maxLength: 100
|
|
3012
|
+
},
|
|
3013
|
+
maxItems: 100
|
|
2805
3014
|
}
|
|
2806
3015
|
},
|
|
2807
3016
|
required: [
|
|
@@ -2861,8 +3070,17 @@ var openapi_default = {
|
|
|
2861
3070
|
"claude",
|
|
2862
3071
|
"codex",
|
|
2863
3072
|
"grok",
|
|
3073
|
+
"opencode",
|
|
2864
3074
|
"opencode2",
|
|
2865
|
-
"pi"
|
|
3075
|
+
"pi",
|
|
3076
|
+
"omp",
|
|
3077
|
+
"dsh",
|
|
3078
|
+
"cline",
|
|
3079
|
+
"hermes",
|
|
3080
|
+
"prime-agent",
|
|
3081
|
+
"gemini",
|
|
3082
|
+
"aider",
|
|
3083
|
+
"kilo"
|
|
2866
3084
|
]
|
|
2867
3085
|
},
|
|
2868
3086
|
model: {
|
|
@@ -2919,8 +3137,17 @@ var openapi_default = {
|
|
|
2919
3137
|
"claude",
|
|
2920
3138
|
"codex",
|
|
2921
3139
|
"grok",
|
|
3140
|
+
"opencode",
|
|
2922
3141
|
"opencode2",
|
|
2923
|
-
"pi"
|
|
3142
|
+
"pi",
|
|
3143
|
+
"omp",
|
|
3144
|
+
"dsh",
|
|
3145
|
+
"cline",
|
|
3146
|
+
"hermes",
|
|
3147
|
+
"prime-agent",
|
|
3148
|
+
"gemini",
|
|
3149
|
+
"aider",
|
|
3150
|
+
"kilo"
|
|
2924
3151
|
]
|
|
2925
3152
|
},
|
|
2926
3153
|
model: {
|
|
@@ -3198,8 +3425,7 @@ function createHandler(store, apiKey, providerEnv = process.env, resolveCredenti
|
|
|
3198
3425
|
if (resource === "profiles") {
|
|
3199
3426
|
const profile = value;
|
|
3200
3427
|
const provider = await store.get("providers", profile.providerId, db);
|
|
3201
|
-
|
|
3202
|
-
throw new Fault(422, "protocol_mismatch", "Harness does not support this provider protocol.");
|
|
3428
|
+
validateHarnessProvider(profile.harness, provider);
|
|
3203
3429
|
}
|
|
3204
3430
|
const saved = await store.put(resource, value, id ? version() : undefined, db);
|
|
3205
3431
|
if (resource === "providers" && id)
|
|
@@ -3227,8 +3453,7 @@ function createHandler(store, apiKey, providerEnv = process.env, resolveCredenti
|
|
|
3227
3453
|
const { profileId } = parse(z2.object({ profileId: idSchema }).strict(), body);
|
|
3228
3454
|
const profile = await store.get("profiles", profileId, db);
|
|
3229
3455
|
const provider = await store.get("providers", profile.providerId, db);
|
|
3230
|
-
|
|
3231
|
-
throw new Fault(422, "protocol_mismatch", "Harness does not support this provider protocol.");
|
|
3456
|
+
validateHarnessProvider(profile.harness, provider);
|
|
3232
3457
|
let catalog;
|
|
3233
3458
|
try {
|
|
3234
3459
|
catalog = await store.get("catalogs", provider.id, db);
|
|
@@ -3240,10 +3465,10 @@ function createHandler(store, apiKey, providerEnv = process.env, resolveCredenti
|
|
|
3240
3465
|
const selected = catalog.models.find((m) => m.id === profile.model);
|
|
3241
3466
|
if (!selected)
|
|
3242
3467
|
throw new Fault(422, "model_missing", "Selected model is not in the provider catalog.");
|
|
3243
|
-
if (!
|
|
3244
|
-
throw new Fault(422, "model_ineligible", "Selected model explicitly lacks text output or tool support.");
|
|
3468
|
+
if (!harnessEligible(selected, profile.harness))
|
|
3469
|
+
throw new Fault(422, "model_ineligible", "Selected model is unavailable or explicitly lacks a required generation method, text output or tool support.");
|
|
3245
3470
|
const warnings = [];
|
|
3246
|
-
if (!selected.supportedParameters)
|
|
3471
|
+
if (profile.harness !== "aider" && !selected.supportedParameters)
|
|
3247
3472
|
warnings.push("Provider does not declare tool capabilities; execution compatibility is unverified.");
|
|
3248
3473
|
if (profile.harness === "claude" && !/claude/i.test(profile.model))
|
|
3249
3474
|
warnings.push("Anthropic does not support non-Claude models in Claude Code; this combination is experimental.");
|