@hasna/switcher 0.1.0
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/Dockerfile +11 -0
- package/LICENSE +201 -0
- package/README.md +112 -0
- package/dist/catalog.d.ts +2 -0
- package/dist/cli/index.js +759 -0
- package/dist/cli.d.ts +2 -0
- package/dist/domain.d.ts +231 -0
- package/dist/generated/api.d.ts +1174 -0
- package/dist/harness-types.d.ts +24 -0
- package/dist/harnesses.d.ts +13 -0
- package/dist/http.d.ts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +227 -0
- package/dist/launcher.d.ts +9 -0
- package/dist/mcp/index.js +268 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/sdk.d.ts +311 -0
- package/dist/sdk.js +227 -0
- package/dist/serve/index.js +2800 -0
- package/dist/serve.d.ts +2 -0
- package/dist/service.d.ts +2 -0
- package/dist/store.d.ts +48 -0
- package/docker-compose.yml +41 -0
- package/hasna.contract.json +94 -0
- package/openapi.json +2202 -0
- package/package.json +70 -0
|
@@ -0,0 +1,2800 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/serve.ts
|
|
5
|
+
import { parseArgs } from "util";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
|
|
8
|
+
// src/store.ts
|
|
9
|
+
var {SQL } = globalThis.Bun;
|
|
10
|
+
import { chmod, mkdir } from "fs/promises";
|
|
11
|
+
import { dirname, resolve } from "path";
|
|
12
|
+
|
|
13
|
+
// src/domain.ts
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
var VERSION = "0.1.0";
|
|
16
|
+
var harnessSchema = z.enum(["claude", "codex", "grok", "opencode2"]);
|
|
17
|
+
var protocolSchema = z.enum(["anthropic-messages", "openai-responses", "openai-chat"]);
|
|
18
|
+
var idSchema = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/);
|
|
19
|
+
var label = z.string().min(1).max(200);
|
|
20
|
+
var envRef = z.string().regex(/^SWITCHER_PROVIDER_[A-Z0-9_]+$/);
|
|
21
|
+
function endpoint(value) {
|
|
22
|
+
let url;
|
|
23
|
+
try {
|
|
24
|
+
url = new URL(value);
|
|
25
|
+
} catch {
|
|
26
|
+
throw new Fault(400, "invalid_url", "Use an absolute HTTPS URL (HTTP is allowed on loopback).");
|
|
27
|
+
}
|
|
28
|
+
const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
29
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && local) || url.username || url.password || url.search || url.hash)
|
|
30
|
+
throw new Fault(400, "invalid_url", "URL must use HTTPS, contain no credentials/query/fragment, or use HTTP on loopback.");
|
|
31
|
+
return url.href.replace(/\/+$/, "");
|
|
32
|
+
}
|
|
33
|
+
var urlSchema = z.string().max(2000).superRefine((v, ctx) => {
|
|
34
|
+
try {
|
|
35
|
+
endpoint(v);
|
|
36
|
+
} catch {
|
|
37
|
+
ctx.addIssue({ code: "custom", message: "Invalid endpoint URL" });
|
|
38
|
+
}
|
|
39
|
+
}).transform(endpoint);
|
|
40
|
+
var modelSchema = z.object({
|
|
41
|
+
id: z.string().min(1).max(300),
|
|
42
|
+
name: label,
|
|
43
|
+
description: z.string().max(8000).optional(),
|
|
44
|
+
contextWindow: z.number().int().positive().optional(),
|
|
45
|
+
maxOutputTokens: z.number().int().positive().optional(),
|
|
46
|
+
inputModalities: z.array(z.string().max(50)).max(20).optional(),
|
|
47
|
+
outputModalities: z.array(z.string().max(50)).max(20).optional(),
|
|
48
|
+
supportedParameters: z.array(z.string().max(100)).max(100).optional()
|
|
49
|
+
}).strict();
|
|
50
|
+
var providerInputSchema = z.object({
|
|
51
|
+
id: idSchema,
|
|
52
|
+
name: label,
|
|
53
|
+
baseUrl: urlSchema,
|
|
54
|
+
protocol: protocolSchema,
|
|
55
|
+
credentialEnv: envRef.optional(),
|
|
56
|
+
authStyle: z.enum(["bearer", "x-api-key"]).default("bearer"),
|
|
57
|
+
modelsPath: z.string().regex(/^[a-zA-Z0-9_/-]+$/).max(200).default("models"),
|
|
58
|
+
manualModels: z.array(modelSchema).max(1e4).default([])
|
|
59
|
+
}).strict().refine((p) => !p.modelsPath.split("/").includes("..") && !p.modelsPath.startsWith("/"), "modelsPath must be relative");
|
|
60
|
+
var profileInputSchema = z.object({
|
|
61
|
+
id: idSchema,
|
|
62
|
+
name: label,
|
|
63
|
+
providerId: idSchema,
|
|
64
|
+
harness: harnessSchema,
|
|
65
|
+
model: z.string().min(1).max(300)
|
|
66
|
+
}).strict();
|
|
67
|
+
var runInputSchema = z.object({
|
|
68
|
+
profileId: idSchema,
|
|
69
|
+
harness: harnessSchema,
|
|
70
|
+
model: z.string().min(1).max(300),
|
|
71
|
+
planToken: z.string().regex(/^[a-f0-9]{64}$/)
|
|
72
|
+
}).strict();
|
|
73
|
+
var runUpdateSchema = z.object({
|
|
74
|
+
status: z.enum(["exited", "failed", "interrupted"]),
|
|
75
|
+
exitCode: z.number().int().min(0).max(255)
|
|
76
|
+
}).strict();
|
|
77
|
+
|
|
78
|
+
class Fault extends Error {
|
|
79
|
+
status;
|
|
80
|
+
code;
|
|
81
|
+
constructor(status, code, message) {
|
|
82
|
+
super(message);
|
|
83
|
+
this.status = status;
|
|
84
|
+
this.code = code;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function parse(schema, value) {
|
|
88
|
+
const result = schema.safeParse(value);
|
|
89
|
+
if (!result.success)
|
|
90
|
+
throw new Fault(400, "invalid_request", result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; "));
|
|
91
|
+
return result.data;
|
|
92
|
+
}
|
|
93
|
+
function compatible(harness, protocol) {
|
|
94
|
+
return harness === "claude" ? protocol === "anthropic-messages" : harness === "codex" ? protocol === "openai-responses" : true;
|
|
95
|
+
}
|
|
96
|
+
function codingEligible(model) {
|
|
97
|
+
return (!model.outputModalities || model.outputModalities.includes("text")) && (!model.supportedParameters || model.supportedParameters.includes("tools"));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/store.ts
|
|
101
|
+
class Store {
|
|
102
|
+
tail = Promise.resolve();
|
|
103
|
+
async exclusive(action) {
|
|
104
|
+
if (this.engine !== "sqlite")
|
|
105
|
+
return action();
|
|
106
|
+
const next = this.tail.then(action, action);
|
|
107
|
+
this.tail = next.catch(() => {});
|
|
108
|
+
return next;
|
|
109
|
+
}
|
|
110
|
+
sql;
|
|
111
|
+
engine;
|
|
112
|
+
constructor(sql, engine) {
|
|
113
|
+
this.sql = sql;
|
|
114
|
+
this.engine = engine;
|
|
115
|
+
}
|
|
116
|
+
static async open(config) {
|
|
117
|
+
if (!!config.databaseUrl === !!config.sqlitePath)
|
|
118
|
+
throw new Fault(500, "storage_config", "Choose exactly one PostgreSQL URL or SQLite path.");
|
|
119
|
+
let sql;
|
|
120
|
+
let engine;
|
|
121
|
+
if (config.databaseUrl) {
|
|
122
|
+
if (!/^postgres(ql)?:\/\//.test(config.databaseUrl))
|
|
123
|
+
throw new Fault(500, "storage_config", "Database URL must use PostgreSQL.");
|
|
124
|
+
sql = new SQL(config.databaseUrl);
|
|
125
|
+
engine = "postgresql";
|
|
126
|
+
} else {
|
|
127
|
+
const file = config.sqlitePath;
|
|
128
|
+
if (file !== ":memory:")
|
|
129
|
+
await mkdir(dirname(resolve(file)), { recursive: true, mode: 448 });
|
|
130
|
+
sql = new SQL({ adapter: "sqlite", filename: file });
|
|
131
|
+
engine = "sqlite";
|
|
132
|
+
await sql.unsafe("PRAGMA foreign_keys = ON");
|
|
133
|
+
await sql.unsafe("PRAGMA busy_timeout = 5000");
|
|
134
|
+
await sql.unsafe("PRAGMA journal_mode = WAL");
|
|
135
|
+
if (file !== ":memory:")
|
|
136
|
+
await chmod(file, 384);
|
|
137
|
+
}
|
|
138
|
+
const store = new Store(sql, engine);
|
|
139
|
+
try {
|
|
140
|
+
await store.migrate();
|
|
141
|
+
} catch {
|
|
142
|
+
await sql.close();
|
|
143
|
+
throw new Fault(500, "storage_unavailable", "Database migration failed; check server database configuration.");
|
|
144
|
+
}
|
|
145
|
+
return store;
|
|
146
|
+
}
|
|
147
|
+
async migrate() {
|
|
148
|
+
await this.sql.begin(async (tx) => {
|
|
149
|
+
if (this.engine === "postgresql")
|
|
150
|
+
await tx.unsafe("SELECT pg_advisory_xact_lock(782034215)");
|
|
151
|
+
await tx.unsafe("CREATE TABLE IF NOT EXISTS switcher_migrations (version INTEGER PRIMARY KEY)");
|
|
152
|
+
const versions = await tx.unsafe("SELECT version FROM switcher_migrations ORDER BY version");
|
|
153
|
+
if (versions.some((v) => v.version > 1))
|
|
154
|
+
throw new Error("Newer database schema");
|
|
155
|
+
if (versions.length)
|
|
156
|
+
return;
|
|
157
|
+
await tx.unsafe("CREATE TABLE switcher_providers (id TEXT PRIMARY KEY, version INTEGER NOT NULL, updated_at TEXT NOT NULL, payload TEXT NOT NULL)");
|
|
158
|
+
await tx.unsafe("CREATE TABLE switcher_profiles (id TEXT PRIMARY KEY, provider_id TEXT NOT NULL REFERENCES switcher_providers(id), version INTEGER NOT NULL, updated_at TEXT NOT NULL, payload TEXT NOT NULL)");
|
|
159
|
+
await tx.unsafe("CREATE TABLE switcher_catalogs (id TEXT PRIMARY KEY REFERENCES switcher_providers(id) ON DELETE CASCADE, version INTEGER NOT NULL, updated_at TEXT NOT NULL, payload TEXT NOT NULL)");
|
|
160
|
+
await tx.unsafe("CREATE TABLE switcher_runs (id TEXT PRIMARY KEY, profile_id TEXT NOT NULL REFERENCES switcher_profiles(id), version INTEGER NOT NULL, updated_at TEXT NOT NULL, payload TEXT NOT NULL)");
|
|
161
|
+
await tx.unsafe("CREATE TABLE switcher_idempotency (key TEXT PRIMARY KEY, fingerprint TEXT NOT NULL, payload TEXT NOT NULL, created_at TEXT NOT NULL)");
|
|
162
|
+
await tx.unsafe("INSERT INTO switcher_migrations(version) VALUES (1)");
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
async ready() {
|
|
166
|
+
await this.exclusive(async () => {
|
|
167
|
+
await this.sql.unsafe("SELECT 1");
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
async close() {
|
|
171
|
+
await this.tail;
|
|
172
|
+
await this.sql.close();
|
|
173
|
+
}
|
|
174
|
+
async get(kind, id, db = this.sql) {
|
|
175
|
+
if (db === this.sql && this.engine === "sqlite")
|
|
176
|
+
return this.exclusive(() => this.read(kind, id, db));
|
|
177
|
+
return this.read(kind, id, db);
|
|
178
|
+
}
|
|
179
|
+
async read(kind, id, db) {
|
|
180
|
+
const rows = await db.unsafe(`SELECT payload FROM switcher_${kind} WHERE id = $1`, [id]);
|
|
181
|
+
if (!rows.length)
|
|
182
|
+
throw new Fault(404, "not_found", `${kind} entry was not found.`);
|
|
183
|
+
return JSON.parse(rows[0].payload);
|
|
184
|
+
}
|
|
185
|
+
async list(kind, { limit = 100, offset = 0, search = "" } = {}) {
|
|
186
|
+
return this.exclusive(async () => {
|
|
187
|
+
const name = this.engine === "sqlite" ? "json_extract(payload, '$.name')" : "payload::jsonb->>'name'";
|
|
188
|
+
const pattern = `%${search.toLowerCase().replace(/[!%_]/g, (c) => "!" + c)}%`;
|
|
189
|
+
const where = `LOWER(id) LIKE $1 ESCAPE '!' OR LOWER(COALESCE(${name}, '')) LIKE $1 ESCAPE '!'`;
|
|
190
|
+
const total = await this.sql.unsafe(`SELECT COUNT(*) AS total FROM switcher_${kind} WHERE ${where}`, [pattern]);
|
|
191
|
+
const rows = await this.sql.unsafe(`SELECT payload FROM switcher_${kind} WHERE ${where} ORDER BY id LIMIT $2 OFFSET $3`, [pattern, limit, offset]);
|
|
192
|
+
return { data: rows.map((r) => JSON.parse(r.payload)), total: Number(total[0].total), limit, offset };
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
async put(kind, input, expectedVersion, db) {
|
|
196
|
+
const now = new Date().toISOString();
|
|
197
|
+
const value = { ...input, version: expectedVersion === undefined ? 1 : expectedVersion + 1, updatedAt: now };
|
|
198
|
+
if (expectedVersion === undefined) {
|
|
199
|
+
const extraColumn = kind === "profiles" ? ", provider_id" : kind === "runs" ? ", profile_id" : "";
|
|
200
|
+
const extraValue = kind === "profiles" ? input.providerId : kind === "runs" ? input.profileId : undefined;
|
|
201
|
+
await db.unsafe(`INSERT INTO switcher_${kind} (id, version, updated_at, payload${extraColumn}) VALUES ($1, $2, $3, $4${extraColumn ? ", $5" : ""})`, [input.id, value.version, now, JSON.stringify(value), ...extraColumn ? [extraValue] : []]);
|
|
202
|
+
} else {
|
|
203
|
+
const extra = kind === "profiles" ? ", provider_id = $4" : "";
|
|
204
|
+
const rows = await db.unsafe(`UPDATE switcher_${kind} SET version = $1, updated_at = $2, payload = $3${extra} WHERE id = $${extra ? 5 : 4} AND version = $${extra ? 6 : 5} RETURNING id`, [value.version, now, JSON.stringify(value), ...extra ? [input.providerId] : [], input.id, expectedVersion]);
|
|
205
|
+
if (!rows.length)
|
|
206
|
+
throw new Fault(409, "version_conflict", "Entry changed or is missing; reload before retrying.");
|
|
207
|
+
}
|
|
208
|
+
return value;
|
|
209
|
+
}
|
|
210
|
+
async remove(kind, id, version, db) {
|
|
211
|
+
const rows = await db.unsafe(`DELETE FROM switcher_${kind} WHERE id = $1 AND version = $2 RETURNING id`, [id, version]);
|
|
212
|
+
if (!rows.length)
|
|
213
|
+
throw new Fault(409, "version_conflict", "Entry changed or is missing; reload before retrying.");
|
|
214
|
+
return { deleted: id };
|
|
215
|
+
}
|
|
216
|
+
async mutate(key, fingerprint, action) {
|
|
217
|
+
return this.exclusive(() => this.transaction(key, fingerprint, action));
|
|
218
|
+
}
|
|
219
|
+
async replay(key, fingerprint) {
|
|
220
|
+
return this.exclusive(async () => {
|
|
221
|
+
const rows = await this.sql.unsafe("SELECT fingerprint, payload FROM switcher_idempotency WHERE key = $1", [key]);
|
|
222
|
+
if (!rows.length)
|
|
223
|
+
return { found: false };
|
|
224
|
+
if (rows[0].fingerprint !== fingerprint)
|
|
225
|
+
throw new Fault(409, "idempotency_conflict", "Idempotency key was already used for a different request.");
|
|
226
|
+
return { found: true, value: JSON.parse(rows[0].payload) };
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
async transaction(key, fingerprint, action) {
|
|
230
|
+
try {
|
|
231
|
+
return await this.sql.begin(async (tx) => {
|
|
232
|
+
const inserted = await tx.unsafe("INSERT INTO switcher_idempotency (key, fingerprint, payload, created_at) VALUES ($1, $2, '', $3) ON CONFLICT (key) DO NOTHING RETURNING key", [key, fingerprint, new Date().toISOString()]);
|
|
233
|
+
if (!inserted.length) {
|
|
234
|
+
const rows = await tx.unsafe("SELECT fingerprint, payload FROM switcher_idempotency WHERE key = $1", [key]);
|
|
235
|
+
if (rows[0].fingerprint !== fingerprint)
|
|
236
|
+
throw new Fault(409, "idempotency_conflict", "Idempotency key was already used for a different request.");
|
|
237
|
+
return JSON.parse(rows[0].payload);
|
|
238
|
+
}
|
|
239
|
+
const value = await action(tx);
|
|
240
|
+
await tx.unsafe("UPDATE switcher_idempotency SET payload = $1 WHERE key = $2", [JSON.stringify(value), key]);
|
|
241
|
+
return value;
|
|
242
|
+
});
|
|
243
|
+
} catch (error) {
|
|
244
|
+
if (error instanceof Fault)
|
|
245
|
+
throw error;
|
|
246
|
+
const code = String(this.engine === "postgresql" ? error?.errno : error?.code);
|
|
247
|
+
if (["23505", "23503", "SQLITE_CONSTRAINT", "SQLITE_CONSTRAINT_PRIMARYKEY", "SQLITE_CONSTRAINT_FOREIGNKEY"].includes(code))
|
|
248
|
+
throw new Fault(409, "conflict", "Duplicate entry or an entry still referenced by another resource.");
|
|
249
|
+
throw new Fault(503, "storage_unavailable", "Storage operation failed.");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/service.ts
|
|
255
|
+
import { createHash, timingSafeEqual } from "crypto";
|
|
256
|
+
import { z as z2 } from "zod";
|
|
257
|
+
|
|
258
|
+
// src/http.ts
|
|
259
|
+
var MAX_BYTES = 16 * 1024 * 1024;
|
|
260
|
+
async function boundedJson(response, maxBytes = MAX_BYTES) {
|
|
261
|
+
if (!response.body)
|
|
262
|
+
throw new Fault(502, "invalid_upstream", "Upstream returned no body.");
|
|
263
|
+
const reader = response.body.getReader();
|
|
264
|
+
const chunks = [];
|
|
265
|
+
let size = 0;
|
|
266
|
+
try {
|
|
267
|
+
while (true) {
|
|
268
|
+
const item = await reader.read();
|
|
269
|
+
if (item.done)
|
|
270
|
+
break;
|
|
271
|
+
size += item.value.byteLength;
|
|
272
|
+
if (size > maxBytes)
|
|
273
|
+
throw new Fault(502, "response_too_large", "Response exceeds the size limit.");
|
|
274
|
+
chunks.push(item.value);
|
|
275
|
+
}
|
|
276
|
+
const bytes = new Uint8Array(size);
|
|
277
|
+
let offset = 0;
|
|
278
|
+
for (const chunk of chunks) {
|
|
279
|
+
bytes.set(chunk, offset);
|
|
280
|
+
offset += chunk.length;
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
284
|
+
} catch {
|
|
285
|
+
throw new Fault(502, "invalid_upstream", "Upstream returned invalid JSON.");
|
|
286
|
+
}
|
|
287
|
+
} finally {
|
|
288
|
+
await reader.cancel().catch(() => {});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/catalog.ts
|
|
293
|
+
var positive = (v) => typeof v === "number" && Number.isInteger(v) && v > 0 ? v : undefined;
|
|
294
|
+
var strings = (v) => Array.isArray(v) && v.every((i) => typeof i === "string") ? v : undefined;
|
|
295
|
+
async function discover(provider, env = process.env) {
|
|
296
|
+
const refreshedAt = new Date().toISOString();
|
|
297
|
+
if (provider.manualModels.length)
|
|
298
|
+
return { models: provider.manualModels, source: "manual", refreshedAt };
|
|
299
|
+
const headers = { accept: "application/json" };
|
|
300
|
+
if (provider.credentialEnv) {
|
|
301
|
+
const credential = env[provider.credentialEnv];
|
|
302
|
+
if (!credential)
|
|
303
|
+
throw new Fault(422, "credential_missing", "Provider credential environment variable is not available on the server.");
|
|
304
|
+
headers[provider.authStyle === "x-api-key" ? "x-api-key" : "authorization"] = provider.authStyle === "x-api-key" ? credential : `Bearer ${credential}`;
|
|
305
|
+
}
|
|
306
|
+
const url = new URL(`${provider.baseUrl}/${provider.modelsPath}`);
|
|
307
|
+
if (provider.protocol === "anthropic-messages" && url.hostname !== "openrouter.ai")
|
|
308
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
309
|
+
if (url.hostname === "openrouter.ai")
|
|
310
|
+
url.searchParams.set("output_modalities", "all");
|
|
311
|
+
const models = new Map;
|
|
312
|
+
const seenCursors = new Set;
|
|
313
|
+
for (let page = 0;page < 100; page++) {
|
|
314
|
+
let response;
|
|
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
|
+
}
|
|
324
|
+
const data = await boundedJson(response);
|
|
325
|
+
if (!Array.isArray(data.data))
|
|
326
|
+
throw new Fault(502, "invalid_catalog", "Expected a provider catalog with a data array.");
|
|
327
|
+
for (const row of data.data) {
|
|
328
|
+
if (!row || typeof row.id !== "string")
|
|
329
|
+
throw new Fault(502, "invalid_catalog", "Catalog entry is missing a model ID.");
|
|
330
|
+
const candidate = {
|
|
331
|
+
id: row.id,
|
|
332
|
+
name: row.name ?? row.display_name ?? row.id,
|
|
333
|
+
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)
|
|
339
|
+
};
|
|
340
|
+
const parsed = modelSchema.safeParse(candidate);
|
|
341
|
+
if (!parsed.success)
|
|
342
|
+
throw new Fault(502, "invalid_catalog", "Provider returned malformed model metadata.");
|
|
343
|
+
models.set(candidate.id, parsed.data);
|
|
344
|
+
if (models.size > 1e4)
|
|
345
|
+
throw new Fault(502, "catalog_too_large", "Catalog exceeds 10,000 models; configure a narrower endpoint.");
|
|
346
|
+
}
|
|
347
|
+
if (!data.has_more)
|
|
348
|
+
return { models: [...models.values()], source: "remote", refreshedAt };
|
|
349
|
+
const cursor = data.last_id;
|
|
350
|
+
if (typeof cursor !== "string" || seenCursors.has(cursor))
|
|
351
|
+
throw new Fault(502, "invalid_catalog", "Provider catalog pagination did not advance.");
|
|
352
|
+
seenCursors.add(cursor);
|
|
353
|
+
url.searchParams.set("after_id", cursor);
|
|
354
|
+
url.searchParams.set("limit", "1000");
|
|
355
|
+
}
|
|
356
|
+
throw new Fault(502, "catalog_too_large", "Provider catalog pagination exceeded 100 pages.");
|
|
357
|
+
}
|
|
358
|
+
// openapi.json
|
|
359
|
+
var openapi_default = {
|
|
360
|
+
openapi: "3.0.3",
|
|
361
|
+
info: {
|
|
362
|
+
title: "Switcher API",
|
|
363
|
+
version: "0.1.0",
|
|
364
|
+
description: "Authenticated provider/profile/catalog control plane. Launches run locally; the API never returns provider credentials."
|
|
365
|
+
},
|
|
366
|
+
security: [
|
|
367
|
+
{
|
|
368
|
+
bearerAuth: []
|
|
369
|
+
}
|
|
370
|
+
],
|
|
371
|
+
paths: {
|
|
372
|
+
"/v1/providers": {
|
|
373
|
+
get: {
|
|
374
|
+
operationId: "listProviders",
|
|
375
|
+
parameters: [
|
|
376
|
+
{
|
|
377
|
+
name: "limit",
|
|
378
|
+
in: "query",
|
|
379
|
+
schema: {
|
|
380
|
+
type: "integer",
|
|
381
|
+
minimum: 1,
|
|
382
|
+
maximum: 1000,
|
|
383
|
+
default: 100
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
name: "offset",
|
|
388
|
+
in: "query",
|
|
389
|
+
schema: {
|
|
390
|
+
type: "integer",
|
|
391
|
+
minimum: 0,
|
|
392
|
+
default: 0
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
name: "search",
|
|
397
|
+
in: "query",
|
|
398
|
+
schema: {
|
|
399
|
+
type: "string"
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
],
|
|
403
|
+
responses: {
|
|
404
|
+
"200": {
|
|
405
|
+
description: "Success",
|
|
406
|
+
content: {
|
|
407
|
+
"application/json": {
|
|
408
|
+
schema: {
|
|
409
|
+
type: "object",
|
|
410
|
+
required: [
|
|
411
|
+
"data",
|
|
412
|
+
"total",
|
|
413
|
+
"limit",
|
|
414
|
+
"offset"
|
|
415
|
+
],
|
|
416
|
+
properties: {
|
|
417
|
+
data: {
|
|
418
|
+
type: "array",
|
|
419
|
+
items: {
|
|
420
|
+
$ref: "#/components/schemas/Provider"
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
total: {
|
|
424
|
+
type: "integer"
|
|
425
|
+
},
|
|
426
|
+
limit: {
|
|
427
|
+
type: "integer"
|
|
428
|
+
},
|
|
429
|
+
offset: {
|
|
430
|
+
type: "integer"
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
default: {
|
|
438
|
+
description: "Structured error",
|
|
439
|
+
content: {
|
|
440
|
+
"application/json": {
|
|
441
|
+
schema: {
|
|
442
|
+
$ref: "#/components/schemas/Error"
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
post: {
|
|
450
|
+
operationId: "createProvider",
|
|
451
|
+
parameters: [
|
|
452
|
+
{
|
|
453
|
+
name: "Idempotency-Key",
|
|
454
|
+
in: "header",
|
|
455
|
+
required: true,
|
|
456
|
+
schema: {
|
|
457
|
+
type: "string",
|
|
458
|
+
minLength: 8,
|
|
459
|
+
maxLength: 128
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
],
|
|
463
|
+
requestBody: {
|
|
464
|
+
required: true,
|
|
465
|
+
content: {
|
|
466
|
+
"application/json": {
|
|
467
|
+
schema: {
|
|
468
|
+
$ref: "#/components/schemas/ProviderInput"
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
responses: {
|
|
474
|
+
"201": {
|
|
475
|
+
description: "Success",
|
|
476
|
+
content: {
|
|
477
|
+
"application/json": {
|
|
478
|
+
schema: {
|
|
479
|
+
$ref: "#/components/schemas/Provider"
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
default: {
|
|
485
|
+
description: "Structured error",
|
|
486
|
+
content: {
|
|
487
|
+
"application/json": {
|
|
488
|
+
schema: {
|
|
489
|
+
$ref: "#/components/schemas/Error"
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
"/v1/providers/{id}": {
|
|
498
|
+
get: {
|
|
499
|
+
operationId: "getProvider",
|
|
500
|
+
parameters: [
|
|
501
|
+
{
|
|
502
|
+
name: "id",
|
|
503
|
+
in: "path",
|
|
504
|
+
required: true,
|
|
505
|
+
schema: {
|
|
506
|
+
type: "string"
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
],
|
|
510
|
+
responses: {
|
|
511
|
+
"200": {
|
|
512
|
+
description: "Success",
|
|
513
|
+
content: {
|
|
514
|
+
"application/json": {
|
|
515
|
+
schema: {
|
|
516
|
+
$ref: "#/components/schemas/Provider"
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
default: {
|
|
522
|
+
description: "Structured error",
|
|
523
|
+
content: {
|
|
524
|
+
"application/json": {
|
|
525
|
+
schema: {
|
|
526
|
+
$ref: "#/components/schemas/Error"
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
put: {
|
|
534
|
+
operationId: "updateProvider",
|
|
535
|
+
parameters: [
|
|
536
|
+
{
|
|
537
|
+
name: "id",
|
|
538
|
+
in: "path",
|
|
539
|
+
required: true,
|
|
540
|
+
schema: {
|
|
541
|
+
type: "string"
|
|
542
|
+
}
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
name: "Idempotency-Key",
|
|
546
|
+
in: "header",
|
|
547
|
+
required: true,
|
|
548
|
+
schema: {
|
|
549
|
+
type: "string",
|
|
550
|
+
minLength: 8,
|
|
551
|
+
maxLength: 128
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
name: "If-Match",
|
|
556
|
+
in: "header",
|
|
557
|
+
required: true,
|
|
558
|
+
schema: {
|
|
559
|
+
type: "integer",
|
|
560
|
+
minimum: 1
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
],
|
|
564
|
+
requestBody: {
|
|
565
|
+
required: true,
|
|
566
|
+
content: {
|
|
567
|
+
"application/json": {
|
|
568
|
+
schema: {
|
|
569
|
+
$ref: "#/components/schemas/ProviderInput"
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
responses: {
|
|
575
|
+
"200": {
|
|
576
|
+
description: "Success",
|
|
577
|
+
content: {
|
|
578
|
+
"application/json": {
|
|
579
|
+
schema: {
|
|
580
|
+
$ref: "#/components/schemas/Provider"
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
},
|
|
585
|
+
default: {
|
|
586
|
+
description: "Structured error",
|
|
587
|
+
content: {
|
|
588
|
+
"application/json": {
|
|
589
|
+
schema: {
|
|
590
|
+
$ref: "#/components/schemas/Error"
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
},
|
|
597
|
+
delete: {
|
|
598
|
+
operationId: "deleteProvider",
|
|
599
|
+
parameters: [
|
|
600
|
+
{
|
|
601
|
+
name: "id",
|
|
602
|
+
in: "path",
|
|
603
|
+
required: true,
|
|
604
|
+
schema: {
|
|
605
|
+
type: "string"
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
name: "Idempotency-Key",
|
|
610
|
+
in: "header",
|
|
611
|
+
required: true,
|
|
612
|
+
schema: {
|
|
613
|
+
type: "string",
|
|
614
|
+
minLength: 8,
|
|
615
|
+
maxLength: 128
|
|
616
|
+
}
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
name: "If-Match",
|
|
620
|
+
in: "header",
|
|
621
|
+
required: true,
|
|
622
|
+
schema: {
|
|
623
|
+
type: "integer",
|
|
624
|
+
minimum: 1
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
],
|
|
628
|
+
responses: {
|
|
629
|
+
"200": {
|
|
630
|
+
description: "Success",
|
|
631
|
+
content: {
|
|
632
|
+
"application/json": {
|
|
633
|
+
schema: {
|
|
634
|
+
type: "object",
|
|
635
|
+
properties: {
|
|
636
|
+
deleted: {
|
|
637
|
+
type: "string"
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
required: [
|
|
641
|
+
"deleted"
|
|
642
|
+
]
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
},
|
|
647
|
+
default: {
|
|
648
|
+
description: "Structured error",
|
|
649
|
+
content: {
|
|
650
|
+
"application/json": {
|
|
651
|
+
schema: {
|
|
652
|
+
$ref: "#/components/schemas/Error"
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
"/v1/profiles": {
|
|
661
|
+
get: {
|
|
662
|
+
operationId: "listProfiles",
|
|
663
|
+
parameters: [
|
|
664
|
+
{
|
|
665
|
+
name: "limit",
|
|
666
|
+
in: "query",
|
|
667
|
+
schema: {
|
|
668
|
+
type: "integer",
|
|
669
|
+
minimum: 1,
|
|
670
|
+
maximum: 1000,
|
|
671
|
+
default: 100
|
|
672
|
+
}
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
name: "offset",
|
|
676
|
+
in: "query",
|
|
677
|
+
schema: {
|
|
678
|
+
type: "integer",
|
|
679
|
+
minimum: 0,
|
|
680
|
+
default: 0
|
|
681
|
+
}
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
name: "search",
|
|
685
|
+
in: "query",
|
|
686
|
+
schema: {
|
|
687
|
+
type: "string"
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
],
|
|
691
|
+
responses: {
|
|
692
|
+
"200": {
|
|
693
|
+
description: "Success",
|
|
694
|
+
content: {
|
|
695
|
+
"application/json": {
|
|
696
|
+
schema: {
|
|
697
|
+
type: "object",
|
|
698
|
+
required: [
|
|
699
|
+
"data",
|
|
700
|
+
"total",
|
|
701
|
+
"limit",
|
|
702
|
+
"offset"
|
|
703
|
+
],
|
|
704
|
+
properties: {
|
|
705
|
+
data: {
|
|
706
|
+
type: "array",
|
|
707
|
+
items: {
|
|
708
|
+
$ref: "#/components/schemas/Profile"
|
|
709
|
+
}
|
|
710
|
+
},
|
|
711
|
+
total: {
|
|
712
|
+
type: "integer"
|
|
713
|
+
},
|
|
714
|
+
limit: {
|
|
715
|
+
type: "integer"
|
|
716
|
+
},
|
|
717
|
+
offset: {
|
|
718
|
+
type: "integer"
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
},
|
|
725
|
+
default: {
|
|
726
|
+
description: "Structured error",
|
|
727
|
+
content: {
|
|
728
|
+
"application/json": {
|
|
729
|
+
schema: {
|
|
730
|
+
$ref: "#/components/schemas/Error"
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
post: {
|
|
738
|
+
operationId: "createProfile",
|
|
739
|
+
parameters: [
|
|
740
|
+
{
|
|
741
|
+
name: "Idempotency-Key",
|
|
742
|
+
in: "header",
|
|
743
|
+
required: true,
|
|
744
|
+
schema: {
|
|
745
|
+
type: "string",
|
|
746
|
+
minLength: 8,
|
|
747
|
+
maxLength: 128
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
],
|
|
751
|
+
requestBody: {
|
|
752
|
+
required: true,
|
|
753
|
+
content: {
|
|
754
|
+
"application/json": {
|
|
755
|
+
schema: {
|
|
756
|
+
$ref: "#/components/schemas/ProfileInput"
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
},
|
|
761
|
+
responses: {
|
|
762
|
+
"201": {
|
|
763
|
+
description: "Success",
|
|
764
|
+
content: {
|
|
765
|
+
"application/json": {
|
|
766
|
+
schema: {
|
|
767
|
+
$ref: "#/components/schemas/Profile"
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
},
|
|
772
|
+
default: {
|
|
773
|
+
description: "Structured error",
|
|
774
|
+
content: {
|
|
775
|
+
"application/json": {
|
|
776
|
+
schema: {
|
|
777
|
+
$ref: "#/components/schemas/Error"
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
},
|
|
785
|
+
"/v1/profiles/{id}": {
|
|
786
|
+
get: {
|
|
787
|
+
operationId: "getProfile",
|
|
788
|
+
parameters: [
|
|
789
|
+
{
|
|
790
|
+
name: "id",
|
|
791
|
+
in: "path",
|
|
792
|
+
required: true,
|
|
793
|
+
schema: {
|
|
794
|
+
type: "string"
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
],
|
|
798
|
+
responses: {
|
|
799
|
+
"200": {
|
|
800
|
+
description: "Success",
|
|
801
|
+
content: {
|
|
802
|
+
"application/json": {
|
|
803
|
+
schema: {
|
|
804
|
+
$ref: "#/components/schemas/Profile"
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
},
|
|
809
|
+
default: {
|
|
810
|
+
description: "Structured error",
|
|
811
|
+
content: {
|
|
812
|
+
"application/json": {
|
|
813
|
+
schema: {
|
|
814
|
+
$ref: "#/components/schemas/Error"
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
},
|
|
821
|
+
put: {
|
|
822
|
+
operationId: "updateProfile",
|
|
823
|
+
parameters: [
|
|
824
|
+
{
|
|
825
|
+
name: "id",
|
|
826
|
+
in: "path",
|
|
827
|
+
required: true,
|
|
828
|
+
schema: {
|
|
829
|
+
type: "string"
|
|
830
|
+
}
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
name: "Idempotency-Key",
|
|
834
|
+
in: "header",
|
|
835
|
+
required: true,
|
|
836
|
+
schema: {
|
|
837
|
+
type: "string",
|
|
838
|
+
minLength: 8,
|
|
839
|
+
maxLength: 128
|
|
840
|
+
}
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
name: "If-Match",
|
|
844
|
+
in: "header",
|
|
845
|
+
required: true,
|
|
846
|
+
schema: {
|
|
847
|
+
type: "integer",
|
|
848
|
+
minimum: 1
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
],
|
|
852
|
+
requestBody: {
|
|
853
|
+
required: true,
|
|
854
|
+
content: {
|
|
855
|
+
"application/json": {
|
|
856
|
+
schema: {
|
|
857
|
+
$ref: "#/components/schemas/ProfileInput"
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
},
|
|
862
|
+
responses: {
|
|
863
|
+
"200": {
|
|
864
|
+
description: "Success",
|
|
865
|
+
content: {
|
|
866
|
+
"application/json": {
|
|
867
|
+
schema: {
|
|
868
|
+
$ref: "#/components/schemas/Profile"
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
},
|
|
873
|
+
default: {
|
|
874
|
+
description: "Structured error",
|
|
875
|
+
content: {
|
|
876
|
+
"application/json": {
|
|
877
|
+
schema: {
|
|
878
|
+
$ref: "#/components/schemas/Error"
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
},
|
|
885
|
+
delete: {
|
|
886
|
+
operationId: "deleteProfile",
|
|
887
|
+
parameters: [
|
|
888
|
+
{
|
|
889
|
+
name: "id",
|
|
890
|
+
in: "path",
|
|
891
|
+
required: true,
|
|
892
|
+
schema: {
|
|
893
|
+
type: "string"
|
|
894
|
+
}
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
name: "Idempotency-Key",
|
|
898
|
+
in: "header",
|
|
899
|
+
required: true,
|
|
900
|
+
schema: {
|
|
901
|
+
type: "string",
|
|
902
|
+
minLength: 8,
|
|
903
|
+
maxLength: 128
|
|
904
|
+
}
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
name: "If-Match",
|
|
908
|
+
in: "header",
|
|
909
|
+
required: true,
|
|
910
|
+
schema: {
|
|
911
|
+
type: "integer",
|
|
912
|
+
minimum: 1
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
],
|
|
916
|
+
responses: {
|
|
917
|
+
"200": {
|
|
918
|
+
description: "Success",
|
|
919
|
+
content: {
|
|
920
|
+
"application/json": {
|
|
921
|
+
schema: {
|
|
922
|
+
type: "object",
|
|
923
|
+
properties: {
|
|
924
|
+
deleted: {
|
|
925
|
+
type: "string"
|
|
926
|
+
}
|
|
927
|
+
},
|
|
928
|
+
required: [
|
|
929
|
+
"deleted"
|
|
930
|
+
]
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
},
|
|
935
|
+
default: {
|
|
936
|
+
description: "Structured error",
|
|
937
|
+
content: {
|
|
938
|
+
"application/json": {
|
|
939
|
+
schema: {
|
|
940
|
+
$ref: "#/components/schemas/Error"
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
},
|
|
948
|
+
"/v1/providers/{id}/models": {
|
|
949
|
+
get: {
|
|
950
|
+
operationId: "listModels",
|
|
951
|
+
parameters: [
|
|
952
|
+
{
|
|
953
|
+
name: "id",
|
|
954
|
+
in: "path",
|
|
955
|
+
required: true,
|
|
956
|
+
schema: {
|
|
957
|
+
type: "string"
|
|
958
|
+
}
|
|
959
|
+
},
|
|
960
|
+
{
|
|
961
|
+
name: "limit",
|
|
962
|
+
in: "query",
|
|
963
|
+
schema: {
|
|
964
|
+
type: "integer",
|
|
965
|
+
minimum: 1,
|
|
966
|
+
maximum: 1000,
|
|
967
|
+
default: 100
|
|
968
|
+
}
|
|
969
|
+
},
|
|
970
|
+
{
|
|
971
|
+
name: "offset",
|
|
972
|
+
in: "query",
|
|
973
|
+
schema: {
|
|
974
|
+
type: "integer",
|
|
975
|
+
minimum: 0,
|
|
976
|
+
default: 0
|
|
977
|
+
}
|
|
978
|
+
},
|
|
979
|
+
{
|
|
980
|
+
name: "search",
|
|
981
|
+
in: "query",
|
|
982
|
+
schema: {
|
|
983
|
+
type: "string"
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
],
|
|
987
|
+
responses: {
|
|
988
|
+
"200": {
|
|
989
|
+
description: "Success",
|
|
990
|
+
content: {
|
|
991
|
+
"application/json": {
|
|
992
|
+
schema: {
|
|
993
|
+
$ref: "#/components/schemas/ModelPage"
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
},
|
|
998
|
+
default: {
|
|
999
|
+
description: "Structured error",
|
|
1000
|
+
content: {
|
|
1001
|
+
"application/json": {
|
|
1002
|
+
schema: {
|
|
1003
|
+
$ref: "#/components/schemas/Error"
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
},
|
|
1011
|
+
"/v1/providers/{id}/refresh": {
|
|
1012
|
+
post: {
|
|
1013
|
+
operationId: "refreshModels",
|
|
1014
|
+
parameters: [
|
|
1015
|
+
{
|
|
1016
|
+
name: "id",
|
|
1017
|
+
in: "path",
|
|
1018
|
+
required: true,
|
|
1019
|
+
schema: {
|
|
1020
|
+
type: "string"
|
|
1021
|
+
}
|
|
1022
|
+
},
|
|
1023
|
+
{
|
|
1024
|
+
name: "Idempotency-Key",
|
|
1025
|
+
in: "header",
|
|
1026
|
+
required: true,
|
|
1027
|
+
schema: {
|
|
1028
|
+
type: "string",
|
|
1029
|
+
minLength: 8,
|
|
1030
|
+
maxLength: 128
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
],
|
|
1034
|
+
requestBody: {
|
|
1035
|
+
required: true,
|
|
1036
|
+
content: {
|
|
1037
|
+
"application/json": {
|
|
1038
|
+
schema: {
|
|
1039
|
+
$ref: "#/components/schemas/Empty"
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
},
|
|
1044
|
+
responses: {
|
|
1045
|
+
"200": {
|
|
1046
|
+
description: "Success",
|
|
1047
|
+
content: {
|
|
1048
|
+
"application/json": {
|
|
1049
|
+
schema: {
|
|
1050
|
+
$ref: "#/components/schemas/Catalog"
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
},
|
|
1055
|
+
default: {
|
|
1056
|
+
description: "Structured error",
|
|
1057
|
+
content: {
|
|
1058
|
+
"application/json": {
|
|
1059
|
+
schema: {
|
|
1060
|
+
$ref: "#/components/schemas/Error"
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
},
|
|
1068
|
+
"/v1/launch-plans": {
|
|
1069
|
+
post: {
|
|
1070
|
+
operationId: "launchPlan",
|
|
1071
|
+
parameters: [
|
|
1072
|
+
{
|
|
1073
|
+
name: "Idempotency-Key",
|
|
1074
|
+
in: "header",
|
|
1075
|
+
required: true,
|
|
1076
|
+
schema: {
|
|
1077
|
+
type: "string",
|
|
1078
|
+
minLength: 8,
|
|
1079
|
+
maxLength: 128
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
],
|
|
1083
|
+
requestBody: {
|
|
1084
|
+
required: true,
|
|
1085
|
+
content: {
|
|
1086
|
+
"application/json": {
|
|
1087
|
+
schema: {
|
|
1088
|
+
$ref: "#/components/schemas/LaunchInput"
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
},
|
|
1093
|
+
responses: {
|
|
1094
|
+
"200": {
|
|
1095
|
+
description: "Success",
|
|
1096
|
+
content: {
|
|
1097
|
+
"application/json": {
|
|
1098
|
+
schema: {
|
|
1099
|
+
$ref: "#/components/schemas/LaunchPlan"
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
},
|
|
1104
|
+
default: {
|
|
1105
|
+
description: "Structured error",
|
|
1106
|
+
content: {
|
|
1107
|
+
"application/json": {
|
|
1108
|
+
schema: {
|
|
1109
|
+
$ref: "#/components/schemas/Error"
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
},
|
|
1117
|
+
"/v1/runs": {
|
|
1118
|
+
get: {
|
|
1119
|
+
operationId: "listRuns",
|
|
1120
|
+
parameters: [
|
|
1121
|
+
{
|
|
1122
|
+
name: "limit",
|
|
1123
|
+
in: "query",
|
|
1124
|
+
schema: {
|
|
1125
|
+
type: "integer",
|
|
1126
|
+
minimum: 1,
|
|
1127
|
+
maximum: 1000,
|
|
1128
|
+
default: 100
|
|
1129
|
+
}
|
|
1130
|
+
},
|
|
1131
|
+
{
|
|
1132
|
+
name: "offset",
|
|
1133
|
+
in: "query",
|
|
1134
|
+
schema: {
|
|
1135
|
+
type: "integer",
|
|
1136
|
+
minimum: 0,
|
|
1137
|
+
default: 0
|
|
1138
|
+
}
|
|
1139
|
+
},
|
|
1140
|
+
{
|
|
1141
|
+
name: "search",
|
|
1142
|
+
in: "query",
|
|
1143
|
+
schema: {
|
|
1144
|
+
type: "string"
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
],
|
|
1148
|
+
responses: {
|
|
1149
|
+
"200": {
|
|
1150
|
+
description: "Success",
|
|
1151
|
+
content: {
|
|
1152
|
+
"application/json": {
|
|
1153
|
+
schema: {
|
|
1154
|
+
type: "object",
|
|
1155
|
+
required: [
|
|
1156
|
+
"data",
|
|
1157
|
+
"total",
|
|
1158
|
+
"limit",
|
|
1159
|
+
"offset"
|
|
1160
|
+
],
|
|
1161
|
+
properties: {
|
|
1162
|
+
data: {
|
|
1163
|
+
type: "array",
|
|
1164
|
+
items: {
|
|
1165
|
+
$ref: "#/components/schemas/Run"
|
|
1166
|
+
}
|
|
1167
|
+
},
|
|
1168
|
+
total: {
|
|
1169
|
+
type: "integer"
|
|
1170
|
+
},
|
|
1171
|
+
limit: {
|
|
1172
|
+
type: "integer"
|
|
1173
|
+
},
|
|
1174
|
+
offset: {
|
|
1175
|
+
type: "integer"
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
},
|
|
1182
|
+
default: {
|
|
1183
|
+
description: "Structured error",
|
|
1184
|
+
content: {
|
|
1185
|
+
"application/json": {
|
|
1186
|
+
schema: {
|
|
1187
|
+
$ref: "#/components/schemas/Error"
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
},
|
|
1194
|
+
post: {
|
|
1195
|
+
operationId: "createRun",
|
|
1196
|
+
parameters: [
|
|
1197
|
+
{
|
|
1198
|
+
name: "Idempotency-Key",
|
|
1199
|
+
in: "header",
|
|
1200
|
+
required: true,
|
|
1201
|
+
schema: {
|
|
1202
|
+
type: "string",
|
|
1203
|
+
minLength: 8,
|
|
1204
|
+
maxLength: 128
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
],
|
|
1208
|
+
requestBody: {
|
|
1209
|
+
required: true,
|
|
1210
|
+
content: {
|
|
1211
|
+
"application/json": {
|
|
1212
|
+
schema: {
|
|
1213
|
+
$ref: "#/components/schemas/RunInput"
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
},
|
|
1218
|
+
responses: {
|
|
1219
|
+
"201": {
|
|
1220
|
+
description: "Success",
|
|
1221
|
+
content: {
|
|
1222
|
+
"application/json": {
|
|
1223
|
+
schema: {
|
|
1224
|
+
$ref: "#/components/schemas/Run"
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
},
|
|
1229
|
+
default: {
|
|
1230
|
+
description: "Structured error",
|
|
1231
|
+
content: {
|
|
1232
|
+
"application/json": {
|
|
1233
|
+
schema: {
|
|
1234
|
+
$ref: "#/components/schemas/Error"
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
},
|
|
1242
|
+
"/v1/runs/{id}": {
|
|
1243
|
+
get: {
|
|
1244
|
+
operationId: "getRun",
|
|
1245
|
+
parameters: [
|
|
1246
|
+
{
|
|
1247
|
+
name: "id",
|
|
1248
|
+
in: "path",
|
|
1249
|
+
required: true,
|
|
1250
|
+
schema: {
|
|
1251
|
+
type: "string"
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
],
|
|
1255
|
+
responses: {
|
|
1256
|
+
"200": {
|
|
1257
|
+
description: "Success",
|
|
1258
|
+
content: {
|
|
1259
|
+
"application/json": {
|
|
1260
|
+
schema: {
|
|
1261
|
+
$ref: "#/components/schemas/Run"
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
},
|
|
1266
|
+
default: {
|
|
1267
|
+
description: "Structured error",
|
|
1268
|
+
content: {
|
|
1269
|
+
"application/json": {
|
|
1270
|
+
schema: {
|
|
1271
|
+
$ref: "#/components/schemas/Error"
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
},
|
|
1278
|
+
patch: {
|
|
1279
|
+
operationId: "finishRun",
|
|
1280
|
+
parameters: [
|
|
1281
|
+
{
|
|
1282
|
+
name: "id",
|
|
1283
|
+
in: "path",
|
|
1284
|
+
required: true,
|
|
1285
|
+
schema: {
|
|
1286
|
+
type: "string"
|
|
1287
|
+
}
|
|
1288
|
+
},
|
|
1289
|
+
{
|
|
1290
|
+
name: "Idempotency-Key",
|
|
1291
|
+
in: "header",
|
|
1292
|
+
required: true,
|
|
1293
|
+
schema: {
|
|
1294
|
+
type: "string",
|
|
1295
|
+
minLength: 8,
|
|
1296
|
+
maxLength: 128
|
|
1297
|
+
}
|
|
1298
|
+
},
|
|
1299
|
+
{
|
|
1300
|
+
name: "If-Match",
|
|
1301
|
+
in: "header",
|
|
1302
|
+
required: true,
|
|
1303
|
+
schema: {
|
|
1304
|
+
type: "integer",
|
|
1305
|
+
minimum: 1
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
],
|
|
1309
|
+
requestBody: {
|
|
1310
|
+
required: true,
|
|
1311
|
+
content: {
|
|
1312
|
+
"application/json": {
|
|
1313
|
+
schema: {
|
|
1314
|
+
$ref: "#/components/schemas/RunUpdate"
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
},
|
|
1319
|
+
responses: {
|
|
1320
|
+
"200": {
|
|
1321
|
+
description: "Success",
|
|
1322
|
+
content: {
|
|
1323
|
+
"application/json": {
|
|
1324
|
+
schema: {
|
|
1325
|
+
$ref: "#/components/schemas/Run"
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
},
|
|
1330
|
+
default: {
|
|
1331
|
+
description: "Structured error",
|
|
1332
|
+
content: {
|
|
1333
|
+
"application/json": {
|
|
1334
|
+
schema: {
|
|
1335
|
+
$ref: "#/components/schemas/Error"
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
},
|
|
1343
|
+
"/health": {
|
|
1344
|
+
get: {
|
|
1345
|
+
operationId: "health",
|
|
1346
|
+
parameters: [],
|
|
1347
|
+
responses: {
|
|
1348
|
+
"200": {
|
|
1349
|
+
description: "Success",
|
|
1350
|
+
content: {
|
|
1351
|
+
"application/json": {
|
|
1352
|
+
schema: {
|
|
1353
|
+
$ref: "#/components/schemas/Health"
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
},
|
|
1358
|
+
default: {
|
|
1359
|
+
description: "Structured error",
|
|
1360
|
+
content: {
|
|
1361
|
+
"application/json": {
|
|
1362
|
+
schema: {
|
|
1363
|
+
$ref: "#/components/schemas/Error"
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
},
|
|
1369
|
+
security: []
|
|
1370
|
+
}
|
|
1371
|
+
},
|
|
1372
|
+
"/ready": {
|
|
1373
|
+
get: {
|
|
1374
|
+
operationId: "ready",
|
|
1375
|
+
parameters: [],
|
|
1376
|
+
responses: {
|
|
1377
|
+
"200": {
|
|
1378
|
+
description: "Success",
|
|
1379
|
+
content: {
|
|
1380
|
+
"application/json": {
|
|
1381
|
+
schema: {
|
|
1382
|
+
$ref: "#/components/schemas/Ready"
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
},
|
|
1387
|
+
default: {
|
|
1388
|
+
description: "Structured error",
|
|
1389
|
+
content: {
|
|
1390
|
+
"application/json": {
|
|
1391
|
+
schema: {
|
|
1392
|
+
$ref: "#/components/schemas/Error"
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
},
|
|
1398
|
+
security: []
|
|
1399
|
+
}
|
|
1400
|
+
},
|
|
1401
|
+
"/version": {
|
|
1402
|
+
get: {
|
|
1403
|
+
operationId: "version",
|
|
1404
|
+
parameters: [],
|
|
1405
|
+
responses: {
|
|
1406
|
+
"200": {
|
|
1407
|
+
description: "Success",
|
|
1408
|
+
content: {
|
|
1409
|
+
"application/json": {
|
|
1410
|
+
schema: {
|
|
1411
|
+
$ref: "#/components/schemas/Version"
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
},
|
|
1416
|
+
default: {
|
|
1417
|
+
description: "Structured error",
|
|
1418
|
+
content: {
|
|
1419
|
+
"application/json": {
|
|
1420
|
+
schema: {
|
|
1421
|
+
$ref: "#/components/schemas/Error"
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
},
|
|
1427
|
+
security: []
|
|
1428
|
+
}
|
|
1429
|
+
},
|
|
1430
|
+
"/v1/openapi.json": {
|
|
1431
|
+
get: {
|
|
1432
|
+
operationId: "openApi",
|
|
1433
|
+
parameters: [],
|
|
1434
|
+
responses: {
|
|
1435
|
+
"200": {
|
|
1436
|
+
description: "Success",
|
|
1437
|
+
content: {
|
|
1438
|
+
"application/json": {
|
|
1439
|
+
schema: {
|
|
1440
|
+
type: "object",
|
|
1441
|
+
additionalProperties: true
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
},
|
|
1446
|
+
default: {
|
|
1447
|
+
description: "Structured error",
|
|
1448
|
+
content: {
|
|
1449
|
+
"application/json": {
|
|
1450
|
+
schema: {
|
|
1451
|
+
$ref: "#/components/schemas/Error"
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
},
|
|
1460
|
+
components: {
|
|
1461
|
+
securitySchemes: {
|
|
1462
|
+
bearerAuth: {
|
|
1463
|
+
type: "http",
|
|
1464
|
+
scheme: "bearer"
|
|
1465
|
+
}
|
|
1466
|
+
},
|
|
1467
|
+
schemas: {
|
|
1468
|
+
ProviderInput: {
|
|
1469
|
+
type: "object",
|
|
1470
|
+
properties: {
|
|
1471
|
+
id: {
|
|
1472
|
+
type: "string",
|
|
1473
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$"
|
|
1474
|
+
},
|
|
1475
|
+
name: {
|
|
1476
|
+
type: "string",
|
|
1477
|
+
minLength: 1,
|
|
1478
|
+
maxLength: 200
|
|
1479
|
+
},
|
|
1480
|
+
baseUrl: {
|
|
1481
|
+
type: "string",
|
|
1482
|
+
maxLength: 2000
|
|
1483
|
+
},
|
|
1484
|
+
protocol: {
|
|
1485
|
+
type: "string",
|
|
1486
|
+
enum: [
|
|
1487
|
+
"anthropic-messages",
|
|
1488
|
+
"openai-responses",
|
|
1489
|
+
"openai-chat"
|
|
1490
|
+
]
|
|
1491
|
+
},
|
|
1492
|
+
credentialEnv: {
|
|
1493
|
+
type: "string",
|
|
1494
|
+
pattern: "^SWITCHER_PROVIDER_[A-Z0-9_]+$"
|
|
1495
|
+
},
|
|
1496
|
+
authStyle: {
|
|
1497
|
+
type: "string",
|
|
1498
|
+
enum: [
|
|
1499
|
+
"bearer",
|
|
1500
|
+
"x-api-key"
|
|
1501
|
+
],
|
|
1502
|
+
default: "bearer"
|
|
1503
|
+
},
|
|
1504
|
+
modelsPath: {
|
|
1505
|
+
type: "string",
|
|
1506
|
+
pattern: "^[a-zA-Z0-9_/-]+$",
|
|
1507
|
+
maxLength: 200,
|
|
1508
|
+
default: "models"
|
|
1509
|
+
},
|
|
1510
|
+
manualModels: {
|
|
1511
|
+
type: "array",
|
|
1512
|
+
items: {
|
|
1513
|
+
type: "object",
|
|
1514
|
+
properties: {
|
|
1515
|
+
id: {
|
|
1516
|
+
type: "string",
|
|
1517
|
+
minLength: 1,
|
|
1518
|
+
maxLength: 300
|
|
1519
|
+
},
|
|
1520
|
+
name: {
|
|
1521
|
+
type: "string",
|
|
1522
|
+
minLength: 1,
|
|
1523
|
+
maxLength: 200
|
|
1524
|
+
},
|
|
1525
|
+
description: {
|
|
1526
|
+
type: "string",
|
|
1527
|
+
maxLength: 8000
|
|
1528
|
+
},
|
|
1529
|
+
contextWindow: {
|
|
1530
|
+
type: "integer",
|
|
1531
|
+
exclusiveMinimum: true,
|
|
1532
|
+
minimum: 0
|
|
1533
|
+
},
|
|
1534
|
+
maxOutputTokens: {
|
|
1535
|
+
type: "integer",
|
|
1536
|
+
exclusiveMinimum: true,
|
|
1537
|
+
minimum: 0
|
|
1538
|
+
},
|
|
1539
|
+
inputModalities: {
|
|
1540
|
+
type: "array",
|
|
1541
|
+
items: {
|
|
1542
|
+
type: "string",
|
|
1543
|
+
maxLength: 50
|
|
1544
|
+
},
|
|
1545
|
+
maxItems: 20
|
|
1546
|
+
},
|
|
1547
|
+
outputModalities: {
|
|
1548
|
+
type: "array",
|
|
1549
|
+
items: {
|
|
1550
|
+
type: "string",
|
|
1551
|
+
maxLength: 50
|
|
1552
|
+
},
|
|
1553
|
+
maxItems: 20
|
|
1554
|
+
},
|
|
1555
|
+
supportedParameters: {
|
|
1556
|
+
type: "array",
|
|
1557
|
+
items: {
|
|
1558
|
+
type: "string",
|
|
1559
|
+
maxLength: 100
|
|
1560
|
+
},
|
|
1561
|
+
maxItems: 100
|
|
1562
|
+
}
|
|
1563
|
+
},
|
|
1564
|
+
required: [
|
|
1565
|
+
"id",
|
|
1566
|
+
"name"
|
|
1567
|
+
],
|
|
1568
|
+
additionalProperties: false
|
|
1569
|
+
},
|
|
1570
|
+
maxItems: 1e4,
|
|
1571
|
+
default: []
|
|
1572
|
+
}
|
|
1573
|
+
},
|
|
1574
|
+
required: [
|
|
1575
|
+
"id",
|
|
1576
|
+
"name",
|
|
1577
|
+
"baseUrl",
|
|
1578
|
+
"protocol"
|
|
1579
|
+
],
|
|
1580
|
+
additionalProperties: false
|
|
1581
|
+
},
|
|
1582
|
+
Provider: {
|
|
1583
|
+
type: "object",
|
|
1584
|
+
properties: {
|
|
1585
|
+
id: {
|
|
1586
|
+
type: "string",
|
|
1587
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$"
|
|
1588
|
+
},
|
|
1589
|
+
name: {
|
|
1590
|
+
type: "string",
|
|
1591
|
+
minLength: 1,
|
|
1592
|
+
maxLength: 200
|
|
1593
|
+
},
|
|
1594
|
+
baseUrl: {
|
|
1595
|
+
type: "string",
|
|
1596
|
+
maxLength: 2000
|
|
1597
|
+
},
|
|
1598
|
+
protocol: {
|
|
1599
|
+
type: "string",
|
|
1600
|
+
enum: [
|
|
1601
|
+
"anthropic-messages",
|
|
1602
|
+
"openai-responses",
|
|
1603
|
+
"openai-chat"
|
|
1604
|
+
]
|
|
1605
|
+
},
|
|
1606
|
+
credentialEnv: {
|
|
1607
|
+
type: "string",
|
|
1608
|
+
pattern: "^SWITCHER_PROVIDER_[A-Z0-9_]+$"
|
|
1609
|
+
},
|
|
1610
|
+
authStyle: {
|
|
1611
|
+
type: "string",
|
|
1612
|
+
enum: [
|
|
1613
|
+
"bearer",
|
|
1614
|
+
"x-api-key"
|
|
1615
|
+
],
|
|
1616
|
+
default: "bearer"
|
|
1617
|
+
},
|
|
1618
|
+
modelsPath: {
|
|
1619
|
+
type: "string",
|
|
1620
|
+
pattern: "^[a-zA-Z0-9_/-]+$",
|
|
1621
|
+
maxLength: 200,
|
|
1622
|
+
default: "models"
|
|
1623
|
+
},
|
|
1624
|
+
manualModels: {
|
|
1625
|
+
type: "array",
|
|
1626
|
+
items: {
|
|
1627
|
+
type: "object",
|
|
1628
|
+
properties: {
|
|
1629
|
+
id: {
|
|
1630
|
+
type: "string",
|
|
1631
|
+
minLength: 1,
|
|
1632
|
+
maxLength: 300
|
|
1633
|
+
},
|
|
1634
|
+
name: {
|
|
1635
|
+
type: "string",
|
|
1636
|
+
minLength: 1,
|
|
1637
|
+
maxLength: 200
|
|
1638
|
+
},
|
|
1639
|
+
description: {
|
|
1640
|
+
type: "string",
|
|
1641
|
+
maxLength: 8000
|
|
1642
|
+
},
|
|
1643
|
+
contextWindow: {
|
|
1644
|
+
type: "integer",
|
|
1645
|
+
exclusiveMinimum: true,
|
|
1646
|
+
minimum: 0
|
|
1647
|
+
},
|
|
1648
|
+
maxOutputTokens: {
|
|
1649
|
+
type: "integer",
|
|
1650
|
+
exclusiveMinimum: true,
|
|
1651
|
+
minimum: 0
|
|
1652
|
+
},
|
|
1653
|
+
inputModalities: {
|
|
1654
|
+
type: "array",
|
|
1655
|
+
items: {
|
|
1656
|
+
type: "string",
|
|
1657
|
+
maxLength: 50
|
|
1658
|
+
},
|
|
1659
|
+
maxItems: 20
|
|
1660
|
+
},
|
|
1661
|
+
outputModalities: {
|
|
1662
|
+
type: "array",
|
|
1663
|
+
items: {
|
|
1664
|
+
type: "string",
|
|
1665
|
+
maxLength: 50
|
|
1666
|
+
},
|
|
1667
|
+
maxItems: 20
|
|
1668
|
+
},
|
|
1669
|
+
supportedParameters: {
|
|
1670
|
+
type: "array",
|
|
1671
|
+
items: {
|
|
1672
|
+
type: "string",
|
|
1673
|
+
maxLength: 100
|
|
1674
|
+
},
|
|
1675
|
+
maxItems: 100
|
|
1676
|
+
}
|
|
1677
|
+
},
|
|
1678
|
+
required: [
|
|
1679
|
+
"id",
|
|
1680
|
+
"name"
|
|
1681
|
+
],
|
|
1682
|
+
additionalProperties: false
|
|
1683
|
+
},
|
|
1684
|
+
maxItems: 1e4,
|
|
1685
|
+
default: []
|
|
1686
|
+
},
|
|
1687
|
+
version: {
|
|
1688
|
+
type: "integer",
|
|
1689
|
+
exclusiveMinimum: true,
|
|
1690
|
+
minimum: 0
|
|
1691
|
+
},
|
|
1692
|
+
updatedAt: {
|
|
1693
|
+
type: "string"
|
|
1694
|
+
}
|
|
1695
|
+
},
|
|
1696
|
+
required: [
|
|
1697
|
+
"id",
|
|
1698
|
+
"name",
|
|
1699
|
+
"baseUrl",
|
|
1700
|
+
"protocol",
|
|
1701
|
+
"version",
|
|
1702
|
+
"updatedAt"
|
|
1703
|
+
],
|
|
1704
|
+
additionalProperties: false
|
|
1705
|
+
},
|
|
1706
|
+
ProfileInput: {
|
|
1707
|
+
type: "object",
|
|
1708
|
+
properties: {
|
|
1709
|
+
id: {
|
|
1710
|
+
type: "string",
|
|
1711
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$"
|
|
1712
|
+
},
|
|
1713
|
+
name: {
|
|
1714
|
+
type: "string",
|
|
1715
|
+
minLength: 1,
|
|
1716
|
+
maxLength: 200
|
|
1717
|
+
},
|
|
1718
|
+
providerId: {
|
|
1719
|
+
type: "string",
|
|
1720
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$"
|
|
1721
|
+
},
|
|
1722
|
+
harness: {
|
|
1723
|
+
type: "string",
|
|
1724
|
+
enum: [
|
|
1725
|
+
"claude",
|
|
1726
|
+
"codex",
|
|
1727
|
+
"grok",
|
|
1728
|
+
"opencode2"
|
|
1729
|
+
]
|
|
1730
|
+
},
|
|
1731
|
+
model: {
|
|
1732
|
+
type: "string",
|
|
1733
|
+
minLength: 1,
|
|
1734
|
+
maxLength: 300
|
|
1735
|
+
}
|
|
1736
|
+
},
|
|
1737
|
+
required: [
|
|
1738
|
+
"id",
|
|
1739
|
+
"name",
|
|
1740
|
+
"providerId",
|
|
1741
|
+
"harness",
|
|
1742
|
+
"model"
|
|
1743
|
+
],
|
|
1744
|
+
additionalProperties: false
|
|
1745
|
+
},
|
|
1746
|
+
Profile: {
|
|
1747
|
+
type: "object",
|
|
1748
|
+
properties: {
|
|
1749
|
+
id: {
|
|
1750
|
+
type: "string",
|
|
1751
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$"
|
|
1752
|
+
},
|
|
1753
|
+
name: {
|
|
1754
|
+
type: "string",
|
|
1755
|
+
minLength: 1,
|
|
1756
|
+
maxLength: 200
|
|
1757
|
+
},
|
|
1758
|
+
providerId: {
|
|
1759
|
+
type: "string",
|
|
1760
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$"
|
|
1761
|
+
},
|
|
1762
|
+
harness: {
|
|
1763
|
+
type: "string",
|
|
1764
|
+
enum: [
|
|
1765
|
+
"claude",
|
|
1766
|
+
"codex",
|
|
1767
|
+
"grok",
|
|
1768
|
+
"opencode2"
|
|
1769
|
+
]
|
|
1770
|
+
},
|
|
1771
|
+
model: {
|
|
1772
|
+
type: "string",
|
|
1773
|
+
minLength: 1,
|
|
1774
|
+
maxLength: 300
|
|
1775
|
+
},
|
|
1776
|
+
version: {
|
|
1777
|
+
type: "integer",
|
|
1778
|
+
exclusiveMinimum: true,
|
|
1779
|
+
minimum: 0
|
|
1780
|
+
},
|
|
1781
|
+
updatedAt: {
|
|
1782
|
+
type: "string"
|
|
1783
|
+
}
|
|
1784
|
+
},
|
|
1785
|
+
required: [
|
|
1786
|
+
"id",
|
|
1787
|
+
"name",
|
|
1788
|
+
"providerId",
|
|
1789
|
+
"harness",
|
|
1790
|
+
"model",
|
|
1791
|
+
"version",
|
|
1792
|
+
"updatedAt"
|
|
1793
|
+
],
|
|
1794
|
+
additionalProperties: false
|
|
1795
|
+
},
|
|
1796
|
+
Model: {
|
|
1797
|
+
type: "object",
|
|
1798
|
+
properties: {
|
|
1799
|
+
id: {
|
|
1800
|
+
type: "string",
|
|
1801
|
+
minLength: 1,
|
|
1802
|
+
maxLength: 300
|
|
1803
|
+
},
|
|
1804
|
+
name: {
|
|
1805
|
+
type: "string",
|
|
1806
|
+
minLength: 1,
|
|
1807
|
+
maxLength: 200
|
|
1808
|
+
},
|
|
1809
|
+
description: {
|
|
1810
|
+
type: "string",
|
|
1811
|
+
maxLength: 8000
|
|
1812
|
+
},
|
|
1813
|
+
contextWindow: {
|
|
1814
|
+
type: "integer",
|
|
1815
|
+
exclusiveMinimum: true,
|
|
1816
|
+
minimum: 0
|
|
1817
|
+
},
|
|
1818
|
+
maxOutputTokens: {
|
|
1819
|
+
type: "integer",
|
|
1820
|
+
exclusiveMinimum: true,
|
|
1821
|
+
minimum: 0
|
|
1822
|
+
},
|
|
1823
|
+
inputModalities: {
|
|
1824
|
+
type: "array",
|
|
1825
|
+
items: {
|
|
1826
|
+
type: "string",
|
|
1827
|
+
maxLength: 50
|
|
1828
|
+
},
|
|
1829
|
+
maxItems: 20
|
|
1830
|
+
},
|
|
1831
|
+
outputModalities: {
|
|
1832
|
+
type: "array",
|
|
1833
|
+
items: {
|
|
1834
|
+
type: "string",
|
|
1835
|
+
maxLength: 50
|
|
1836
|
+
},
|
|
1837
|
+
maxItems: 20
|
|
1838
|
+
},
|
|
1839
|
+
supportedParameters: {
|
|
1840
|
+
type: "array",
|
|
1841
|
+
items: {
|
|
1842
|
+
type: "string",
|
|
1843
|
+
maxLength: 100
|
|
1844
|
+
},
|
|
1845
|
+
maxItems: 100
|
|
1846
|
+
}
|
|
1847
|
+
},
|
|
1848
|
+
required: [
|
|
1849
|
+
"id",
|
|
1850
|
+
"name"
|
|
1851
|
+
],
|
|
1852
|
+
additionalProperties: false
|
|
1853
|
+
},
|
|
1854
|
+
ModelPage: {
|
|
1855
|
+
type: "object",
|
|
1856
|
+
properties: {
|
|
1857
|
+
data: {
|
|
1858
|
+
type: "array",
|
|
1859
|
+
items: {
|
|
1860
|
+
type: "object",
|
|
1861
|
+
properties: {
|
|
1862
|
+
id: {
|
|
1863
|
+
type: "string",
|
|
1864
|
+
minLength: 1,
|
|
1865
|
+
maxLength: 300
|
|
1866
|
+
},
|
|
1867
|
+
name: {
|
|
1868
|
+
type: "string",
|
|
1869
|
+
minLength: 1,
|
|
1870
|
+
maxLength: 200
|
|
1871
|
+
},
|
|
1872
|
+
description: {
|
|
1873
|
+
type: "string",
|
|
1874
|
+
maxLength: 8000
|
|
1875
|
+
},
|
|
1876
|
+
contextWindow: {
|
|
1877
|
+
type: "integer",
|
|
1878
|
+
exclusiveMinimum: true,
|
|
1879
|
+
minimum: 0
|
|
1880
|
+
},
|
|
1881
|
+
maxOutputTokens: {
|
|
1882
|
+
type: "integer",
|
|
1883
|
+
exclusiveMinimum: true,
|
|
1884
|
+
minimum: 0
|
|
1885
|
+
},
|
|
1886
|
+
inputModalities: {
|
|
1887
|
+
type: "array",
|
|
1888
|
+
items: {
|
|
1889
|
+
type: "string",
|
|
1890
|
+
maxLength: 50
|
|
1891
|
+
},
|
|
1892
|
+
maxItems: 20
|
|
1893
|
+
},
|
|
1894
|
+
outputModalities: {
|
|
1895
|
+
type: "array",
|
|
1896
|
+
items: {
|
|
1897
|
+
type: "string",
|
|
1898
|
+
maxLength: 50
|
|
1899
|
+
},
|
|
1900
|
+
maxItems: 20
|
|
1901
|
+
},
|
|
1902
|
+
supportedParameters: {
|
|
1903
|
+
type: "array",
|
|
1904
|
+
items: {
|
|
1905
|
+
type: "string",
|
|
1906
|
+
maxLength: 100
|
|
1907
|
+
},
|
|
1908
|
+
maxItems: 100
|
|
1909
|
+
},
|
|
1910
|
+
codingEligible: {
|
|
1911
|
+
type: "boolean"
|
|
1912
|
+
}
|
|
1913
|
+
},
|
|
1914
|
+
required: [
|
|
1915
|
+
"id",
|
|
1916
|
+
"name",
|
|
1917
|
+
"codingEligible"
|
|
1918
|
+
],
|
|
1919
|
+
additionalProperties: false
|
|
1920
|
+
}
|
|
1921
|
+
},
|
|
1922
|
+
total: {
|
|
1923
|
+
type: "integer"
|
|
1924
|
+
},
|
|
1925
|
+
limit: {
|
|
1926
|
+
type: "integer"
|
|
1927
|
+
},
|
|
1928
|
+
offset: {
|
|
1929
|
+
type: "integer"
|
|
1930
|
+
},
|
|
1931
|
+
refreshedAt: {
|
|
1932
|
+
type: "string"
|
|
1933
|
+
},
|
|
1934
|
+
source: {
|
|
1935
|
+
type: "string",
|
|
1936
|
+
enum: [
|
|
1937
|
+
"remote",
|
|
1938
|
+
"manual"
|
|
1939
|
+
]
|
|
1940
|
+
}
|
|
1941
|
+
},
|
|
1942
|
+
required: [
|
|
1943
|
+
"data",
|
|
1944
|
+
"total",
|
|
1945
|
+
"limit",
|
|
1946
|
+
"offset",
|
|
1947
|
+
"refreshedAt",
|
|
1948
|
+
"source"
|
|
1949
|
+
],
|
|
1950
|
+
additionalProperties: false
|
|
1951
|
+
},
|
|
1952
|
+
Catalog: {
|
|
1953
|
+
type: "object",
|
|
1954
|
+
properties: {
|
|
1955
|
+
models: {
|
|
1956
|
+
type: "array",
|
|
1957
|
+
items: {
|
|
1958
|
+
type: "object",
|
|
1959
|
+
properties: {
|
|
1960
|
+
id: {
|
|
1961
|
+
type: "string",
|
|
1962
|
+
minLength: 1,
|
|
1963
|
+
maxLength: 300
|
|
1964
|
+
},
|
|
1965
|
+
name: {
|
|
1966
|
+
type: "string",
|
|
1967
|
+
minLength: 1,
|
|
1968
|
+
maxLength: 200
|
|
1969
|
+
},
|
|
1970
|
+
description: {
|
|
1971
|
+
type: "string",
|
|
1972
|
+
maxLength: 8000
|
|
1973
|
+
},
|
|
1974
|
+
contextWindow: {
|
|
1975
|
+
type: "integer",
|
|
1976
|
+
exclusiveMinimum: true,
|
|
1977
|
+
minimum: 0
|
|
1978
|
+
},
|
|
1979
|
+
maxOutputTokens: {
|
|
1980
|
+
type: "integer",
|
|
1981
|
+
exclusiveMinimum: true,
|
|
1982
|
+
minimum: 0
|
|
1983
|
+
},
|
|
1984
|
+
inputModalities: {
|
|
1985
|
+
type: "array",
|
|
1986
|
+
items: {
|
|
1987
|
+
type: "string",
|
|
1988
|
+
maxLength: 50
|
|
1989
|
+
},
|
|
1990
|
+
maxItems: 20
|
|
1991
|
+
},
|
|
1992
|
+
outputModalities: {
|
|
1993
|
+
type: "array",
|
|
1994
|
+
items: {
|
|
1995
|
+
type: "string",
|
|
1996
|
+
maxLength: 50
|
|
1997
|
+
},
|
|
1998
|
+
maxItems: 20
|
|
1999
|
+
},
|
|
2000
|
+
supportedParameters: {
|
|
2001
|
+
type: "array",
|
|
2002
|
+
items: {
|
|
2003
|
+
type: "string",
|
|
2004
|
+
maxLength: 100
|
|
2005
|
+
},
|
|
2006
|
+
maxItems: 100
|
|
2007
|
+
}
|
|
2008
|
+
},
|
|
2009
|
+
required: [
|
|
2010
|
+
"id",
|
|
2011
|
+
"name"
|
|
2012
|
+
],
|
|
2013
|
+
additionalProperties: false
|
|
2014
|
+
}
|
|
2015
|
+
},
|
|
2016
|
+
refreshedAt: {
|
|
2017
|
+
type: "string"
|
|
2018
|
+
},
|
|
2019
|
+
source: {
|
|
2020
|
+
type: "string",
|
|
2021
|
+
enum: [
|
|
2022
|
+
"remote",
|
|
2023
|
+
"manual"
|
|
2024
|
+
]
|
|
2025
|
+
}
|
|
2026
|
+
},
|
|
2027
|
+
required: [
|
|
2028
|
+
"models",
|
|
2029
|
+
"refreshedAt",
|
|
2030
|
+
"source"
|
|
2031
|
+
],
|
|
2032
|
+
additionalProperties: false
|
|
2033
|
+
},
|
|
2034
|
+
LaunchPlan: {
|
|
2035
|
+
type: "object",
|
|
2036
|
+
properties: {
|
|
2037
|
+
provider: {
|
|
2038
|
+
type: "object",
|
|
2039
|
+
properties: {
|
|
2040
|
+
id: {
|
|
2041
|
+
type: "string",
|
|
2042
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$"
|
|
2043
|
+
},
|
|
2044
|
+
name: {
|
|
2045
|
+
type: "string",
|
|
2046
|
+
minLength: 1,
|
|
2047
|
+
maxLength: 200
|
|
2048
|
+
},
|
|
2049
|
+
baseUrl: {
|
|
2050
|
+
type: "string",
|
|
2051
|
+
maxLength: 2000
|
|
2052
|
+
},
|
|
2053
|
+
protocol: {
|
|
2054
|
+
type: "string",
|
|
2055
|
+
enum: [
|
|
2056
|
+
"anthropic-messages",
|
|
2057
|
+
"openai-responses",
|
|
2058
|
+
"openai-chat"
|
|
2059
|
+
]
|
|
2060
|
+
},
|
|
2061
|
+
credentialEnv: {
|
|
2062
|
+
type: "string",
|
|
2063
|
+
pattern: "^SWITCHER_PROVIDER_[A-Z0-9_]+$"
|
|
2064
|
+
},
|
|
2065
|
+
authStyle: {
|
|
2066
|
+
type: "string",
|
|
2067
|
+
enum: [
|
|
2068
|
+
"bearer",
|
|
2069
|
+
"x-api-key"
|
|
2070
|
+
],
|
|
2071
|
+
default: "bearer"
|
|
2072
|
+
},
|
|
2073
|
+
modelsPath: {
|
|
2074
|
+
type: "string",
|
|
2075
|
+
pattern: "^[a-zA-Z0-9_/-]+$",
|
|
2076
|
+
maxLength: 200,
|
|
2077
|
+
default: "models"
|
|
2078
|
+
},
|
|
2079
|
+
manualModels: {
|
|
2080
|
+
type: "array",
|
|
2081
|
+
items: {
|
|
2082
|
+
type: "object",
|
|
2083
|
+
properties: {
|
|
2084
|
+
id: {
|
|
2085
|
+
type: "string",
|
|
2086
|
+
minLength: 1,
|
|
2087
|
+
maxLength: 300
|
|
2088
|
+
},
|
|
2089
|
+
name: {
|
|
2090
|
+
type: "string",
|
|
2091
|
+
minLength: 1,
|
|
2092
|
+
maxLength: 200
|
|
2093
|
+
},
|
|
2094
|
+
description: {
|
|
2095
|
+
type: "string",
|
|
2096
|
+
maxLength: 8000
|
|
2097
|
+
},
|
|
2098
|
+
contextWindow: {
|
|
2099
|
+
type: "integer",
|
|
2100
|
+
exclusiveMinimum: true,
|
|
2101
|
+
minimum: 0
|
|
2102
|
+
},
|
|
2103
|
+
maxOutputTokens: {
|
|
2104
|
+
type: "integer",
|
|
2105
|
+
exclusiveMinimum: true,
|
|
2106
|
+
minimum: 0
|
|
2107
|
+
},
|
|
2108
|
+
inputModalities: {
|
|
2109
|
+
type: "array",
|
|
2110
|
+
items: {
|
|
2111
|
+
type: "string",
|
|
2112
|
+
maxLength: 50
|
|
2113
|
+
},
|
|
2114
|
+
maxItems: 20
|
|
2115
|
+
},
|
|
2116
|
+
outputModalities: {
|
|
2117
|
+
type: "array",
|
|
2118
|
+
items: {
|
|
2119
|
+
type: "string",
|
|
2120
|
+
maxLength: 50
|
|
2121
|
+
},
|
|
2122
|
+
maxItems: 20
|
|
2123
|
+
},
|
|
2124
|
+
supportedParameters: {
|
|
2125
|
+
type: "array",
|
|
2126
|
+
items: {
|
|
2127
|
+
type: "string",
|
|
2128
|
+
maxLength: 100
|
|
2129
|
+
},
|
|
2130
|
+
maxItems: 100
|
|
2131
|
+
}
|
|
2132
|
+
},
|
|
2133
|
+
required: [
|
|
2134
|
+
"id",
|
|
2135
|
+
"name"
|
|
2136
|
+
],
|
|
2137
|
+
additionalProperties: false
|
|
2138
|
+
},
|
|
2139
|
+
maxItems: 1e4,
|
|
2140
|
+
default: []
|
|
2141
|
+
},
|
|
2142
|
+
version: {
|
|
2143
|
+
type: "integer",
|
|
2144
|
+
exclusiveMinimum: true,
|
|
2145
|
+
minimum: 0
|
|
2146
|
+
},
|
|
2147
|
+
updatedAt: {
|
|
2148
|
+
type: "string"
|
|
2149
|
+
}
|
|
2150
|
+
},
|
|
2151
|
+
required: [
|
|
2152
|
+
"id",
|
|
2153
|
+
"name",
|
|
2154
|
+
"baseUrl",
|
|
2155
|
+
"protocol",
|
|
2156
|
+
"version",
|
|
2157
|
+
"updatedAt"
|
|
2158
|
+
],
|
|
2159
|
+
additionalProperties: false
|
|
2160
|
+
},
|
|
2161
|
+
profile: {
|
|
2162
|
+
type: "object",
|
|
2163
|
+
properties: {
|
|
2164
|
+
id: {
|
|
2165
|
+
type: "string",
|
|
2166
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$"
|
|
2167
|
+
},
|
|
2168
|
+
name: {
|
|
2169
|
+
type: "string",
|
|
2170
|
+
minLength: 1,
|
|
2171
|
+
maxLength: 200
|
|
2172
|
+
},
|
|
2173
|
+
providerId: {
|
|
2174
|
+
type: "string",
|
|
2175
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$"
|
|
2176
|
+
},
|
|
2177
|
+
harness: {
|
|
2178
|
+
type: "string",
|
|
2179
|
+
enum: [
|
|
2180
|
+
"claude",
|
|
2181
|
+
"codex",
|
|
2182
|
+
"grok",
|
|
2183
|
+
"opencode2"
|
|
2184
|
+
]
|
|
2185
|
+
},
|
|
2186
|
+
model: {
|
|
2187
|
+
type: "string",
|
|
2188
|
+
minLength: 1,
|
|
2189
|
+
maxLength: 300
|
|
2190
|
+
},
|
|
2191
|
+
version: {
|
|
2192
|
+
type: "integer",
|
|
2193
|
+
exclusiveMinimum: true,
|
|
2194
|
+
minimum: 0
|
|
2195
|
+
},
|
|
2196
|
+
updatedAt: {
|
|
2197
|
+
type: "string"
|
|
2198
|
+
}
|
|
2199
|
+
},
|
|
2200
|
+
required: [
|
|
2201
|
+
"id",
|
|
2202
|
+
"name",
|
|
2203
|
+
"providerId",
|
|
2204
|
+
"harness",
|
|
2205
|
+
"model",
|
|
2206
|
+
"version",
|
|
2207
|
+
"updatedAt"
|
|
2208
|
+
],
|
|
2209
|
+
additionalProperties: false
|
|
2210
|
+
},
|
|
2211
|
+
catalog: {
|
|
2212
|
+
type: "object",
|
|
2213
|
+
properties: {
|
|
2214
|
+
models: {
|
|
2215
|
+
type: "array",
|
|
2216
|
+
items: {
|
|
2217
|
+
type: "object",
|
|
2218
|
+
properties: {
|
|
2219
|
+
id: {
|
|
2220
|
+
type: "string",
|
|
2221
|
+
minLength: 1,
|
|
2222
|
+
maxLength: 300
|
|
2223
|
+
},
|
|
2224
|
+
name: {
|
|
2225
|
+
type: "string",
|
|
2226
|
+
minLength: 1,
|
|
2227
|
+
maxLength: 200
|
|
2228
|
+
},
|
|
2229
|
+
description: {
|
|
2230
|
+
type: "string",
|
|
2231
|
+
maxLength: 8000
|
|
2232
|
+
},
|
|
2233
|
+
contextWindow: {
|
|
2234
|
+
type: "integer",
|
|
2235
|
+
exclusiveMinimum: true,
|
|
2236
|
+
minimum: 0
|
|
2237
|
+
},
|
|
2238
|
+
maxOutputTokens: {
|
|
2239
|
+
type: "integer",
|
|
2240
|
+
exclusiveMinimum: true,
|
|
2241
|
+
minimum: 0
|
|
2242
|
+
},
|
|
2243
|
+
inputModalities: {
|
|
2244
|
+
type: "array",
|
|
2245
|
+
items: {
|
|
2246
|
+
type: "string",
|
|
2247
|
+
maxLength: 50
|
|
2248
|
+
},
|
|
2249
|
+
maxItems: 20
|
|
2250
|
+
},
|
|
2251
|
+
outputModalities: {
|
|
2252
|
+
type: "array",
|
|
2253
|
+
items: {
|
|
2254
|
+
type: "string",
|
|
2255
|
+
maxLength: 50
|
|
2256
|
+
},
|
|
2257
|
+
maxItems: 20
|
|
2258
|
+
},
|
|
2259
|
+
supportedParameters: {
|
|
2260
|
+
type: "array",
|
|
2261
|
+
items: {
|
|
2262
|
+
type: "string",
|
|
2263
|
+
maxLength: 100
|
|
2264
|
+
},
|
|
2265
|
+
maxItems: 100
|
|
2266
|
+
}
|
|
2267
|
+
},
|
|
2268
|
+
required: [
|
|
2269
|
+
"id",
|
|
2270
|
+
"name"
|
|
2271
|
+
],
|
|
2272
|
+
additionalProperties: false
|
|
2273
|
+
}
|
|
2274
|
+
},
|
|
2275
|
+
refreshedAt: {
|
|
2276
|
+
type: "string"
|
|
2277
|
+
},
|
|
2278
|
+
source: {
|
|
2279
|
+
type: "string",
|
|
2280
|
+
enum: [
|
|
2281
|
+
"remote",
|
|
2282
|
+
"manual"
|
|
2283
|
+
]
|
|
2284
|
+
}
|
|
2285
|
+
},
|
|
2286
|
+
required: [
|
|
2287
|
+
"models",
|
|
2288
|
+
"refreshedAt",
|
|
2289
|
+
"source"
|
|
2290
|
+
],
|
|
2291
|
+
additionalProperties: false
|
|
2292
|
+
},
|
|
2293
|
+
planToken: {
|
|
2294
|
+
type: "string"
|
|
2295
|
+
},
|
|
2296
|
+
warnings: {
|
|
2297
|
+
type: "array",
|
|
2298
|
+
items: {
|
|
2299
|
+
type: "string"
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
},
|
|
2303
|
+
required: [
|
|
2304
|
+
"provider",
|
|
2305
|
+
"profile",
|
|
2306
|
+
"catalog",
|
|
2307
|
+
"planToken",
|
|
2308
|
+
"warnings"
|
|
2309
|
+
],
|
|
2310
|
+
additionalProperties: false
|
|
2311
|
+
},
|
|
2312
|
+
RunInput: {
|
|
2313
|
+
type: "object",
|
|
2314
|
+
properties: {
|
|
2315
|
+
profileId: {
|
|
2316
|
+
type: "string",
|
|
2317
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$"
|
|
2318
|
+
},
|
|
2319
|
+
harness: {
|
|
2320
|
+
type: "string",
|
|
2321
|
+
enum: [
|
|
2322
|
+
"claude",
|
|
2323
|
+
"codex",
|
|
2324
|
+
"grok",
|
|
2325
|
+
"opencode2"
|
|
2326
|
+
]
|
|
2327
|
+
},
|
|
2328
|
+
model: {
|
|
2329
|
+
type: "string",
|
|
2330
|
+
minLength: 1,
|
|
2331
|
+
maxLength: 300
|
|
2332
|
+
},
|
|
2333
|
+
planToken: {
|
|
2334
|
+
type: "string",
|
|
2335
|
+
pattern: "^[a-f0-9]{64}$"
|
|
2336
|
+
}
|
|
2337
|
+
},
|
|
2338
|
+
required: [
|
|
2339
|
+
"profileId",
|
|
2340
|
+
"harness",
|
|
2341
|
+
"model",
|
|
2342
|
+
"planToken"
|
|
2343
|
+
],
|
|
2344
|
+
additionalProperties: false
|
|
2345
|
+
},
|
|
2346
|
+
RunUpdate: {
|
|
2347
|
+
type: "object",
|
|
2348
|
+
properties: {
|
|
2349
|
+
status: {
|
|
2350
|
+
type: "string",
|
|
2351
|
+
enum: [
|
|
2352
|
+
"exited",
|
|
2353
|
+
"failed",
|
|
2354
|
+
"interrupted"
|
|
2355
|
+
]
|
|
2356
|
+
},
|
|
2357
|
+
exitCode: {
|
|
2358
|
+
type: "integer",
|
|
2359
|
+
minimum: 0,
|
|
2360
|
+
maximum: 255
|
|
2361
|
+
}
|
|
2362
|
+
},
|
|
2363
|
+
required: [
|
|
2364
|
+
"status",
|
|
2365
|
+
"exitCode"
|
|
2366
|
+
],
|
|
2367
|
+
additionalProperties: false
|
|
2368
|
+
},
|
|
2369
|
+
Run: {
|
|
2370
|
+
type: "object",
|
|
2371
|
+
properties: {
|
|
2372
|
+
profileId: {
|
|
2373
|
+
type: "string",
|
|
2374
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$"
|
|
2375
|
+
},
|
|
2376
|
+
harness: {
|
|
2377
|
+
type: "string",
|
|
2378
|
+
enum: [
|
|
2379
|
+
"claude",
|
|
2380
|
+
"codex",
|
|
2381
|
+
"grok",
|
|
2382
|
+
"opencode2"
|
|
2383
|
+
]
|
|
2384
|
+
},
|
|
2385
|
+
model: {
|
|
2386
|
+
type: "string",
|
|
2387
|
+
minLength: 1,
|
|
2388
|
+
maxLength: 300
|
|
2389
|
+
},
|
|
2390
|
+
planToken: {
|
|
2391
|
+
type: "string",
|
|
2392
|
+
pattern: "^[a-f0-9]{64}$"
|
|
2393
|
+
},
|
|
2394
|
+
version: {
|
|
2395
|
+
type: "integer",
|
|
2396
|
+
exclusiveMinimum: true,
|
|
2397
|
+
minimum: 0
|
|
2398
|
+
},
|
|
2399
|
+
updatedAt: {
|
|
2400
|
+
type: "string"
|
|
2401
|
+
},
|
|
2402
|
+
providerId: {
|
|
2403
|
+
type: "string",
|
|
2404
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$"
|
|
2405
|
+
},
|
|
2406
|
+
providerVersion: {
|
|
2407
|
+
type: "integer",
|
|
2408
|
+
exclusiveMinimum: true,
|
|
2409
|
+
minimum: 0
|
|
2410
|
+
},
|
|
2411
|
+
profileVersion: {
|
|
2412
|
+
type: "integer",
|
|
2413
|
+
exclusiveMinimum: true,
|
|
2414
|
+
minimum: 0
|
|
2415
|
+
},
|
|
2416
|
+
id: {
|
|
2417
|
+
type: "string",
|
|
2418
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$"
|
|
2419
|
+
},
|
|
2420
|
+
status: {
|
|
2421
|
+
type: "string",
|
|
2422
|
+
enum: [
|
|
2423
|
+
"running",
|
|
2424
|
+
"exited",
|
|
2425
|
+
"failed",
|
|
2426
|
+
"interrupted"
|
|
2427
|
+
]
|
|
2428
|
+
},
|
|
2429
|
+
startedAt: {
|
|
2430
|
+
type: "string"
|
|
2431
|
+
},
|
|
2432
|
+
endedAt: {
|
|
2433
|
+
type: "string"
|
|
2434
|
+
},
|
|
2435
|
+
exitCode: {
|
|
2436
|
+
type: "integer"
|
|
2437
|
+
}
|
|
2438
|
+
},
|
|
2439
|
+
required: [
|
|
2440
|
+
"profileId",
|
|
2441
|
+
"harness",
|
|
2442
|
+
"model",
|
|
2443
|
+
"planToken",
|
|
2444
|
+
"version",
|
|
2445
|
+
"updatedAt",
|
|
2446
|
+
"providerId",
|
|
2447
|
+
"providerVersion",
|
|
2448
|
+
"profileVersion",
|
|
2449
|
+
"id",
|
|
2450
|
+
"status",
|
|
2451
|
+
"startedAt"
|
|
2452
|
+
],
|
|
2453
|
+
additionalProperties: false
|
|
2454
|
+
},
|
|
2455
|
+
Health: {
|
|
2456
|
+
type: "object",
|
|
2457
|
+
properties: {
|
|
2458
|
+
status: {
|
|
2459
|
+
type: "string",
|
|
2460
|
+
enum: [
|
|
2461
|
+
"ok",
|
|
2462
|
+
"degraded",
|
|
2463
|
+
"unavailable"
|
|
2464
|
+
]
|
|
2465
|
+
},
|
|
2466
|
+
version: {
|
|
2467
|
+
type: "string"
|
|
2468
|
+
},
|
|
2469
|
+
backend: {
|
|
2470
|
+
type: "string",
|
|
2471
|
+
enum: [
|
|
2472
|
+
"sqlite",
|
|
2473
|
+
"postgresql"
|
|
2474
|
+
]
|
|
2475
|
+
}
|
|
2476
|
+
},
|
|
2477
|
+
required: [
|
|
2478
|
+
"status",
|
|
2479
|
+
"version",
|
|
2480
|
+
"backend"
|
|
2481
|
+
],
|
|
2482
|
+
additionalProperties: false
|
|
2483
|
+
},
|
|
2484
|
+
Ready: {
|
|
2485
|
+
type: "object",
|
|
2486
|
+
properties: {
|
|
2487
|
+
ready: {
|
|
2488
|
+
type: "boolean"
|
|
2489
|
+
},
|
|
2490
|
+
reason: {
|
|
2491
|
+
type: "string"
|
|
2492
|
+
}
|
|
2493
|
+
},
|
|
2494
|
+
required: [
|
|
2495
|
+
"ready"
|
|
2496
|
+
],
|
|
2497
|
+
additionalProperties: false
|
|
2498
|
+
},
|
|
2499
|
+
Version: {
|
|
2500
|
+
type: "object",
|
|
2501
|
+
properties: {
|
|
2502
|
+
version: {
|
|
2503
|
+
type: "string"
|
|
2504
|
+
}
|
|
2505
|
+
},
|
|
2506
|
+
required: [
|
|
2507
|
+
"version"
|
|
2508
|
+
],
|
|
2509
|
+
additionalProperties: false
|
|
2510
|
+
},
|
|
2511
|
+
LaunchInput: {
|
|
2512
|
+
type: "object",
|
|
2513
|
+
properties: {
|
|
2514
|
+
profileId: {
|
|
2515
|
+
type: "string",
|
|
2516
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$"
|
|
2517
|
+
}
|
|
2518
|
+
},
|
|
2519
|
+
required: [
|
|
2520
|
+
"profileId"
|
|
2521
|
+
],
|
|
2522
|
+
additionalProperties: false
|
|
2523
|
+
},
|
|
2524
|
+
Empty: {
|
|
2525
|
+
type: "object",
|
|
2526
|
+
properties: {},
|
|
2527
|
+
additionalProperties: false
|
|
2528
|
+
},
|
|
2529
|
+
Error: {
|
|
2530
|
+
type: "object",
|
|
2531
|
+
properties: {
|
|
2532
|
+
error: {
|
|
2533
|
+
type: "object",
|
|
2534
|
+
properties: {
|
|
2535
|
+
code: {
|
|
2536
|
+
type: "string"
|
|
2537
|
+
},
|
|
2538
|
+
message: {
|
|
2539
|
+
type: "string"
|
|
2540
|
+
},
|
|
2541
|
+
requestId: {
|
|
2542
|
+
type: "string"
|
|
2543
|
+
}
|
|
2544
|
+
},
|
|
2545
|
+
required: [
|
|
2546
|
+
"code",
|
|
2547
|
+
"message",
|
|
2548
|
+
"requestId"
|
|
2549
|
+
],
|
|
2550
|
+
additionalProperties: false
|
|
2551
|
+
}
|
|
2552
|
+
},
|
|
2553
|
+
required: [
|
|
2554
|
+
"error"
|
|
2555
|
+
],
|
|
2556
|
+
additionalProperties: false
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
};
|
|
2561
|
+
|
|
2562
|
+
// src/service.ts
|
|
2563
|
+
var snapshot = (profile, provider, catalog) => createHash("sha256").update(JSON.stringify([profile, provider, { models: catalog.models, source: catalog.source }])).digest("hex");
|
|
2564
|
+
var hash = (s) => createHash("sha256").update(s).digest();
|
|
2565
|
+
function createHandler(store, apiKey, providerEnv = process.env) {
|
|
2566
|
+
if (!apiKey || apiKey.length < 24)
|
|
2567
|
+
throw new Fault(500, "auth_config", "Set HASNA_SWITCHER_API_KEY to a random token of at least 24 characters.");
|
|
2568
|
+
const expected = hash(`Bearer ${apiKey}`);
|
|
2569
|
+
return async (request) => {
|
|
2570
|
+
const requestId = crypto.randomUUID();
|
|
2571
|
+
const json = (body, status = 200) => Response.json(body, { status, headers: { "x-request-id": requestId, "cache-control": "no-store", "x-content-type-options": "nosniff" } });
|
|
2572
|
+
try {
|
|
2573
|
+
const url = new URL(request.url);
|
|
2574
|
+
const route = url.pathname.replace(/\/$/, "");
|
|
2575
|
+
if (request.method === "GET" && route === "/health")
|
|
2576
|
+
return json({ status: "ok", version: VERSION, backend: store.engine });
|
|
2577
|
+
if (request.method === "GET" && route === "/version")
|
|
2578
|
+
return json({ version: VERSION });
|
|
2579
|
+
if (request.method === "GET" && route === "/ready") {
|
|
2580
|
+
try {
|
|
2581
|
+
await store.ready();
|
|
2582
|
+
return json({ ready: true });
|
|
2583
|
+
} catch {
|
|
2584
|
+
return json({ ready: false, reason: "Storage is unavailable." }, 503);
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
if (!timingSafeEqual(expected, hash(request.headers.get("authorization") ?? "")))
|
|
2588
|
+
throw new Fault(401, "unauthorized", "A valid API bearer token is required.");
|
|
2589
|
+
if (request.method === "GET" && ["/v1/openapi.json", "/openapi.json"].includes(route))
|
|
2590
|
+
return json(openapi_default);
|
|
2591
|
+
const parts = route.split("/").filter(Boolean);
|
|
2592
|
+
if (parts[0] !== "v1")
|
|
2593
|
+
throw new Fault(404, "not_found", "Route was not found.");
|
|
2594
|
+
const resource = parts[1];
|
|
2595
|
+
const id = parts[2];
|
|
2596
|
+
if (id)
|
|
2597
|
+
parse(idSchema, id);
|
|
2598
|
+
const page = () => parse(z2.object({
|
|
2599
|
+
limit: z2.coerce.number().int().min(1).max(1000).default(100),
|
|
2600
|
+
offset: z2.coerce.number().int().min(0).max(1e6).default(0),
|
|
2601
|
+
search: z2.string().max(200).default("")
|
|
2602
|
+
}).strict(), Object.fromEntries(url.searchParams));
|
|
2603
|
+
if (request.method === "GET") {
|
|
2604
|
+
if (["providers", "profiles", "runs"].includes(resource) && parts.length <= 3) {
|
|
2605
|
+
const kind = resource;
|
|
2606
|
+
return json(id ? await store.get(kind, id) : await store.list(kind, page()));
|
|
2607
|
+
}
|
|
2608
|
+
if (resource === "providers" && id && parts[3] === "models" && parts.length === 4) {
|
|
2609
|
+
const catalog = await store.get("catalogs", id);
|
|
2610
|
+
const p = page();
|
|
2611
|
+
const filtered = catalog.models.filter((m) => [m.id, m.name].some((s) => s.toLowerCase().includes(p.search.toLowerCase())));
|
|
2612
|
+
return json({ ...catalog, models: undefined, data: filtered.slice(p.offset, p.offset + p.limit).map((m) => ({ ...m, codingEligible: codingEligible(m) })), total: filtered.length, ...p });
|
|
2613
|
+
}
|
|
2614
|
+
throw new Fault(404, "not_found", "Route was not found.");
|
|
2615
|
+
}
|
|
2616
|
+
if (!["POST", "PUT", "PATCH", "DELETE"].includes(request.method))
|
|
2617
|
+
throw new Fault(405, "method_not_allowed", "Method is not supported.");
|
|
2618
|
+
const key = request.headers.get("idempotency-key");
|
|
2619
|
+
if (!key || !/^[a-zA-Z0-9._:-]{8,128}$/.test(key))
|
|
2620
|
+
throw new Fault(400, "idempotency_required", "Supply an Idempotency-Key of 8\u2013128 ASCII letters, digits, dots, colons, underscores or dashes.");
|
|
2621
|
+
let body = {};
|
|
2622
|
+
if (request.method !== "DELETE") {
|
|
2623
|
+
if (!request.headers.get("content-type")?.startsWith("application/json"))
|
|
2624
|
+
throw new Fault(415, "content_type", "Send application/json.");
|
|
2625
|
+
try {
|
|
2626
|
+
body = await boundedJson(request, 1024 * 1024);
|
|
2627
|
+
} catch {
|
|
2628
|
+
throw new Fault(400, "invalid_json", "Request must contain valid JSON under 1 MiB.");
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
const fingerprint = hash(JSON.stringify([request.method, route, body, request.headers.get("if-match")])).toString("hex");
|
|
2632
|
+
const version = () => {
|
|
2633
|
+
const v = request.headers.get("if-match");
|
|
2634
|
+
if (!v || !/^[1-9]\d*$/.test(v))
|
|
2635
|
+
throw new Fault(428, "version_required", "Supply the current numeric version in If-Match.");
|
|
2636
|
+
return Number(v);
|
|
2637
|
+
};
|
|
2638
|
+
const replay = await store.replay(key, fingerprint);
|
|
2639
|
+
if (replay.found)
|
|
2640
|
+
return json(replay.value, request.method === "POST" && ["providers", "profiles", "runs"].includes(resource) && !id ? 201 : 200);
|
|
2641
|
+
let refreshed;
|
|
2642
|
+
if (resource === "providers" && id && parts[3] === "refresh" && parts.length === 4 && request.method === "POST") {
|
|
2643
|
+
parse(z2.object({}).strict(), body);
|
|
2644
|
+
const provider = await store.get("providers", id);
|
|
2645
|
+
refreshed = { provider, catalog: await discover(provider, providerEnv) };
|
|
2646
|
+
}
|
|
2647
|
+
const result = await store.mutate(key, fingerprint, async (db) => {
|
|
2648
|
+
if ((resource === "providers" || resource === "profiles") && parts.length <= 3) {
|
|
2649
|
+
if (request.method === "DELETE" && id)
|
|
2650
|
+
return store.remove(resource, id, version(), db);
|
|
2651
|
+
if (request.method === "POST" && !id || request.method === "PUT" && id) {
|
|
2652
|
+
const value = resource === "providers" ? parse(providerInputSchema, body) : parse(profileInputSchema, body);
|
|
2653
|
+
if (id && value.id !== id)
|
|
2654
|
+
throw new Fault(400, "id_mismatch", "Path and body IDs must match.");
|
|
2655
|
+
if (resource === "profiles") {
|
|
2656
|
+
const profile = value;
|
|
2657
|
+
const provider = await store.get("providers", profile.providerId, db);
|
|
2658
|
+
if (!compatible(profile.harness, provider.protocol))
|
|
2659
|
+
throw new Fault(422, "protocol_mismatch", "Harness does not support this provider protocol.");
|
|
2660
|
+
}
|
|
2661
|
+
const saved = await store.put(resource, value, id ? version() : undefined, db);
|
|
2662
|
+
if (resource === "providers" && id)
|
|
2663
|
+
await db.unsafe("DELETE FROM switcher_catalogs WHERE id = $1", [id]);
|
|
2664
|
+
return saved;
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
if (resource === "providers" && id && parts[3] === "refresh" && parts.length === 4 && request.method === "POST") {
|
|
2668
|
+
if (store.engine === "postgresql")
|
|
2669
|
+
await db.unsafe("SELECT id FROM switcher_providers WHERE id = $1 FOR SHARE", [id]);
|
|
2670
|
+
const provider = await store.get("providers", id, db);
|
|
2671
|
+
if (!refreshed || provider.version !== refreshed.provider.version)
|
|
2672
|
+
throw new Fault(409, "provider_changed", "Provider changed during discovery; refresh again.");
|
|
2673
|
+
const catalog = refreshed.catalog;
|
|
2674
|
+
let old;
|
|
2675
|
+
try {
|
|
2676
|
+
old = await store.get("catalogs", id, db);
|
|
2677
|
+
} catch (e) {
|
|
2678
|
+
if (!(e instanceof Fault && e.status === 404))
|
|
2679
|
+
throw e;
|
|
2680
|
+
}
|
|
2681
|
+
return store.put("catalogs", { id, ...catalog }, old?.version, db);
|
|
2682
|
+
}
|
|
2683
|
+
if (resource === "launch-plans" && !id && request.method === "POST") {
|
|
2684
|
+
const { profileId } = parse(z2.object({ profileId: idSchema }).strict(), body);
|
|
2685
|
+
const profile = await store.get("profiles", profileId, db);
|
|
2686
|
+
const provider = await store.get("providers", profile.providerId, db);
|
|
2687
|
+
if (!compatible(profile.harness, provider.protocol))
|
|
2688
|
+
throw new Fault(422, "protocol_mismatch", "Harness does not support this provider protocol.");
|
|
2689
|
+
let catalog;
|
|
2690
|
+
try {
|
|
2691
|
+
catalog = await store.get("catalogs", provider.id, db);
|
|
2692
|
+
} catch (e) {
|
|
2693
|
+
if (e instanceof Fault && e.status === 404)
|
|
2694
|
+
throw new Fault(422, "catalog_missing", "Refresh the provider catalog before launching.");
|
|
2695
|
+
throw e;
|
|
2696
|
+
}
|
|
2697
|
+
const selected = catalog.models.find((m) => m.id === profile.model);
|
|
2698
|
+
if (!selected)
|
|
2699
|
+
throw new Fault(422, "model_missing", "Selected model is not in the provider catalog.");
|
|
2700
|
+
if (!codingEligible(selected))
|
|
2701
|
+
throw new Fault(422, "model_ineligible", "Selected model explicitly lacks text output or tool support.");
|
|
2702
|
+
const warnings = [];
|
|
2703
|
+
if (!selected.supportedParameters)
|
|
2704
|
+
warnings.push("Provider does not declare tool capabilities; execution compatibility is unverified.");
|
|
2705
|
+
if (profile.harness === "claude" && !/claude/i.test(profile.model))
|
|
2706
|
+
warnings.push("Anthropic does not support non-Claude models in Claude Code; this combination is experimental.");
|
|
2707
|
+
if (Date.now() - Date.parse(catalog.refreshedAt) > 300000)
|
|
2708
|
+
warnings.push("Catalog snapshot is older than five minutes; refresh before launching.");
|
|
2709
|
+
return { profile, provider, catalog, warnings, planToken: snapshot(profile, provider, catalog) };
|
|
2710
|
+
}
|
|
2711
|
+
if (resource === "runs" && !id && request.method === "POST") {
|
|
2712
|
+
const input = parse(runInputSchema, body);
|
|
2713
|
+
if (store.engine === "postgresql")
|
|
2714
|
+
await db.unsafe("SELECT id FROM switcher_profiles WHERE id = $1 FOR SHARE", [input.profileId]);
|
|
2715
|
+
const profile = await store.get("profiles", input.profileId, db);
|
|
2716
|
+
if (store.engine === "postgresql") {
|
|
2717
|
+
await db.unsafe("SELECT id FROM switcher_providers WHERE id = $1 FOR SHARE", [profile.providerId]);
|
|
2718
|
+
await db.unsafe("SELECT id FROM switcher_catalogs WHERE id = $1 FOR SHARE", [profile.providerId]);
|
|
2719
|
+
}
|
|
2720
|
+
const provider = await store.get("providers", profile.providerId, db);
|
|
2721
|
+
let catalog;
|
|
2722
|
+
try {
|
|
2723
|
+
catalog = await store.get("catalogs", profile.providerId, db);
|
|
2724
|
+
} catch (error) {
|
|
2725
|
+
if (error instanceof Fault && error.status === 404)
|
|
2726
|
+
throw new Fault(409, "plan_changed", "Catalog changed; request a fresh launch plan.");
|
|
2727
|
+
throw error;
|
|
2728
|
+
}
|
|
2729
|
+
if (profile.harness !== input.harness || profile.model !== input.model || snapshot(profile, provider, catalog) !== input.planToken)
|
|
2730
|
+
throw new Fault(409, "plan_changed", "Provider, profile or catalog changed; request a fresh launch plan.");
|
|
2731
|
+
return store.put("runs", { ...input, providerId: provider.id, providerVersion: provider.version, profileVersion: profile.version, id: crypto.randomUUID(), status: "running", startedAt: new Date().toISOString() }, undefined, db);
|
|
2732
|
+
}
|
|
2733
|
+
if (resource === "runs" && id && request.method === "PATCH" && parts.length === 3) {
|
|
2734
|
+
const input = parse(runUpdateSchema, body);
|
|
2735
|
+
const run = await store.get("runs", id, db);
|
|
2736
|
+
if (run.status !== "running")
|
|
2737
|
+
throw new Fault(409, "run_finished", "Run has already finished.");
|
|
2738
|
+
return store.put("runs", { ...run, ...input, endedAt: new Date().toISOString() }, version(), db);
|
|
2739
|
+
}
|
|
2740
|
+
throw new Fault(404, "not_found", "Route was not found.");
|
|
2741
|
+
});
|
|
2742
|
+
return json(result, request.method === "POST" && ["providers", "profiles", "runs"].includes(resource) && !id ? 201 : 200);
|
|
2743
|
+
} catch (error) {
|
|
2744
|
+
const safe = error instanceof Fault ? error : new Fault(500, "internal_error", "Request failed.");
|
|
2745
|
+
return json({ error: { code: safe.code, message: safe.message, requestId } }, safe.status);
|
|
2746
|
+
}
|
|
2747
|
+
};
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
// src/serve.ts
|
|
2751
|
+
async function main(args = process.argv.slice(2)) {
|
|
2752
|
+
const { values } = parseArgs({ args, options: {
|
|
2753
|
+
host: { type: "string" },
|
|
2754
|
+
port: { type: "string" },
|
|
2755
|
+
"data-dir": { type: "string" },
|
|
2756
|
+
sqlite: { type: "string" },
|
|
2757
|
+
json: { type: "boolean" },
|
|
2758
|
+
version: { type: "boolean" },
|
|
2759
|
+
help: { type: "boolean" }
|
|
2760
|
+
} });
|
|
2761
|
+
if (values.version) {
|
|
2762
|
+
console.log(VERSION);
|
|
2763
|
+
return;
|
|
2764
|
+
}
|
|
2765
|
+
if (values.help) {
|
|
2766
|
+
console.log(`switcher-serve --sqlite PATH | --data-dir DIR | inject HASNA_SWITCHER_DATABASE_URL
|
|
2767
|
+
--host HOST (127.0.0.1) --port PORT (8080; 0 allocates a port) --json --version
|
|
2768
|
+
Requires HASNA_SWITCHER_API_KEY (24+ characters). Provider credentials: SWITCHER_PROVIDER_* environment references.`);
|
|
2769
|
+
return;
|
|
2770
|
+
}
|
|
2771
|
+
const port = Number(values.port ?? process.env.PORT ?? 8080);
|
|
2772
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535)
|
|
2773
|
+
throw new Fault(400, "invalid_port", "Port must be an integer between 0 and 65535.");
|
|
2774
|
+
const apiKey = process.env.HASNA_SWITCHER_API_KEY ?? "";
|
|
2775
|
+
if (apiKey.length < 24)
|
|
2776
|
+
throw new Fault(500, "auth_config", "Set HASNA_SWITCHER_API_KEY to a random token of at least 24 characters.");
|
|
2777
|
+
if (values.sqlite && values["data-dir"])
|
|
2778
|
+
throw new Fault(400, "storage_config", "Choose --sqlite or --data-dir.");
|
|
2779
|
+
const store = await Store.open({ databaseUrl: process.env.HASNA_SWITCHER_DATABASE_URL, sqlitePath: values.sqlite ?? (values["data-dir"] ? join(values["data-dir"], "switcher.db") : process.env.HASNA_SWITCHER_SQLITE_PATH) });
|
|
2780
|
+
const server = Bun.serve({ hostname: values.host ?? "127.0.0.1", port, maxRequestBodySize: 1024 * 1024, idleTimeout: 60, fetch: createHandler(store, apiKey) });
|
|
2781
|
+
console.log(JSON.stringify({ event: "listening", version: VERSION, url: server.url.href, storage: store.engine }));
|
|
2782
|
+
let stopping = false;
|
|
2783
|
+
const stop = async () => {
|
|
2784
|
+
if (stopping)
|
|
2785
|
+
return;
|
|
2786
|
+
stopping = true;
|
|
2787
|
+
await server.stop();
|
|
2788
|
+
await store.close();
|
|
2789
|
+
};
|
|
2790
|
+
process.once("SIGTERM", stop);
|
|
2791
|
+
process.once("SIGINT", stop);
|
|
2792
|
+
}
|
|
2793
|
+
if (import.meta.main)
|
|
2794
|
+
main().catch((error) => {
|
|
2795
|
+
console.error(JSON.stringify({ error: error instanceof Fault ? error.message : "Server startup failed; check configuration." }));
|
|
2796
|
+
process.exitCode = 1;
|
|
2797
|
+
});
|
|
2798
|
+
export {
|
|
2799
|
+
main
|
|
2800
|
+
};
|