@hasna/switcher 0.1.1 → 0.1.2

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