@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/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"]);
|
|
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_]+$/);
|
|
@@ -41,11 +41,13 @@ var modelSchema = z.object({
|
|
|
41
41
|
id: z.string().min(1).max(300),
|
|
42
42
|
name: label,
|
|
43
43
|
description: z.string().max(8000).optional(),
|
|
44
|
+
available: z.boolean().optional(),
|
|
44
45
|
contextWindow: z.number().int().positive().optional(),
|
|
45
46
|
maxOutputTokens: z.number().int().positive().optional(),
|
|
46
47
|
inputModalities: z.array(z.string().max(50)).max(20).optional(),
|
|
47
48
|
outputModalities: z.array(z.string().max(50)).max(20).optional(),
|
|
48
|
-
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()
|
|
49
51
|
}).strict();
|
|
50
52
|
var providerInputSchema = z.object({
|
|
51
53
|
id: idSchema,
|
|
@@ -53,10 +55,33 @@ var providerInputSchema = z.object({
|
|
|
53
55
|
baseUrl: urlSchema,
|
|
54
56
|
protocol: protocolSchema,
|
|
55
57
|
credentialEnv: envRef.optional(),
|
|
56
|
-
authStyle: z.enum(["bearer", "x-api-key"]).default("bearer"),
|
|
58
|
+
authStyle: z.enum(["bearer", "x-api-key", "api-key"]).default("bearer"),
|
|
59
|
+
catalogBaseUrl: urlSchema.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(),
|
|
62
|
+
catalogCredentialEnv: envRef.optional(),
|
|
63
|
+
catalogAccountId: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/).optional(),
|
|
57
64
|
modelsPath: z.string().regex(/^[a-zA-Z0-9_/-]+$/).max(200).default("models"),
|
|
58
65
|
manualModels: z.array(modelSchema).max(1e4).default([])
|
|
59
66
|
}).strict().refine((p) => !p.modelsPath.split("/").includes("..") && !p.modelsPath.startsWith("/"), "modelsPath must be relative");
|
|
67
|
+
var providerPresetSchema = z.object({
|
|
68
|
+
id: idSchema,
|
|
69
|
+
name: label,
|
|
70
|
+
credentialEnv: envRef.optional(),
|
|
71
|
+
credentialAliases: z.array(z.string().regex(/^[A-Z][A-Z0-9_]+$/)),
|
|
72
|
+
protocols: z.array(z.object({
|
|
73
|
+
protocol: protocolSchema,
|
|
74
|
+
baseUrl: urlSchema.optional(),
|
|
75
|
+
authStyle: z.enum(["bearer", "x-api-key", "api-key"]),
|
|
76
|
+
catalogBaseUrl: urlSchema.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(),
|
|
79
|
+
modelsPath: z.string(),
|
|
80
|
+
notes: z.array(z.string())
|
|
81
|
+
}).strict()).min(1),
|
|
82
|
+
sources: z.array(z.string().url()),
|
|
83
|
+
verification: z.literal("documented")
|
|
84
|
+
}).strict();
|
|
60
85
|
var profileInputSchema = z.object({
|
|
61
86
|
id: idSchema,
|
|
62
87
|
name: label,
|
|
@@ -91,10 +116,27 @@ function parse(schema, value) {
|
|
|
91
116
|
return result.data;
|
|
92
117
|
}
|
|
93
118
|
function compatible(harness, protocol) {
|
|
94
|
-
|
|
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.");
|
|
95
134
|
}
|
|
96
135
|
function codingEligible(model) {
|
|
97
|
-
return (!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);
|
|
98
140
|
}
|
|
99
141
|
|
|
100
142
|
// src/store.ts
|
|
@@ -116,33 +158,37 @@ class Store {
|
|
|
116
158
|
static async open(config) {
|
|
117
159
|
if (!!config.databaseUrl === !!config.sqlitePath)
|
|
118
160
|
throw new Fault(500, "storage_config", "Choose exactly one PostgreSQL URL or SQLite path.");
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
161
|
+
const engine = config.databaseUrl ? "postgresql" : "sqlite";
|
|
162
|
+
if (config.databaseUrl && !/^postgres(ql)?:\/\//.test(config.databaseUrl))
|
|
163
|
+
throw new Fault(500, "storage_config", "Database URL must use PostgreSQL.");
|
|
164
|
+
const file = config.sqlitePath;
|
|
165
|
+
if (engine === "sqlite" && file !== ":memory:")
|
|
166
|
+
await mkdir(dirname(resolve(file)), { recursive: true, mode: 448 });
|
|
167
|
+
const deadline = Date.now() + 1e4;
|
|
168
|
+
for (let attempt = 0;; attempt++) {
|
|
169
|
+
let sql;
|
|
170
|
+
try {
|
|
171
|
+
sql = engine === "postgresql" ? new SQL(config.databaseUrl) : new SQL({ adapter: "sqlite", filename: file });
|
|
172
|
+
if (engine === "sqlite") {
|
|
173
|
+
await sql.unsafe("PRAGMA busy_timeout = 5000");
|
|
174
|
+
await sql.unsafe("PRAGMA foreign_keys = ON");
|
|
175
|
+
await sql.unsafe("PRAGMA journal_mode = WAL");
|
|
176
|
+
if (file !== ":memory:")
|
|
177
|
+
await chmod(file, 384);
|
|
178
|
+
}
|
|
179
|
+
const store = new Store(sql, engine);
|
|
180
|
+
await store.migrate();
|
|
181
|
+
return store;
|
|
182
|
+
} catch (error) {
|
|
183
|
+
await sql?.close().catch(() => {});
|
|
184
|
+
const code = error?.code;
|
|
185
|
+
if (engine === "sqlite" && ["SQLITE_BUSY", "SQLITE_BUSY_SNAPSHOT", "SQLITE_LOCKED"].includes(code ?? "") && Date.now() < deadline) {
|
|
186
|
+
await new Promise((resolve2) => setTimeout(resolve2, Math.min(200, 20 * (attempt + 1))));
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
throw new Fault(500, "storage_unavailable", "Database startup failed; check configuration, permissions and other database users.");
|
|
190
|
+
}
|
|
144
191
|
}
|
|
145
|
-
return store;
|
|
146
192
|
}
|
|
147
193
|
async migrate() {
|
|
148
194
|
await this.sql.begin(async (tx) => {
|
|
@@ -289,54 +335,172 @@ async function boundedJson(response, maxBytes = MAX_BYTES) {
|
|
|
289
335
|
}
|
|
290
336
|
}
|
|
291
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
|
+
|
|
292
345
|
// src/catalog.ts
|
|
293
346
|
var positive = (v) => typeof v === "number" && Number.isInteger(v) && v > 0 ? v : undefined;
|
|
294
347
|
var strings = (v) => Array.isArray(v) && v.every((i) => typeof i === "string") ? v : undefined;
|
|
295
|
-
|
|
348
|
+
var modalities = (v) => {
|
|
349
|
+
if (v === undefined)
|
|
350
|
+
return;
|
|
351
|
+
if (!Array.isArray(v) || !v.every((i) => typeof i === "string"))
|
|
352
|
+
throw new Fault(502, "invalid_catalog", "Provider returned malformed modality metadata.");
|
|
353
|
+
return v.map((i) => i.toLowerCase());
|
|
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
|
+
}
|
|
413
|
+
async function discover(provider, env = process.env, resolveCredential) {
|
|
296
414
|
const refreshedAt = new Date().toISOString();
|
|
297
415
|
if (provider.manualModels.length)
|
|
298
416
|
return { models: provider.manualModels, source: "manual", refreshedAt };
|
|
417
|
+
if (provider.catalogFormat === "none")
|
|
418
|
+
throw new Fault(422, "catalog_unsupported", "This provider has no documented model-list contract; configure manual models or an explicit catalog URL and parser.");
|
|
299
419
|
const headers = { accept: "application/json" };
|
|
300
|
-
if (provider.
|
|
301
|
-
|
|
420
|
+
if (provider.catalogFormat === "fireworks" && !provider.catalogBaseUrl && !provider.catalogAccountId)
|
|
421
|
+
throw new Fault(422, "catalog_account_required", "Fireworks model discovery requires a catalog account ID or an explicit catalog URL.");
|
|
422
|
+
if (provider.catalogFormat === "fireworks" && !provider.catalogBaseUrl && new URL(provider.baseUrl).origin !== "https://api.fireworks.ai")
|
|
423
|
+
throw new Fault(422, "catalog_url_required", "A custom Fireworks inference authority requires an explicit catalog URL; its deployment prefix cannot be inferred.");
|
|
424
|
+
const catalogRoot = provider.catalogBaseUrl ?? (provider.catalogFormat === "fireworks" ? `https://api.fireworks.ai/v1/accounts/${encodeURIComponent(provider.catalogAccountId)}` : provider.baseUrl);
|
|
425
|
+
const url = new URL(`${catalogRoot}/${provider.modelsPath}`);
|
|
426
|
+
if (provider.catalogFormat === "fireworks")
|
|
427
|
+
url.searchParams.set("pageSize", "200");
|
|
428
|
+
const authStyle = provider.catalogAuthStyle ?? provider.authStyle;
|
|
429
|
+
const credentialEnv = provider.catalogCredentialEnv ?? provider.credentialEnv;
|
|
430
|
+
if (authStyle !== "none" && credentialEnv) {
|
|
431
|
+
if (url.origin !== new URL(provider.baseUrl).origin && !provider.catalogCredentialEnv)
|
|
432
|
+
throw new Fault(422, "catalog_credential_authority", "A different catalog origin requires an explicit catalog credential reference or catalogAuthStyle: none.");
|
|
433
|
+
const credential = resolveCredential ? await resolveCredential({ ...provider, baseUrl: provider.catalogBaseUrl ?? provider.baseUrl, credentialEnv }) : env[credentialEnv];
|
|
302
434
|
if (!credential)
|
|
303
435
|
throw new Fault(422, "credential_missing", "Provider credential environment variable is not available on the server.");
|
|
304
|
-
|
|
436
|
+
if (/[\r\n]/.test(credential))
|
|
437
|
+
throw new Fault(422, "credential_invalid", "Catalog credential contains invalid header characters.");
|
|
438
|
+
const [header, value] = authHeader(authStyle, credential);
|
|
439
|
+
headers[provider.catalogFormat === "gemini" && header === "x-api-key" ? "x-goog-api-key" : header] = value;
|
|
305
440
|
}
|
|
306
|
-
const url = new URL(`${provider.baseUrl}/${provider.modelsPath}`);
|
|
307
441
|
if (provider.protocol === "anthropic-messages" && url.hostname !== "openrouter.ai")
|
|
308
442
|
headers["anthropic-version"] = "2023-06-01";
|
|
309
443
|
if (url.hostname === "openrouter.ai")
|
|
310
444
|
url.searchParams.set("output_modalities", "all");
|
|
311
445
|
const models = new Map;
|
|
312
446
|
const seenCursors = new Set;
|
|
447
|
+
const seenPages = new Set([url.href]);
|
|
448
|
+
const deadline = Date.now() + CATALOG_REFRESH_DEADLINE_MS;
|
|
449
|
+
let fireworksTotal;
|
|
313
450
|
for (let page = 0;page < 100; page++) {
|
|
314
|
-
|
|
315
|
-
try {
|
|
316
|
-
response = await fetch(url, { headers, redirect: "manual", signal: AbortSignal.timeout(20000) });
|
|
317
|
-
} catch {
|
|
318
|
-
throw new Fault(502, "provider_unavailable", "Provider catalog request failed.");
|
|
319
|
-
}
|
|
320
|
-
if (!response.ok) {
|
|
321
|
-
await response.body?.cancel();
|
|
322
|
-
throw new Fault(502, "provider_rejected", `Provider catalog returned HTTP ${response.status}.`);
|
|
323
|
-
}
|
|
451
|
+
const response = await fetchCatalogPage(url, headers, deadline);
|
|
324
452
|
const data = await boundedJson(response);
|
|
325
|
-
if (
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if (
|
|
453
|
+
if (provider.catalogFormat === "fireworks" && data?.totalSize !== undefined) {
|
|
454
|
+
if (typeof data.totalSize !== "number" || !Number.isInteger(data.totalSize) || data.totalSize < 0)
|
|
455
|
+
throw new Fault(502, "invalid_catalog", "Fireworks catalog count metadata is malformed.");
|
|
456
|
+
if (fireworksTotal !== undefined && fireworksTotal !== data.totalSize)
|
|
457
|
+
throw new Fault(502, "incomplete_catalog", "Provider catalog count changed during pagination; retry the refresh.");
|
|
458
|
+
fireworksTotal = data.totalSize;
|
|
459
|
+
}
|
|
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;
|
|
461
|
+
if (!Array.isArray(rows))
|
|
462
|
+
throw new Fault(502, "invalid_catalog", "Expected a provider catalog with a model array matching its configured format.");
|
|
463
|
+
for (const row of rows) {
|
|
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;
|
|
465
|
+
if (typeof id !== "string")
|
|
329
466
|
throw new Fault(502, "invalid_catalog", "Catalog entry is missing a model ID.");
|
|
330
467
|
const candidate = {
|
|
331
|
-
id
|
|
332
|
-
name: row.name ?? row.display_name ??
|
|
468
|
+
id,
|
|
469
|
+
name: row.displayName ?? row.name ?? row.display_name ?? id,
|
|
470
|
+
available: provider.catalogFormat === "mistral" && typeof row.archived === "boolean" ? !row.archived : undefined,
|
|
333
471
|
description: typeof row.description === "string" ? row.description.slice(0, 8000) : undefined,
|
|
334
|
-
contextWindow: positive(row.context_length ?? row.context_window),
|
|
335
|
-
maxOutputTokens: positive(row.top_provider?.max_completion_tokens ?? row.max_output_tokens),
|
|
336
|
-
inputModalities: strings(row.architecture?.input_modalities ?? row.input_modalities),
|
|
337
|
-
outputModalities: strings(row.architecture?.output_modalities ?? row.output_modalities),
|
|
338
|
-
supportedParameters: strings(row.supported_parameters)
|
|
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),
|
|
474
|
+
inputModalities: strings(row.architecture?.input_modalities ?? row.input_modalities) ?? modalities(row.inference_metadata?.request_modality),
|
|
475
|
+
outputModalities: strings(row.architecture?.output_modalities ?? row.output_modalities) ?? modalities(row.inference_metadata?.response_modality),
|
|
476
|
+
supportedParameters: strings(row.supported_parameters),
|
|
477
|
+
...provider.catalogFormat === "gemini" && row.supportedGenerationMethods !== undefined ? { supportedGenerationMethods: row.supportedGenerationMethods } : {}
|
|
339
478
|
};
|
|
479
|
+
if (provider.catalogFormat === "mistral") {
|
|
480
|
+
const capabilities = row.capabilities;
|
|
481
|
+
if (typeof capabilities?.function_calling === "boolean")
|
|
482
|
+
candidate.supportedParameters = capabilities.function_calling ? ["tools"] : [];
|
|
483
|
+
if (typeof capabilities?.vision === "boolean")
|
|
484
|
+
candidate.inputModalities = capabilities.vision ? ["text", "image"] : ["text"];
|
|
485
|
+
if (typeof capabilities?.completion_chat === "boolean")
|
|
486
|
+
candidate.outputModalities = capabilities.completion_chat ? ["text"] : [];
|
|
487
|
+
}
|
|
488
|
+
if (provider.catalogFormat === "together") {
|
|
489
|
+
const modalities2 = { chat: ["text"], language: ["text"], code: ["text"], image: ["image"], audio: ["audio"], video: ["video"], embedding: ["embedding"], rerank: ["rerank"], moderation: ["classification"] };
|
|
490
|
+
candidate.outputModalities = modalities2[row.type];
|
|
491
|
+
}
|
|
492
|
+
if (provider.catalogFormat === "fireworks") {
|
|
493
|
+
if (row.supportsImageInput === true)
|
|
494
|
+
candidate.inputModalities = ["text", "image"];
|
|
495
|
+
if (typeof row.supportsTools === "boolean")
|
|
496
|
+
candidate.supportedParameters = row.supportsTools ? ["tools"] : [];
|
|
497
|
+
}
|
|
498
|
+
if (provider.catalogFormat === "dashscope") {
|
|
499
|
+
if (row.features !== undefined && !Array.isArray(row.features))
|
|
500
|
+
throw new Fault(502, "invalid_catalog", "DashScope model features metadata is malformed.");
|
|
501
|
+
if (Array.isArray(row.features))
|
|
502
|
+
candidate.supportedParameters = row.features.includes("function-calling") ? ["tools"] : [];
|
|
503
|
+
}
|
|
340
504
|
const parsed = modelSchema.safeParse(candidate);
|
|
341
505
|
if (!parsed.success)
|
|
342
506
|
throw new Fault(502, "invalid_catalog", "Provider returned malformed model metadata.");
|
|
@@ -344,8 +508,83 @@ async function discover(provider, env = process.env) {
|
|
|
344
508
|
if (models.size > 1e4)
|
|
345
509
|
throw new Fault(502, "catalog_too_large", "Catalog exceeds 10,000 models; configure a narrower endpoint.");
|
|
346
510
|
}
|
|
347
|
-
|
|
511
|
+
const next = data.links?.next;
|
|
512
|
+
if (next !== undefined && next !== null) {
|
|
513
|
+
if (typeof next !== "string" || !next || next.length > 2000)
|
|
514
|
+
throw new Fault(502, "invalid_catalog", "Provider returned an invalid catalog continuation link.");
|
|
515
|
+
let target;
|
|
516
|
+
try {
|
|
517
|
+
target = new URL(next, url);
|
|
518
|
+
} catch {
|
|
519
|
+
throw new Fault(502, "invalid_catalog", "Provider returned an invalid catalog continuation link.");
|
|
520
|
+
}
|
|
521
|
+
if (target.origin !== url.origin || target.pathname !== url.pathname || target.username || target.password || target.hash)
|
|
522
|
+
throw new Fault(502, "catalog_credential_authority", "Catalog pagination must stay on its original origin and path.");
|
|
523
|
+
if (url.searchParams.has("output_modalities"))
|
|
524
|
+
target.searchParams.set("output_modalities", url.searchParams.get("output_modalities"));
|
|
525
|
+
if (seenPages.has(target.href))
|
|
526
|
+
throw new Fault(502, "invalid_catalog", "Provider catalog pagination did not advance.");
|
|
527
|
+
seenPages.add(target.href);
|
|
528
|
+
url.href = target.href;
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
if (provider.catalogFormat === "fireworks" && data.nextPageToken !== undefined && data.nextPageToken !== null) {
|
|
532
|
+
if (typeof data.nextPageToken !== "string" || data.nextPageToken.length > 2000)
|
|
533
|
+
throw new Fault(502, "invalid_catalog", "Provider catalog pagination did not advance.");
|
|
534
|
+
if (data.nextPageToken) {
|
|
535
|
+
if (seenCursors.has(data.nextPageToken))
|
|
536
|
+
throw new Fault(502, "invalid_catalog", "Provider catalog pagination did not advance.");
|
|
537
|
+
seenCursors.add(data.nextPageToken);
|
|
538
|
+
url.searchParams.set("pageToken", data.nextPageToken);
|
|
539
|
+
url.searchParams.set("pageSize", "200");
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (provider.catalogFormat === "fireworks") {
|
|
544
|
+
if (fireworksTotal !== undefined && fireworksTotal !== models.size)
|
|
545
|
+
throw new Fault(502, "incomplete_catalog", "Provider catalog count does not match the collected models; retry the refresh.");
|
|
546
|
+
return { models: [...models.values()], source: "remote", refreshedAt };
|
|
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
|
+
}
|
|
563
|
+
if (provider.catalogFormat === "dashscope") {
|
|
564
|
+
const output = data.output;
|
|
565
|
+
const total = output?.total;
|
|
566
|
+
const pageNo = output?.page_no;
|
|
567
|
+
const pageSize = output?.page_size;
|
|
568
|
+
if (typeof total !== "number" || !Number.isInteger(total) || total < 0 || typeof pageNo !== "number" || !Number.isInteger(pageNo) || pageNo < 1 || typeof pageSize !== "number" || !Number.isInteger(pageSize) || pageSize < 1)
|
|
569
|
+
throw new Fault(502, "invalid_catalog", "DashScope catalog pagination metadata is malformed.");
|
|
570
|
+
if (models.size < total && pageNo * pageSize < total) {
|
|
571
|
+
const nextPage = pageNo + 1;
|
|
572
|
+
if (seenCursors.has(String(nextPage)))
|
|
573
|
+
throw new Fault(502, "invalid_catalog", "Provider catalog pagination did not advance.");
|
|
574
|
+
seenCursors.add(String(nextPage));
|
|
575
|
+
url.searchParams.set("page_no", String(nextPage));
|
|
576
|
+
url.searchParams.set("page_size", String(pageSize));
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
if (models.size !== total)
|
|
580
|
+
throw new Fault(502, "incomplete_catalog", "Provider catalog count does not match the collected models; retry the refresh.");
|
|
581
|
+
return { models: [...models.values()], source: "remote", refreshedAt };
|
|
582
|
+
}
|
|
583
|
+
if (provider.catalogFormat === "together" || !data.has_more) {
|
|
584
|
+
if (typeof data.total_count === "number" && data.total_count !== models.size)
|
|
585
|
+
throw new Fault(502, "incomplete_catalog", "Provider catalog count does not match the collected models; retry the refresh.");
|
|
348
586
|
return { models: [...models.values()], source: "remote", refreshedAt };
|
|
587
|
+
}
|
|
349
588
|
const cursor = data.last_id;
|
|
350
589
|
if (typeof cursor !== "string" || seenCursors.has(cursor))
|
|
351
590
|
throw new Fault(502, "invalid_catalog", "Provider catalog pagination did not advance.");
|
|
@@ -355,12 +594,103 @@ async function discover(provider, env = process.env) {
|
|
|
355
594
|
}
|
|
356
595
|
throw new Fault(502, "catalog_too_large", "Provider catalog pagination exceeded 100 pages.");
|
|
357
596
|
}
|
|
597
|
+
|
|
598
|
+
// src/presets.ts
|
|
599
|
+
var route = (protocol, baseUrl, options = {}) => ({
|
|
600
|
+
protocol,
|
|
601
|
+
baseUrl,
|
|
602
|
+
authStyle: "bearer",
|
|
603
|
+
catalogFormat: "openai",
|
|
604
|
+
modelsPath: "models",
|
|
605
|
+
notes: [],
|
|
606
|
+
...options
|
|
607
|
+
});
|
|
608
|
+
var preset = (id, name, protocols, sources, alias) => parse(providerPresetSchema, {
|
|
609
|
+
id,
|
|
610
|
+
name,
|
|
611
|
+
protocols,
|
|
612
|
+
sources,
|
|
613
|
+
credentialAliases: alias ? [alias] : [],
|
|
614
|
+
credentialEnv: alias ? `SWITCHER_PROVIDER_${id.toUpperCase().replace(/-/g, "_")}` : undefined,
|
|
615
|
+
verification: "documented"
|
|
616
|
+
});
|
|
617
|
+
var providerPresets = [
|
|
618
|
+
preset("deepseek", "DeepSeek", [
|
|
619
|
+
route("openai-chat", "https://api.deepseek.com", { catalogBaseUrl: "https://api.deepseek.com" }),
|
|
620
|
+
route("anthropic-messages", "https://api.deepseek.com/anthropic/v1", { catalogBaseUrl: "https://api.deepseek.com" })
|
|
621
|
+
], ["https://api-docs.deepseek.com/guides/anthropic_api", "https://api-docs.deepseek.com/api/list-models"], "DEEPSEEK_API_KEY"),
|
|
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"),
|
|
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"),
|
|
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"),
|
|
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"),
|
|
637
|
+
preset("ollama", "Ollama", ["openai-chat", "openai-responses"].map((protocol) => route(protocol, "http://127.0.0.1:11434/v1", {
|
|
638
|
+
catalogBaseUrl: "http://127.0.0.1:11434",
|
|
639
|
+
modelsPath: "api/tags",
|
|
640
|
+
catalogFormat: "ollama",
|
|
641
|
+
catalogAuthStyle: "none",
|
|
642
|
+
notes: protocol === "openai-responses" ? ["Requires Ollama 0.13.3 or newer; only stateless Responses are supported."] : []
|
|
643
|
+
})), ["https://docs.ollama.com/api/openai-compatibility", "https://docs.ollama.com/api/tags"]),
|
|
644
|
+
preset("lmstudio", "LM Studio", ["openai-chat", "openai-responses", "anthropic-messages"].map((protocol) => route(protocol, "http://127.0.0.1:1234/v1", {
|
|
645
|
+
notes: ["Server authentication is optional. Use --credential-env when authentication is enabled."]
|
|
646
|
+
})), ["https://lmstudio.ai/docs/developer/rest"]),
|
|
647
|
+
preset("vllm", "vLLM (operator endpoint)", [
|
|
648
|
+
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."] }),
|
|
649
|
+
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."] }),
|
|
650
|
+
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."] })
|
|
651
|
+
], ["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"]),
|
|
652
|
+
preset("litellm", "LiteLLM Proxy (operator endpoint)", [
|
|
653
|
+
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."] }),
|
|
654
|
+
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."] }),
|
|
655
|
+
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."] })
|
|
656
|
+
], ["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"]),
|
|
657
|
+
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"),
|
|
658
|
+
preset("cerebras", "Cerebras", [route("openai-chat", "https://api.cerebras.ai/v1")], ["https://inference-docs.cerebras.ai/api-reference/chat-completions"], "CEREBRAS_API_KEY"),
|
|
659
|
+
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"),
|
|
660
|
+
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"),
|
|
661
|
+
preset("fireworks", "Fireworks AI", [
|
|
662
|
+
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."] }),
|
|
663
|
+
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."] }),
|
|
664
|
+
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."] })
|
|
665
|
+
], ["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"),
|
|
666
|
+
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"),
|
|
667
|
+
preset("dashscope", "Alibaba Cloud Model Studio (Qwen)", [route("openai-chat", "https://dashscope-us.aliyuncs.com/compatible-mode/v1", {
|
|
668
|
+
catalogFormat: "none",
|
|
669
|
+
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."]
|
|
670
|
+
})], ["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"),
|
|
671
|
+
preset("zai", "Z.AI", [route("openai-chat", "https://api.z.ai/api/paas/v4", {
|
|
672
|
+
catalogFormat: "none",
|
|
673
|
+
notes: ["The published API reference documents inference endpoints but no model-list endpoint; use manual models or provide an explicit catalog URL and parser."]
|
|
674
|
+
})], ["https://docs.z.ai/api-reference/introduction", "https://docs.z.ai/devpack/quick-start"], "ZAI_API_KEY"),
|
|
675
|
+
preset("minimax", "MiniMax", [
|
|
676
|
+
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."] }),
|
|
677
|
+
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."] })
|
|
678
|
+
], ["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"),
|
|
679
|
+
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"),
|
|
680
|
+
...["anthropic-messages", "openai-responses", "openai-chat"].map((protocol) => preset(`generic-${protocol}`, `Custom ${protocol}`, [route(protocol)], []))
|
|
681
|
+
];
|
|
682
|
+
function getProviderPreset(id) {
|
|
683
|
+
const entry = providerPresets.find((p) => p.id === id);
|
|
684
|
+
if (!entry)
|
|
685
|
+
throw new Fault(404, "preset_not_found", "Unknown provider preset. Use switcher providers presets to list available presets.");
|
|
686
|
+
return structuredClone(entry);
|
|
687
|
+
}
|
|
358
688
|
// openapi.json
|
|
359
689
|
var openapi_default = {
|
|
360
690
|
openapi: "3.0.3",
|
|
361
691
|
info: {
|
|
362
692
|
title: "Switcher API",
|
|
363
|
-
version: "0.1.
|
|
693
|
+
version: "0.1.2",
|
|
364
694
|
description: "Authenticated provider/profile/catalog control plane. Launches run locally; the API never returns provider credentials."
|
|
365
695
|
},
|
|
366
696
|
security: [
|
|
@@ -945,6 +1275,82 @@ var openapi_default = {
|
|
|
945
1275
|
}
|
|
946
1276
|
}
|
|
947
1277
|
},
|
|
1278
|
+
"/v1/provider-presets": {
|
|
1279
|
+
get: {
|
|
1280
|
+
operationId: "listProviderPresets",
|
|
1281
|
+
parameters: [],
|
|
1282
|
+
responses: {
|
|
1283
|
+
"200": {
|
|
1284
|
+
description: "Success",
|
|
1285
|
+
content: {
|
|
1286
|
+
"application/json": {
|
|
1287
|
+
schema: {
|
|
1288
|
+
type: "object",
|
|
1289
|
+
required: [
|
|
1290
|
+
"data"
|
|
1291
|
+
],
|
|
1292
|
+
properties: {
|
|
1293
|
+
data: {
|
|
1294
|
+
type: "array",
|
|
1295
|
+
items: {
|
|
1296
|
+
$ref: "#/components/schemas/ProviderPreset"
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
},
|
|
1304
|
+
default: {
|
|
1305
|
+
description: "Structured error",
|
|
1306
|
+
content: {
|
|
1307
|
+
"application/json": {
|
|
1308
|
+
schema: {
|
|
1309
|
+
$ref: "#/components/schemas/Error"
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
},
|
|
1317
|
+
"/v1/provider-presets/{id}": {
|
|
1318
|
+
get: {
|
|
1319
|
+
operationId: "getProviderPreset",
|
|
1320
|
+
parameters: [
|
|
1321
|
+
{
|
|
1322
|
+
name: "id",
|
|
1323
|
+
in: "path",
|
|
1324
|
+
required: true,
|
|
1325
|
+
schema: {
|
|
1326
|
+
type: "string"
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
],
|
|
1330
|
+
responses: {
|
|
1331
|
+
"200": {
|
|
1332
|
+
description: "Success",
|
|
1333
|
+
content: {
|
|
1334
|
+
"application/json": {
|
|
1335
|
+
schema: {
|
|
1336
|
+
$ref: "#/components/schemas/ProviderPreset"
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
},
|
|
1341
|
+
default: {
|
|
1342
|
+
description: "Structured error",
|
|
1343
|
+
content: {
|
|
1344
|
+
"application/json": {
|
|
1345
|
+
schema: {
|
|
1346
|
+
$ref: "#/components/schemas/Error"
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
},
|
|
948
1354
|
"/v1/providers/{id}/models": {
|
|
949
1355
|
get: {
|
|
950
1356
|
operationId: "listModels",
|
|
@@ -1465,6 +1871,126 @@ var openapi_default = {
|
|
|
1465
1871
|
}
|
|
1466
1872
|
},
|
|
1467
1873
|
schemas: {
|
|
1874
|
+
ProviderPreset: {
|
|
1875
|
+
type: "object",
|
|
1876
|
+
properties: {
|
|
1877
|
+
id: {
|
|
1878
|
+
type: "string",
|
|
1879
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$"
|
|
1880
|
+
},
|
|
1881
|
+
name: {
|
|
1882
|
+
type: "string",
|
|
1883
|
+
minLength: 1,
|
|
1884
|
+
maxLength: 200
|
|
1885
|
+
},
|
|
1886
|
+
credentialEnv: {
|
|
1887
|
+
type: "string",
|
|
1888
|
+
pattern: "^SWITCHER_PROVIDER_[A-Z0-9_]+$"
|
|
1889
|
+
},
|
|
1890
|
+
credentialAliases: {
|
|
1891
|
+
type: "array",
|
|
1892
|
+
items: {
|
|
1893
|
+
type: "string",
|
|
1894
|
+
pattern: "^[A-Z][A-Z0-9_]+$"
|
|
1895
|
+
}
|
|
1896
|
+
},
|
|
1897
|
+
protocols: {
|
|
1898
|
+
type: "array",
|
|
1899
|
+
items: {
|
|
1900
|
+
type: "object",
|
|
1901
|
+
properties: {
|
|
1902
|
+
protocol: {
|
|
1903
|
+
type: "string",
|
|
1904
|
+
enum: [
|
|
1905
|
+
"anthropic-messages",
|
|
1906
|
+
"openai-responses",
|
|
1907
|
+
"openai-chat",
|
|
1908
|
+
"gemini-generate-content"
|
|
1909
|
+
]
|
|
1910
|
+
},
|
|
1911
|
+
baseUrl: {
|
|
1912
|
+
type: "string",
|
|
1913
|
+
maxLength: 2000
|
|
1914
|
+
},
|
|
1915
|
+
authStyle: {
|
|
1916
|
+
type: "string",
|
|
1917
|
+
enum: [
|
|
1918
|
+
"bearer",
|
|
1919
|
+
"x-api-key",
|
|
1920
|
+
"api-key"
|
|
1921
|
+
]
|
|
1922
|
+
},
|
|
1923
|
+
catalogBaseUrl: {
|
|
1924
|
+
type: "string",
|
|
1925
|
+
maxLength: 2000
|
|
1926
|
+
},
|
|
1927
|
+
catalogFormat: {
|
|
1928
|
+
type: "string",
|
|
1929
|
+
enum: [
|
|
1930
|
+
"openai",
|
|
1931
|
+
"ollama",
|
|
1932
|
+
"mistral",
|
|
1933
|
+
"together",
|
|
1934
|
+
"fireworks",
|
|
1935
|
+
"dashscope",
|
|
1936
|
+
"gemini",
|
|
1937
|
+
"none"
|
|
1938
|
+
]
|
|
1939
|
+
},
|
|
1940
|
+
catalogAuthStyle: {
|
|
1941
|
+
type: "string",
|
|
1942
|
+
enum: [
|
|
1943
|
+
"bearer",
|
|
1944
|
+
"x-api-key",
|
|
1945
|
+
"api-key",
|
|
1946
|
+
"none"
|
|
1947
|
+
]
|
|
1948
|
+
},
|
|
1949
|
+
modelsPath: {
|
|
1950
|
+
type: "string"
|
|
1951
|
+
},
|
|
1952
|
+
notes: {
|
|
1953
|
+
type: "array",
|
|
1954
|
+
items: {
|
|
1955
|
+
type: "string"
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
},
|
|
1959
|
+
required: [
|
|
1960
|
+
"protocol",
|
|
1961
|
+
"authStyle",
|
|
1962
|
+
"catalogFormat",
|
|
1963
|
+
"modelsPath",
|
|
1964
|
+
"notes"
|
|
1965
|
+
],
|
|
1966
|
+
additionalProperties: false
|
|
1967
|
+
},
|
|
1968
|
+
minItems: 1
|
|
1969
|
+
},
|
|
1970
|
+
sources: {
|
|
1971
|
+
type: "array",
|
|
1972
|
+
items: {
|
|
1973
|
+
type: "string",
|
|
1974
|
+
format: "uri"
|
|
1975
|
+
}
|
|
1976
|
+
},
|
|
1977
|
+
verification: {
|
|
1978
|
+
type: "string",
|
|
1979
|
+
enum: [
|
|
1980
|
+
"documented"
|
|
1981
|
+
]
|
|
1982
|
+
}
|
|
1983
|
+
},
|
|
1984
|
+
required: [
|
|
1985
|
+
"id",
|
|
1986
|
+
"name",
|
|
1987
|
+
"credentialAliases",
|
|
1988
|
+
"protocols",
|
|
1989
|
+
"sources",
|
|
1990
|
+
"verification"
|
|
1991
|
+
],
|
|
1992
|
+
additionalProperties: false
|
|
1993
|
+
},
|
|
1468
1994
|
ProviderInput: {
|
|
1469
1995
|
type: "object",
|
|
1470
1996
|
properties: {
|
|
@@ -1486,7 +2012,8 @@ var openapi_default = {
|
|
|
1486
2012
|
enum: [
|
|
1487
2013
|
"anthropic-messages",
|
|
1488
2014
|
"openai-responses",
|
|
1489
|
-
"openai-chat"
|
|
2015
|
+
"openai-chat",
|
|
2016
|
+
"gemini-generate-content"
|
|
1490
2017
|
]
|
|
1491
2018
|
},
|
|
1492
2019
|
credentialEnv: {
|
|
@@ -1497,10 +2024,45 @@ var openapi_default = {
|
|
|
1497
2024
|
type: "string",
|
|
1498
2025
|
enum: [
|
|
1499
2026
|
"bearer",
|
|
1500
|
-
"x-api-key"
|
|
2027
|
+
"x-api-key",
|
|
2028
|
+
"api-key"
|
|
1501
2029
|
],
|
|
1502
2030
|
default: "bearer"
|
|
1503
2031
|
},
|
|
2032
|
+
catalogBaseUrl: {
|
|
2033
|
+
type: "string",
|
|
2034
|
+
maxLength: 2000
|
|
2035
|
+
},
|
|
2036
|
+
catalogFormat: {
|
|
2037
|
+
type: "string",
|
|
2038
|
+
enum: [
|
|
2039
|
+
"openai",
|
|
2040
|
+
"ollama",
|
|
2041
|
+
"mistral",
|
|
2042
|
+
"together",
|
|
2043
|
+
"fireworks",
|
|
2044
|
+
"dashscope",
|
|
2045
|
+
"gemini",
|
|
2046
|
+
"none"
|
|
2047
|
+
]
|
|
2048
|
+
},
|
|
2049
|
+
catalogAuthStyle: {
|
|
2050
|
+
type: "string",
|
|
2051
|
+
enum: [
|
|
2052
|
+
"bearer",
|
|
2053
|
+
"x-api-key",
|
|
2054
|
+
"api-key",
|
|
2055
|
+
"none"
|
|
2056
|
+
]
|
|
2057
|
+
},
|
|
2058
|
+
catalogCredentialEnv: {
|
|
2059
|
+
type: "string",
|
|
2060
|
+
pattern: "^SWITCHER_PROVIDER_[A-Z0-9_]+$"
|
|
2061
|
+
},
|
|
2062
|
+
catalogAccountId: {
|
|
2063
|
+
type: "string",
|
|
2064
|
+
pattern: "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$"
|
|
2065
|
+
},
|
|
1504
2066
|
modelsPath: {
|
|
1505
2067
|
type: "string",
|
|
1506
2068
|
pattern: "^[a-zA-Z0-9_/-]+$",
|
|
@@ -1526,6 +2088,9 @@ var openapi_default = {
|
|
|
1526
2088
|
type: "string",
|
|
1527
2089
|
maxLength: 8000
|
|
1528
2090
|
},
|
|
2091
|
+
available: {
|
|
2092
|
+
type: "boolean"
|
|
2093
|
+
},
|
|
1529
2094
|
contextWindow: {
|
|
1530
2095
|
type: "integer",
|
|
1531
2096
|
exclusiveMinimum: true,
|
|
@@ -1559,6 +2124,15 @@ var openapi_default = {
|
|
|
1559
2124
|
maxLength: 100
|
|
1560
2125
|
},
|
|
1561
2126
|
maxItems: 100
|
|
2127
|
+
},
|
|
2128
|
+
supportedGenerationMethods: {
|
|
2129
|
+
type: "array",
|
|
2130
|
+
items: {
|
|
2131
|
+
type: "string",
|
|
2132
|
+
minLength: 1,
|
|
2133
|
+
maxLength: 100
|
|
2134
|
+
},
|
|
2135
|
+
maxItems: 100
|
|
1562
2136
|
}
|
|
1563
2137
|
},
|
|
1564
2138
|
required: [
|
|
@@ -1600,7 +2174,8 @@ var openapi_default = {
|
|
|
1600
2174
|
enum: [
|
|
1601
2175
|
"anthropic-messages",
|
|
1602
2176
|
"openai-responses",
|
|
1603
|
-
"openai-chat"
|
|
2177
|
+
"openai-chat",
|
|
2178
|
+
"gemini-generate-content"
|
|
1604
2179
|
]
|
|
1605
2180
|
},
|
|
1606
2181
|
credentialEnv: {
|
|
@@ -1611,10 +2186,45 @@ var openapi_default = {
|
|
|
1611
2186
|
type: "string",
|
|
1612
2187
|
enum: [
|
|
1613
2188
|
"bearer",
|
|
1614
|
-
"x-api-key"
|
|
2189
|
+
"x-api-key",
|
|
2190
|
+
"api-key"
|
|
1615
2191
|
],
|
|
1616
2192
|
default: "bearer"
|
|
1617
2193
|
},
|
|
2194
|
+
catalogBaseUrl: {
|
|
2195
|
+
type: "string",
|
|
2196
|
+
maxLength: 2000
|
|
2197
|
+
},
|
|
2198
|
+
catalogFormat: {
|
|
2199
|
+
type: "string",
|
|
2200
|
+
enum: [
|
|
2201
|
+
"openai",
|
|
2202
|
+
"ollama",
|
|
2203
|
+
"mistral",
|
|
2204
|
+
"together",
|
|
2205
|
+
"fireworks",
|
|
2206
|
+
"dashscope",
|
|
2207
|
+
"gemini",
|
|
2208
|
+
"none"
|
|
2209
|
+
]
|
|
2210
|
+
},
|
|
2211
|
+
catalogAuthStyle: {
|
|
2212
|
+
type: "string",
|
|
2213
|
+
enum: [
|
|
2214
|
+
"bearer",
|
|
2215
|
+
"x-api-key",
|
|
2216
|
+
"api-key",
|
|
2217
|
+
"none"
|
|
2218
|
+
]
|
|
2219
|
+
},
|
|
2220
|
+
catalogCredentialEnv: {
|
|
2221
|
+
type: "string",
|
|
2222
|
+
pattern: "^SWITCHER_PROVIDER_[A-Z0-9_]+$"
|
|
2223
|
+
},
|
|
2224
|
+
catalogAccountId: {
|
|
2225
|
+
type: "string",
|
|
2226
|
+
pattern: "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$"
|
|
2227
|
+
},
|
|
1618
2228
|
modelsPath: {
|
|
1619
2229
|
type: "string",
|
|
1620
2230
|
pattern: "^[a-zA-Z0-9_/-]+$",
|
|
@@ -1640,6 +2250,9 @@ var openapi_default = {
|
|
|
1640
2250
|
type: "string",
|
|
1641
2251
|
maxLength: 8000
|
|
1642
2252
|
},
|
|
2253
|
+
available: {
|
|
2254
|
+
type: "boolean"
|
|
2255
|
+
},
|
|
1643
2256
|
contextWindow: {
|
|
1644
2257
|
type: "integer",
|
|
1645
2258
|
exclusiveMinimum: true,
|
|
@@ -1673,6 +2286,15 @@ var openapi_default = {
|
|
|
1673
2286
|
maxLength: 100
|
|
1674
2287
|
},
|
|
1675
2288
|
maxItems: 100
|
|
2289
|
+
},
|
|
2290
|
+
supportedGenerationMethods: {
|
|
2291
|
+
type: "array",
|
|
2292
|
+
items: {
|
|
2293
|
+
type: "string",
|
|
2294
|
+
minLength: 1,
|
|
2295
|
+
maxLength: 100
|
|
2296
|
+
},
|
|
2297
|
+
maxItems: 100
|
|
1676
2298
|
}
|
|
1677
2299
|
},
|
|
1678
2300
|
required: [
|
|
@@ -1725,7 +2347,17 @@ var openapi_default = {
|
|
|
1725
2347
|
"claude",
|
|
1726
2348
|
"codex",
|
|
1727
2349
|
"grok",
|
|
1728
|
-
"
|
|
2350
|
+
"opencode",
|
|
2351
|
+
"opencode2",
|
|
2352
|
+
"pi",
|
|
2353
|
+
"omp",
|
|
2354
|
+
"dsh",
|
|
2355
|
+
"cline",
|
|
2356
|
+
"hermes",
|
|
2357
|
+
"prime-agent",
|
|
2358
|
+
"gemini",
|
|
2359
|
+
"aider",
|
|
2360
|
+
"kilo"
|
|
1729
2361
|
]
|
|
1730
2362
|
},
|
|
1731
2363
|
model: {
|
|
@@ -1765,7 +2397,17 @@ var openapi_default = {
|
|
|
1765
2397
|
"claude",
|
|
1766
2398
|
"codex",
|
|
1767
2399
|
"grok",
|
|
1768
|
-
"
|
|
2400
|
+
"opencode",
|
|
2401
|
+
"opencode2",
|
|
2402
|
+
"pi",
|
|
2403
|
+
"omp",
|
|
2404
|
+
"dsh",
|
|
2405
|
+
"cline",
|
|
2406
|
+
"hermes",
|
|
2407
|
+
"prime-agent",
|
|
2408
|
+
"gemini",
|
|
2409
|
+
"aider",
|
|
2410
|
+
"kilo"
|
|
1769
2411
|
]
|
|
1770
2412
|
},
|
|
1771
2413
|
model: {
|
|
@@ -1810,6 +2452,9 @@ var openapi_default = {
|
|
|
1810
2452
|
type: "string",
|
|
1811
2453
|
maxLength: 8000
|
|
1812
2454
|
},
|
|
2455
|
+
available: {
|
|
2456
|
+
type: "boolean"
|
|
2457
|
+
},
|
|
1813
2458
|
contextWindow: {
|
|
1814
2459
|
type: "integer",
|
|
1815
2460
|
exclusiveMinimum: true,
|
|
@@ -1843,6 +2488,15 @@ var openapi_default = {
|
|
|
1843
2488
|
maxLength: 100
|
|
1844
2489
|
},
|
|
1845
2490
|
maxItems: 100
|
|
2491
|
+
},
|
|
2492
|
+
supportedGenerationMethods: {
|
|
2493
|
+
type: "array",
|
|
2494
|
+
items: {
|
|
2495
|
+
type: "string",
|
|
2496
|
+
minLength: 1,
|
|
2497
|
+
maxLength: 100
|
|
2498
|
+
},
|
|
2499
|
+
maxItems: 100
|
|
1846
2500
|
}
|
|
1847
2501
|
},
|
|
1848
2502
|
required: [
|
|
@@ -1873,6 +2527,9 @@ var openapi_default = {
|
|
|
1873
2527
|
type: "string",
|
|
1874
2528
|
maxLength: 8000
|
|
1875
2529
|
},
|
|
2530
|
+
available: {
|
|
2531
|
+
type: "boolean"
|
|
2532
|
+
},
|
|
1876
2533
|
contextWindow: {
|
|
1877
2534
|
type: "integer",
|
|
1878
2535
|
exclusiveMinimum: true,
|
|
@@ -1907,6 +2564,15 @@ var openapi_default = {
|
|
|
1907
2564
|
},
|
|
1908
2565
|
maxItems: 100
|
|
1909
2566
|
},
|
|
2567
|
+
supportedGenerationMethods: {
|
|
2568
|
+
type: "array",
|
|
2569
|
+
items: {
|
|
2570
|
+
type: "string",
|
|
2571
|
+
minLength: 1,
|
|
2572
|
+
maxLength: 100
|
|
2573
|
+
},
|
|
2574
|
+
maxItems: 100
|
|
2575
|
+
},
|
|
1910
2576
|
codingEligible: {
|
|
1911
2577
|
type: "boolean"
|
|
1912
2578
|
}
|
|
@@ -1971,6 +2637,9 @@ var openapi_default = {
|
|
|
1971
2637
|
type: "string",
|
|
1972
2638
|
maxLength: 8000
|
|
1973
2639
|
},
|
|
2640
|
+
available: {
|
|
2641
|
+
type: "boolean"
|
|
2642
|
+
},
|
|
1974
2643
|
contextWindow: {
|
|
1975
2644
|
type: "integer",
|
|
1976
2645
|
exclusiveMinimum: true,
|
|
@@ -2004,6 +2673,15 @@ var openapi_default = {
|
|
|
2004
2673
|
maxLength: 100
|
|
2005
2674
|
},
|
|
2006
2675
|
maxItems: 100
|
|
2676
|
+
},
|
|
2677
|
+
supportedGenerationMethods: {
|
|
2678
|
+
type: "array",
|
|
2679
|
+
items: {
|
|
2680
|
+
type: "string",
|
|
2681
|
+
minLength: 1,
|
|
2682
|
+
maxLength: 100
|
|
2683
|
+
},
|
|
2684
|
+
maxItems: 100
|
|
2007
2685
|
}
|
|
2008
2686
|
},
|
|
2009
2687
|
required: [
|
|
@@ -2055,7 +2733,8 @@ var openapi_default = {
|
|
|
2055
2733
|
enum: [
|
|
2056
2734
|
"anthropic-messages",
|
|
2057
2735
|
"openai-responses",
|
|
2058
|
-
"openai-chat"
|
|
2736
|
+
"openai-chat",
|
|
2737
|
+
"gemini-generate-content"
|
|
2059
2738
|
]
|
|
2060
2739
|
},
|
|
2061
2740
|
credentialEnv: {
|
|
@@ -2066,10 +2745,45 @@ var openapi_default = {
|
|
|
2066
2745
|
type: "string",
|
|
2067
2746
|
enum: [
|
|
2068
2747
|
"bearer",
|
|
2069
|
-
"x-api-key"
|
|
2748
|
+
"x-api-key",
|
|
2749
|
+
"api-key"
|
|
2070
2750
|
],
|
|
2071
2751
|
default: "bearer"
|
|
2072
2752
|
},
|
|
2753
|
+
catalogBaseUrl: {
|
|
2754
|
+
type: "string",
|
|
2755
|
+
maxLength: 2000
|
|
2756
|
+
},
|
|
2757
|
+
catalogFormat: {
|
|
2758
|
+
type: "string",
|
|
2759
|
+
enum: [
|
|
2760
|
+
"openai",
|
|
2761
|
+
"ollama",
|
|
2762
|
+
"mistral",
|
|
2763
|
+
"together",
|
|
2764
|
+
"fireworks",
|
|
2765
|
+
"dashscope",
|
|
2766
|
+
"gemini",
|
|
2767
|
+
"none"
|
|
2768
|
+
]
|
|
2769
|
+
},
|
|
2770
|
+
catalogAuthStyle: {
|
|
2771
|
+
type: "string",
|
|
2772
|
+
enum: [
|
|
2773
|
+
"bearer",
|
|
2774
|
+
"x-api-key",
|
|
2775
|
+
"api-key",
|
|
2776
|
+
"none"
|
|
2777
|
+
]
|
|
2778
|
+
},
|
|
2779
|
+
catalogCredentialEnv: {
|
|
2780
|
+
type: "string",
|
|
2781
|
+
pattern: "^SWITCHER_PROVIDER_[A-Z0-9_]+$"
|
|
2782
|
+
},
|
|
2783
|
+
catalogAccountId: {
|
|
2784
|
+
type: "string",
|
|
2785
|
+
pattern: "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$"
|
|
2786
|
+
},
|
|
2073
2787
|
modelsPath: {
|
|
2074
2788
|
type: "string",
|
|
2075
2789
|
pattern: "^[a-zA-Z0-9_/-]+$",
|
|
@@ -2095,6 +2809,9 @@ var openapi_default = {
|
|
|
2095
2809
|
type: "string",
|
|
2096
2810
|
maxLength: 8000
|
|
2097
2811
|
},
|
|
2812
|
+
available: {
|
|
2813
|
+
type: "boolean"
|
|
2814
|
+
},
|
|
2098
2815
|
contextWindow: {
|
|
2099
2816
|
type: "integer",
|
|
2100
2817
|
exclusiveMinimum: true,
|
|
@@ -2128,6 +2845,15 @@ var openapi_default = {
|
|
|
2128
2845
|
maxLength: 100
|
|
2129
2846
|
},
|
|
2130
2847
|
maxItems: 100
|
|
2848
|
+
},
|
|
2849
|
+
supportedGenerationMethods: {
|
|
2850
|
+
type: "array",
|
|
2851
|
+
items: {
|
|
2852
|
+
type: "string",
|
|
2853
|
+
minLength: 1,
|
|
2854
|
+
maxLength: 100
|
|
2855
|
+
},
|
|
2856
|
+
maxItems: 100
|
|
2131
2857
|
}
|
|
2132
2858
|
},
|
|
2133
2859
|
required: [
|
|
@@ -2180,7 +2906,17 @@ var openapi_default = {
|
|
|
2180
2906
|
"claude",
|
|
2181
2907
|
"codex",
|
|
2182
2908
|
"grok",
|
|
2183
|
-
"
|
|
2909
|
+
"opencode",
|
|
2910
|
+
"opencode2",
|
|
2911
|
+
"pi",
|
|
2912
|
+
"omp",
|
|
2913
|
+
"dsh",
|
|
2914
|
+
"cline",
|
|
2915
|
+
"hermes",
|
|
2916
|
+
"prime-agent",
|
|
2917
|
+
"gemini",
|
|
2918
|
+
"aider",
|
|
2919
|
+
"kilo"
|
|
2184
2920
|
]
|
|
2185
2921
|
},
|
|
2186
2922
|
model: {
|
|
@@ -2230,6 +2966,9 @@ var openapi_default = {
|
|
|
2230
2966
|
type: "string",
|
|
2231
2967
|
maxLength: 8000
|
|
2232
2968
|
},
|
|
2969
|
+
available: {
|
|
2970
|
+
type: "boolean"
|
|
2971
|
+
},
|
|
2233
2972
|
contextWindow: {
|
|
2234
2973
|
type: "integer",
|
|
2235
2974
|
exclusiveMinimum: true,
|
|
@@ -2263,6 +3002,15 @@ var openapi_default = {
|
|
|
2263
3002
|
maxLength: 100
|
|
2264
3003
|
},
|
|
2265
3004
|
maxItems: 100
|
|
3005
|
+
},
|
|
3006
|
+
supportedGenerationMethods: {
|
|
3007
|
+
type: "array",
|
|
3008
|
+
items: {
|
|
3009
|
+
type: "string",
|
|
3010
|
+
minLength: 1,
|
|
3011
|
+
maxLength: 100
|
|
3012
|
+
},
|
|
3013
|
+
maxItems: 100
|
|
2266
3014
|
}
|
|
2267
3015
|
},
|
|
2268
3016
|
required: [
|
|
@@ -2322,7 +3070,17 @@ var openapi_default = {
|
|
|
2322
3070
|
"claude",
|
|
2323
3071
|
"codex",
|
|
2324
3072
|
"grok",
|
|
2325
|
-
"
|
|
3073
|
+
"opencode",
|
|
3074
|
+
"opencode2",
|
|
3075
|
+
"pi",
|
|
3076
|
+
"omp",
|
|
3077
|
+
"dsh",
|
|
3078
|
+
"cline",
|
|
3079
|
+
"hermes",
|
|
3080
|
+
"prime-agent",
|
|
3081
|
+
"gemini",
|
|
3082
|
+
"aider",
|
|
3083
|
+
"kilo"
|
|
2326
3084
|
]
|
|
2327
3085
|
},
|
|
2328
3086
|
model: {
|
|
@@ -2379,7 +3137,17 @@ var openapi_default = {
|
|
|
2379
3137
|
"claude",
|
|
2380
3138
|
"codex",
|
|
2381
3139
|
"grok",
|
|
2382
|
-
"
|
|
3140
|
+
"opencode",
|
|
3141
|
+
"opencode2",
|
|
3142
|
+
"pi",
|
|
3143
|
+
"omp",
|
|
3144
|
+
"dsh",
|
|
3145
|
+
"cline",
|
|
3146
|
+
"hermes",
|
|
3147
|
+
"prime-agent",
|
|
3148
|
+
"gemini",
|
|
3149
|
+
"aider",
|
|
3150
|
+
"kilo"
|
|
2383
3151
|
]
|
|
2384
3152
|
},
|
|
2385
3153
|
model: {
|
|
@@ -2562,7 +3330,7 @@ var openapi_default = {
|
|
|
2562
3330
|
// src/service.ts
|
|
2563
3331
|
var snapshot = (profile, provider, catalog) => createHash("sha256").update(JSON.stringify([profile, provider, { models: catalog.models, source: catalog.source }])).digest("hex");
|
|
2564
3332
|
var hash = (s) => createHash("sha256").update(s).digest();
|
|
2565
|
-
function createHandler(store, apiKey, providerEnv = process.env) {
|
|
3333
|
+
function createHandler(store, apiKey, providerEnv = process.env, resolveCredential) {
|
|
2566
3334
|
if (!apiKey || apiKey.length < 24)
|
|
2567
3335
|
throw new Fault(500, "auth_config", "Set HASNA_SWITCHER_API_KEY to a random token of at least 24 characters.");
|
|
2568
3336
|
const expected = hash(`Bearer ${apiKey}`);
|
|
@@ -2571,12 +3339,12 @@ function createHandler(store, apiKey, providerEnv = process.env) {
|
|
|
2571
3339
|
const json = (body, status = 200) => Response.json(body, { status, headers: { "x-request-id": requestId, "cache-control": "no-store", "x-content-type-options": "nosniff" } });
|
|
2572
3340
|
try {
|
|
2573
3341
|
const url = new URL(request.url);
|
|
2574
|
-
const
|
|
2575
|
-
if (request.method === "GET" &&
|
|
3342
|
+
const route2 = url.pathname.replace(/\/$/, "");
|
|
3343
|
+
if (request.method === "GET" && route2 === "/health")
|
|
2576
3344
|
return json({ status: "ok", version: VERSION, backend: store.engine });
|
|
2577
|
-
if (request.method === "GET" &&
|
|
3345
|
+
if (request.method === "GET" && route2 === "/version")
|
|
2578
3346
|
return json({ version: VERSION });
|
|
2579
|
-
if (request.method === "GET" &&
|
|
3347
|
+
if (request.method === "GET" && route2 === "/ready") {
|
|
2580
3348
|
try {
|
|
2581
3349
|
await store.ready();
|
|
2582
3350
|
return json({ ready: true });
|
|
@@ -2586,9 +3354,9 @@ function createHandler(store, apiKey, providerEnv = process.env) {
|
|
|
2586
3354
|
}
|
|
2587
3355
|
if (!timingSafeEqual(expected, hash(request.headers.get("authorization") ?? "")))
|
|
2588
3356
|
throw new Fault(401, "unauthorized", "A valid API bearer token is required.");
|
|
2589
|
-
if (request.method === "GET" && ["/v1/openapi.json", "/openapi.json"].includes(
|
|
3357
|
+
if (request.method === "GET" && ["/v1/openapi.json", "/openapi.json"].includes(route2))
|
|
2590
3358
|
return json(openapi_default);
|
|
2591
|
-
const parts =
|
|
3359
|
+
const parts = route2.split("/").filter(Boolean);
|
|
2592
3360
|
if (parts[0] !== "v1")
|
|
2593
3361
|
throw new Fault(404, "not_found", "Route was not found.");
|
|
2594
3362
|
const resource = parts[1];
|
|
@@ -2601,6 +3369,8 @@ function createHandler(store, apiKey, providerEnv = process.env) {
|
|
|
2601
3369
|
search: z2.string().max(200).default("")
|
|
2602
3370
|
}).strict(), Object.fromEntries(url.searchParams));
|
|
2603
3371
|
if (request.method === "GET") {
|
|
3372
|
+
if (resource === "provider-presets" && parts.length <= 3)
|
|
3373
|
+
return json(id ? getProviderPreset(id) : { data: providerPresets });
|
|
2604
3374
|
if (["providers", "profiles", "runs"].includes(resource) && parts.length <= 3) {
|
|
2605
3375
|
const kind = resource;
|
|
2606
3376
|
return json(id ? await store.get(kind, id) : await store.list(kind, page()));
|
|
@@ -2628,7 +3398,7 @@ function createHandler(store, apiKey, providerEnv = process.env) {
|
|
|
2628
3398
|
throw new Fault(400, "invalid_json", "Request must contain valid JSON under 1 MiB.");
|
|
2629
3399
|
}
|
|
2630
3400
|
}
|
|
2631
|
-
const fingerprint = hash(JSON.stringify([request.method,
|
|
3401
|
+
const fingerprint = hash(JSON.stringify([request.method, route2, body, request.headers.get("if-match")])).toString("hex");
|
|
2632
3402
|
const version = () => {
|
|
2633
3403
|
const v = request.headers.get("if-match");
|
|
2634
3404
|
if (!v || !/^[1-9]\d*$/.test(v))
|
|
@@ -2642,7 +3412,7 @@ function createHandler(store, apiKey, providerEnv = process.env) {
|
|
|
2642
3412
|
if (resource === "providers" && id && parts[3] === "refresh" && parts.length === 4 && request.method === "POST") {
|
|
2643
3413
|
parse(z2.object({}).strict(), body);
|
|
2644
3414
|
const provider = await store.get("providers", id);
|
|
2645
|
-
refreshed = { provider, catalog: await discover(provider, providerEnv) };
|
|
3415
|
+
refreshed = { provider, catalog: await discover(provider, providerEnv, resolveCredential) };
|
|
2646
3416
|
}
|
|
2647
3417
|
const result = await store.mutate(key, fingerprint, async (db) => {
|
|
2648
3418
|
if ((resource === "providers" || resource === "profiles") && parts.length <= 3) {
|
|
@@ -2655,8 +3425,7 @@ function createHandler(store, apiKey, providerEnv = process.env) {
|
|
|
2655
3425
|
if (resource === "profiles") {
|
|
2656
3426
|
const profile = value;
|
|
2657
3427
|
const provider = await store.get("providers", profile.providerId, db);
|
|
2658
|
-
|
|
2659
|
-
throw new Fault(422, "protocol_mismatch", "Harness does not support this provider protocol.");
|
|
3428
|
+
validateHarnessProvider(profile.harness, provider);
|
|
2660
3429
|
}
|
|
2661
3430
|
const saved = await store.put(resource, value, id ? version() : undefined, db);
|
|
2662
3431
|
if (resource === "providers" && id)
|
|
@@ -2684,8 +3453,7 @@ function createHandler(store, apiKey, providerEnv = process.env) {
|
|
|
2684
3453
|
const { profileId } = parse(z2.object({ profileId: idSchema }).strict(), body);
|
|
2685
3454
|
const profile = await store.get("profiles", profileId, db);
|
|
2686
3455
|
const provider = await store.get("providers", profile.providerId, db);
|
|
2687
|
-
|
|
2688
|
-
throw new Fault(422, "protocol_mismatch", "Harness does not support this provider protocol.");
|
|
3456
|
+
validateHarnessProvider(profile.harness, provider);
|
|
2689
3457
|
let catalog;
|
|
2690
3458
|
try {
|
|
2691
3459
|
catalog = await store.get("catalogs", provider.id, db);
|
|
@@ -2697,10 +3465,10 @@ function createHandler(store, apiKey, providerEnv = process.env) {
|
|
|
2697
3465
|
const selected = catalog.models.find((m) => m.id === profile.model);
|
|
2698
3466
|
if (!selected)
|
|
2699
3467
|
throw new Fault(422, "model_missing", "Selected model is not in the provider catalog.");
|
|
2700
|
-
if (!
|
|
2701
|
-
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.");
|
|
2702
3470
|
const warnings = [];
|
|
2703
|
-
if (!selected.supportedParameters)
|
|
3471
|
+
if (profile.harness !== "aider" && !selected.supportedParameters)
|
|
2704
3472
|
warnings.push("Provider does not declare tool capabilities; execution compatibility is unverified.");
|
|
2705
3473
|
if (profile.harness === "claude" && !/claude/i.test(profile.model))
|
|
2706
3474
|
warnings.push("Anthropic does not support non-Claude models in Claude Code; this combination is experimental.");
|
|
@@ -2747,6 +3515,46 @@ function createHandler(store, apiKey, providerEnv = process.env) {
|
|
|
2747
3515
|
};
|
|
2748
3516
|
}
|
|
2749
3517
|
|
|
3518
|
+
// src/server.ts
|
|
3519
|
+
async function startServer(options) {
|
|
3520
|
+
const port = options.port ?? 0;
|
|
3521
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535)
|
|
3522
|
+
throw new Fault(400, "invalid_port", "Port must be an integer between 0 and 65535.");
|
|
3523
|
+
if (options.apiKey.length < 24 || /[\r\n]/.test(options.apiKey))
|
|
3524
|
+
throw new Fault(500, "auth_config", "Use a random operator token of at least 24 characters.");
|
|
3525
|
+
const store = await Store.open({ databaseUrl: options.databaseUrl, sqlitePath: options.sqlitePath });
|
|
3526
|
+
let server;
|
|
3527
|
+
try {
|
|
3528
|
+
server = Bun.serve({
|
|
3529
|
+
hostname: options.hostname ?? "127.0.0.1",
|
|
3530
|
+
port,
|
|
3531
|
+
maxRequestBodySize: 1024 * 1024,
|
|
3532
|
+
idleTimeout: 60,
|
|
3533
|
+
fetch: createHandler(store, options.apiKey, options.providerEnv, options.resolveCredential)
|
|
3534
|
+
});
|
|
3535
|
+
await store.ready();
|
|
3536
|
+
} catch (error) {
|
|
3537
|
+
await server?.stop(true);
|
|
3538
|
+
await store.close();
|
|
3539
|
+
throw error;
|
|
3540
|
+
}
|
|
3541
|
+
const listener = server;
|
|
3542
|
+
let closing;
|
|
3543
|
+
return {
|
|
3544
|
+
url: listener.url.href,
|
|
3545
|
+
storage: store.engine,
|
|
3546
|
+
close() {
|
|
3547
|
+
return closing ??= (async () => {
|
|
3548
|
+
try {
|
|
3549
|
+
await listener.stop(true);
|
|
3550
|
+
} finally {
|
|
3551
|
+
await store.close();
|
|
3552
|
+
}
|
|
3553
|
+
})();
|
|
3554
|
+
}
|
|
3555
|
+
};
|
|
3556
|
+
}
|
|
3557
|
+
|
|
2750
3558
|
// src/serve.ts
|
|
2751
3559
|
async function main(args = process.argv.slice(2)) {
|
|
2752
3560
|
const { values } = parseArgs({ args, options: {
|
|
@@ -2776,16 +3584,19 @@ Requires HASNA_SWITCHER_API_KEY (24+ characters). Provider credentials: SWITCHER
|
|
|
2776
3584
|
throw new Fault(500, "auth_config", "Set HASNA_SWITCHER_API_KEY to a random token of at least 24 characters.");
|
|
2777
3585
|
if (values.sqlite && values["data-dir"])
|
|
2778
3586
|
throw new Fault(400, "storage_config", "Choose --sqlite or --data-dir.");
|
|
2779
|
-
const
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
3587
|
+
const server = await startServer({
|
|
3588
|
+
apiKey,
|
|
3589
|
+
hostname: values.host ?? "127.0.0.1",
|
|
3590
|
+
port,
|
|
3591
|
+
databaseUrl: process.env.HASNA_SWITCHER_DATABASE_URL,
|
|
3592
|
+
sqlitePath: values.sqlite ?? (values["data-dir"] ? join(values["data-dir"], "switcher.db") : process.env.HASNA_SWITCHER_SQLITE_PATH)
|
|
3593
|
+
});
|
|
3594
|
+
console.log(JSON.stringify({ event: "listening", version: VERSION, url: server.url, storage: server.storage }));
|
|
3595
|
+
const stop = () => {
|
|
3596
|
+
server.close().catch(() => {
|
|
3597
|
+
console.error("Server shutdown failed.");
|
|
3598
|
+
process.exitCode = 1;
|
|
3599
|
+
});
|
|
2789
3600
|
};
|
|
2790
3601
|
process.once("SIGTERM", stop);
|
|
2791
3602
|
process.once("SIGINT", stop);
|