@juspay/neurolink 12.50.0 → 12.51.1

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/dist/browser/neurolink.min.js +666 -666
  3. package/dist/cli/commands/setup.d.ts +7 -5
  4. package/dist/cli/commands/setup.js +10 -8
  5. package/dist/constants/enums.d.ts +7 -15
  6. package/dist/constants/enums.js +8 -16
  7. package/dist/constants/timeouts.d.ts +6 -0
  8. package/dist/constants/timeouts.js +6 -0
  9. package/dist/factories/providerDescriptors.d.ts +17 -3
  10. package/dist/factories/providerDescriptors.js +81 -56
  11. package/dist/factories/providerRegistry.d.ts +9 -1
  12. package/dist/factories/providerRegistry.js +19 -9
  13. package/dist/models/manifestRegistry.js +40 -3
  14. package/dist/providers/catalog/index.generated.d.ts +3 -1
  15. package/dist/providers/catalog/index.generated.js +11 -0
  16. package/dist/providers/catalog/laya.json +107 -0
  17. package/dist/providers/catalog/loader.d.ts +7 -4
  18. package/dist/providers/catalog/loader.js +10 -3
  19. package/dist/providers/catalog/provider-catalog.schema.json +218 -11
  20. package/dist/providers/catalog/schema.d.ts +11 -9
  21. package/dist/providers/catalog/schema.js +333 -59
  22. package/dist/providers/configuredDecision.d.ts +23 -0
  23. package/dist/providers/configuredDecision.js +111 -0
  24. package/dist/providers/decide/baseUrl.d.ts +2 -0
  25. package/dist/providers/decide/baseUrl.js +14 -0
  26. package/dist/providers/decide/errors.d.ts +8 -0
  27. package/dist/providers/decide/errors.js +57 -0
  28. package/dist/providers/decide/formatters.d.ts +2 -0
  29. package/dist/providers/decide/formatters.js +4 -0
  30. package/dist/providers/decide/paths.d.ts +1 -0
  31. package/dist/providers/decide/paths.js +21 -0
  32. package/dist/providers/decide/template.d.ts +3 -0
  33. package/dist/providers/decide/template.js +13 -0
  34. package/dist/types/providerCatalog.d.ts +74 -1
  35. package/dist/types/providerCatalog.generated.d.ts +2 -2
  36. package/dist/types/providers.d.ts +12 -9
  37. package/dist/utils/errorHandling.d.ts +11 -1
  38. package/dist/utils/errorHandling.js +17 -2
  39. package/dist/utils/modelChoices.js +11 -25
  40. package/dist/utils/providerConfig.d.ts +0 -5
  41. package/dist/utils/providerConfig.js +0 -17
  42. package/dist/workflow/core/ensembleExecutor.js +29 -16
  43. package/docs-site/static/search-index.json +7 -7
  44. package/package.json +7 -2
  45. package/dist/models/manifests/laya.d.ts +0 -12
  46. package/dist/models/manifests/laya.js +0 -54
  47. package/dist/providers/laya.d.ts +0 -40
  48. package/dist/providers/laya.js +0 -152
@@ -0,0 +1,21 @@
1
+ export function readPath(value, path) {
2
+ const segments = path.split(".");
3
+ if (segments.some((segment) => segment === "")) {
4
+ return undefined;
5
+ }
6
+ let current = value;
7
+ for (const segment of segments) {
8
+ if (typeof current !== "object" ||
9
+ current === null ||
10
+ Array.isArray(current)) {
11
+ return undefined;
12
+ }
13
+ // Own data properties only: no prototype names, and no getter is run.
14
+ const descriptor = Object.getOwnPropertyDescriptor(current, segment);
15
+ if (descriptor === undefined || !("value" in descriptor)) {
16
+ return undefined;
17
+ }
18
+ current = descriptor.value;
19
+ }
20
+ return current;
21
+ }
@@ -0,0 +1,3 @@
1
+ export declare function fillEndpointTemplate(template: string, parts: {
2
+ baseURL: string;
3
+ }): string;
@@ -0,0 +1,13 @@
1
+ const PLACEHOLDER = /\{([^{}]*)\}/g;
2
+ export function fillEndpointTemplate(template, parts) {
3
+ if (/[{}]/.test(template.replace(PLACEHOLDER, ""))) {
4
+ throw new Error(`Decide endpoint template "${template}" has an unmatched brace; write placeholders as {baseURL}.`);
5
+ }
6
+ // A function replacer, so "$&" or "$1" inside a base URL is inserted as text.
7
+ return template.replace(PLACEHOLDER, (_match, name) => {
8
+ if (name !== "baseURL") {
9
+ throw new Error(`Decide endpoint template "${template}" uses an unknown placeholder {${name}}; the only one supported is {baseURL}.`);
10
+ }
11
+ return parts.baseURL;
12
+ });
13
+ }
@@ -4,6 +4,7 @@
4
4
  * src/lib/providers/catalog/<id>.json file per provider is the single
5
5
  * source of truth; see docs/superpowers/plans/2026-08-28-provider-json-catalog-spec.md.
6
6
  */
7
+ import type { DecideDialectName, DecisionErrorKind, DecisionLimits } from "./decision.js";
7
8
  export type CatalogPricingPerMTok = {
8
9
  input: number;
9
10
  output: number;
@@ -196,7 +197,7 @@ export type ProviderCatalogJson = {
196
197
  */
197
198
  credentialsKey?: string;
198
199
  aliases: string[];
199
- tier: 2;
200
+ tier?: 2;
200
201
  wire: CatalogWire;
201
202
  models: {
202
203
  default: string;
@@ -254,3 +255,75 @@ export type ProviderCatalogJson = {
254
255
  setup: CatalogSetup;
255
256
  evidence: CatalogEvidence;
256
257
  };
258
+ export type DecideFormatterName = "fastapi-validation";
259
+ export type DecideBaseURLTransform = "strip-trailing-slash";
260
+ export type DecideErrorRule = {
261
+ /** One status or a list of them. A rule sets this or `statusMin`, never both. */
262
+ status?: number | readonly number[];
263
+ /** Matches every status at or above this value. */
264
+ statusMin?: number;
265
+ kind: DecisionErrorKind;
266
+ retryable?: boolean;
267
+ };
268
+ export type DecideBodyShape = {
269
+ /** Dot path into the parsed error body. */
270
+ path: string;
271
+ isArray?: boolean;
272
+ formatter?: DecideFormatterName;
273
+ kind?: DecisionErrorKind;
274
+ retryable?: boolean;
275
+ };
276
+ export type DecideErrors = {
277
+ rules: readonly DecideErrorRule[];
278
+ default: DecisionErrorKind;
279
+ bodyShapes: readonly DecideBodyShape[];
280
+ };
281
+ export type DecideModel = {
282
+ placement: "body" | "body-unless-alias";
283
+ /** With `body-unless-alias`: the model name that means "send no model". */
284
+ alias?: string;
285
+ };
286
+ export type DecideBlock = {
287
+ dialect: DecideDialectName;
288
+ /** Decision order, lower runs first; unique across decide providers. */
289
+ order: number;
290
+ /** True when the provider has no built-in endpoint and the caller must supply one. */
291
+ baseURLRequired?: boolean;
292
+ baseURLNormalize?: readonly DecideBaseURLTransform[];
293
+ /** Request URL; `{baseURL}` is the only placeholder. */
294
+ endpoint: string;
295
+ model: DecideModel;
296
+ requestIdHeaders?: readonly string[];
297
+ response?: {
298
+ modelPaths?: readonly string[];
299
+ };
300
+ errors: DecideErrors;
301
+ };
302
+ export type DecideCatalogJson = {
303
+ $schema?: string;
304
+ id: string;
305
+ displayName: string;
306
+ enumTypeName?: string;
307
+ credentialsKey?: string;
308
+ aliases: string[];
309
+ inferenceKinds: readonly ["decide"];
310
+ tier?: 2;
311
+ wire: CatalogWire;
312
+ models: {
313
+ default: string;
314
+ catalog: Record<string, CatalogModelSpec>;
315
+ topModels?: string[];
316
+ testModel?: string;
317
+ };
318
+ timeouts?: {
319
+ decideMs?: number;
320
+ };
321
+ decisionLimits?: DecisionLimits;
322
+ decide: DecideBlock;
323
+ setup: CatalogSetup;
324
+ evidence: CatalogEvidence;
325
+ };
326
+ export type CatalogEntry = ProviderCatalogJson | DecideCatalogJson;
327
+ export type CatalogKeySource = Pick<CatalogEntry, "id" | "credentialsKey">;
328
+ export type CatalogEnvSource = Pick<CatalogEntry, "id" | "wire">;
329
+ export type CatalogSetupSource = Pick<CatalogEntry, "id" | "displayName" | "wire" | "setup">;
@@ -1,2 +1,2 @@
1
- export type CatalogProviderName = "a2agent" | "abliteration" | "above-dev" | "ai21" | "aiand" | "aihubmix" | "aionlabs" | "ambient" | "api-route" | "arcee" | "atlas-cloud" | "avian-io" | "baidu-qianfan" | "baseten" | "bee-heossi" | "byteplus-modelark" | "bytez" | "cerebras" | "charm-hyper" | "chutes" | "cloudflare" | "crusoe" | "dashscope" | "deepinfra" | "deepseek" | "empiriolabs" | "featherless-ai" | "fireworks" | "friendli" | "gmicloud" | "gradientai" | "groq" | "hetzner-inference" | "huggingface" | "inception-labs" | "inceptron" | "inco" | "inference-net" | "io-intelligence" | "koscompute" | "lemonfox-ai" | "lilac" | "llmtech" | "mancer" | "meta-model-api" | "minimax" | "mistral" | "moark" | "modelscope" | "moonshot-ai" | "morph" | "naver-clova-studio" | "nebius" | "neuralwatt" | "novita" | "ovhcloud" | "parasail" | "pareto-inference" | "perplexity" | "poolside" | "prime-intellect" | "reka" | "sakana-ai" | "sambanova" | "sarvam" | "scaleway" | "siliconflow" | "stackit" | "stepfun" | "subconscious" | "synthetic" | "telnyx" | "thinking-machines" | "tinfoil" | "together-ai" | "umans-ai" | "upstage" | "venice-ai" | "vispark" | "vultr-inference" | "wafer" | "wandb-inference" | "xai" | "z-ai";
2
- export type CatalogCredentialKey = "a2agent" | "abliteration" | "aboveDev" | "ai21" | "aiand" | "aihubmix" | "aionlabs" | "ambient" | "apiRoute" | "arcee" | "atlasCloud" | "avianIo" | "baiduQianfan" | "baseten" | "beeHeossi" | "byteplusModelark" | "bytez" | "cerebras" | "charmHyper" | "chutes" | "cloudflare" | "crusoe" | "dashscope" | "deepinfra" | "deepseek" | "empiriolabs" | "featherlessAi" | "fireworks" | "friendli" | "gmicloud" | "gradientai" | "groq" | "hetznerInference" | "huggingFace" | "inceptionLabs" | "inceptron" | "inco" | "inferenceNet" | "ioIntelligence" | "koscompute" | "lemonfoxAi" | "lilac" | "llmtech" | "mancer" | "metaModelApi" | "minimax" | "mistral" | "moark" | "modelscope" | "moonshotAi" | "morph" | "naverClovaStudio" | "nebius" | "neuralwatt" | "novita" | "ovhcloud" | "parasail" | "paretoInference" | "perplexity" | "poolside" | "primeIntellect" | "reka" | "sakanaAi" | "sambanova" | "sarvam" | "scaleway" | "siliconflow" | "stackit" | "stepfun" | "subconscious" | "synthetic" | "telnyx" | "thinkingMachines" | "tinfoil" | "together" | "umansAi" | "upstage" | "veniceAi" | "vispark" | "vultrInference" | "wafer" | "wandbInference" | "xai" | "zAi";
1
+ export type CatalogProviderName = "a2agent" | "abliteration" | "above-dev" | "ai21" | "aiand" | "aihubmix" | "aionlabs" | "ambient" | "api-route" | "arcee" | "atlas-cloud" | "avian-io" | "baidu-qianfan" | "baseten" | "bee-heossi" | "byteplus-modelark" | "bytez" | "cerebras" | "charm-hyper" | "chutes" | "cloudflare" | "crusoe" | "dashscope" | "deepinfra" | "deepseek" | "empiriolabs" | "featherless-ai" | "fireworks" | "friendli" | "gmicloud" | "gradientai" | "groq" | "hetzner-inference" | "huggingface" | "inception-labs" | "inceptron" | "inco" | "inference-net" | "io-intelligence" | "koscompute" | "laya" | "lemonfox-ai" | "lilac" | "llmtech" | "mancer" | "meta-model-api" | "minimax" | "mistral" | "moark" | "modelscope" | "moonshot-ai" | "morph" | "naver-clova-studio" | "nebius" | "neuralwatt" | "novita" | "ovhcloud" | "parasail" | "pareto-inference" | "perplexity" | "poolside" | "prime-intellect" | "reka" | "sakana-ai" | "sambanova" | "sarvam" | "scaleway" | "siliconflow" | "stackit" | "stepfun" | "subconscious" | "synthetic" | "telnyx" | "thinking-machines" | "tinfoil" | "together-ai" | "umans-ai" | "upstage" | "venice-ai" | "vispark" | "vultr-inference" | "wafer" | "wandb-inference" | "xai" | "z-ai";
2
+ export type CatalogCredentialKey = "a2agent" | "abliteration" | "aboveDev" | "ai21" | "aiand" | "aihubmix" | "aionlabs" | "ambient" | "apiRoute" | "arcee" | "atlasCloud" | "avianIo" | "baiduQianfan" | "baseten" | "beeHeossi" | "byteplusModelark" | "bytez" | "cerebras" | "charmHyper" | "chutes" | "cloudflare" | "crusoe" | "dashscope" | "deepinfra" | "deepseek" | "empiriolabs" | "featherlessAi" | "fireworks" | "friendli" | "gmicloud" | "gradientai" | "groq" | "hetznerInference" | "huggingFace" | "inceptionLabs" | "inceptron" | "inco" | "inferenceNet" | "ioIntelligence" | "koscompute" | "laya" | "lemonfoxAi" | "lilac" | "llmtech" | "mancer" | "metaModelApi" | "minimax" | "mistral" | "moark" | "modelscope" | "moonshotAi" | "morph" | "naverClovaStudio" | "nebius" | "neuralwatt" | "novita" | "ovhcloud" | "parasail" | "paretoInference" | "perplexity" | "poolside" | "primeIntellect" | "reka" | "sakanaAi" | "sambanova" | "sarvam" | "scaleway" | "siliconflow" | "stackit" | "stepfun" | "subconscious" | "synthetic" | "telnyx" | "thinkingMachines" | "tinfoil" | "together" | "umansAi" | "upstage" | "veniceAi" | "vispark" | "vultrInference" | "wafer" | "wandbInference" | "xai" | "zAi";
@@ -353,6 +353,10 @@ export type NeurolinkCredentials = {
353
353
  apiKey?: string;
354
354
  baseURL?: string;
355
355
  };
356
+ laya?: {
357
+ apiKey?: string;
358
+ baseURL?: string;
359
+ };
356
360
  lemonfoxAi?: {
357
361
  apiKey?: string;
358
362
  baseURL?: string;
@@ -585,15 +589,6 @@ export type NeurolinkCredentials = {
585
589
  */
586
590
  gatewayURL?: string;
587
591
  };
588
- /**
589
- * Laya (Convai, open weights) — the `decide` inference type. There is no
590
- * built-in endpoint: `baseURL` (or LAYA_BASE_URL) is required, pointing at a
591
- * Laya server or a proxy route to one; requests go to `<baseURL>/predict`.
592
- */
593
- laya?: {
594
- apiKey?: string;
595
- baseURL?: string;
596
- };
597
592
  /**
598
593
  * XOR (Juspay, open weights) — the `decide` inference type. There is no
599
594
  * built-in endpoint: `baseURL` (or XOR_BASE_URL) is required, pointing at the
@@ -2447,6 +2442,14 @@ export type ProviderDescriptor = {
2447
2442
  * checks to its own server (TypeSafe). See {@link DecisionLimits}.
2448
2443
  */
2449
2444
  decisionLimits?: DecisionLimits;
2445
+ /**
2446
+ * Precedence among `decide` providers: lower runs first and no two decide
2447
+ * descriptors share a value. resolveDefaultDecisionProvider() returns the
2448
+ * first configured provider in this order, so changing a number changes which
2449
+ * model routes for every host that configures several. Required on every
2450
+ * descriptor that serves `decide`; checked when the descriptors load.
2451
+ */
2452
+ decideOrder?: number;
2450
2453
  /**
2451
2454
  * Ascending priority (1 = tried first) in the auto-select fallback chain
2452
2455
  * used by getBestProvider(). Undefined = not part of the auto-select chain.
@@ -361,7 +361,17 @@ export declare class ErrorFactory {
361
361
  static csvProcessingFailed(filename: string, originalError: Error): NeuroLinkError;
362
362
  }
363
363
  /**
364
- * Timeout wrapper for async operations
364
+ * Timeout wrapper for async operations.
365
+ *
366
+ * Races an already-created promise against a deadline. It cannot cancel that
367
+ * promise: when the deadline wins the operation keeps running as an abandoned
368
+ * ghost. A caller that owns the abortable operation (an HTTP request, a
369
+ * provider call) must hand it an `AbortSignal` itself and abort it when this
370
+ * rejects.
371
+ *
372
+ * `timeoutMs` is held at `MAX_TIMER_MS`: a longer deadline would otherwise be
373
+ * handed to `setTimeout`, which fires after 1 ms and so rejected an operation
374
+ * that had asked for the longest wait available.
365
375
  */
366
376
  export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, timeoutError?: Error): Promise<T>;
367
377
  /**
@@ -3,6 +3,7 @@
3
3
  * Provides structured error management for tool execution and system operations
4
4
  */
5
5
  import { ErrorCategory, ErrorSeverity } from "../constants/enums.js";
6
+ import { MAX_TIMER_MS } from "../constants/timeouts.js";
6
7
  import { logger } from "./logger.js";
7
8
  import { CircuitBreakerOpenError } from "../types/index.js";
8
9
  import { HITLTimeoutError } from "../hitl/hitlErrors.js";
@@ -1081,14 +1082,28 @@ export class ErrorFactory {
1081
1082
  }
1082
1083
  }
1083
1084
  /**
1084
- * Timeout wrapper for async operations
1085
+ * Timeout wrapper for async operations.
1086
+ *
1087
+ * Races an already-created promise against a deadline. It cannot cancel that
1088
+ * promise: when the deadline wins the operation keeps running as an abandoned
1089
+ * ghost. A caller that owns the abortable operation (an HTTP request, a
1090
+ * provider call) must hand it an `AbortSignal` itself and abort it when this
1091
+ * rejects.
1092
+ *
1093
+ * `timeoutMs` is held at `MAX_TIMER_MS`: a longer deadline would otherwise be
1094
+ * handed to `setTimeout`, which fires after 1 ms and so rejected an operation
1095
+ * that had asked for the longest wait available.
1085
1096
  */
1086
1097
  export async function withTimeout(promise, timeoutMs, timeoutError) {
1087
1098
  let timer;
1099
+ // An explicit upper-bound comparison, and `>` rather than `<=`: NaN fails
1100
+ // `>`, so NaN, zero and negative deadlines reach `setTimeout` as they always
1101
+ // did, and only a value above the limit is replaced by it.
1102
+ const delayMs = timeoutMs > MAX_TIMER_MS ? MAX_TIMER_MS : timeoutMs;
1088
1103
  const timeoutPromise = new Promise((_, reject) => {
1089
1104
  timer = setTimeout(() => {
1090
1105
  reject(timeoutError || new Error(`Operation timed out after ${timeoutMs}ms`));
1091
- }, timeoutMs);
1106
+ }, delayMs);
1092
1107
  });
1093
1108
  try {
1094
1109
  return await Promise.race([promise, timeoutPromise]);
@@ -2,16 +2,19 @@
2
2
  * Centralized model choices for CLI commands
3
3
  * Derives choices from model enums to ensure consistency
4
4
  */
5
- import { AIProviderName, OpenAIModels, AnthropicModels, GoogleAIModels, BedrockModels, VertexModels, OllamaModels, AzureOpenAIModels, LiteLLMModels, SageMakerModels, OpenRouterModels, NvidiaNimModels, CohereModels, VoyageModels, JinaModels, StabilityModels, IdeogramModels, RecraftModels, TypeSafeModels, LayaModels, XorModels, PerplexityDeciderModels, CloudflareClefModels, ReplicateModels, } from "../constants/enums.js";
6
- import { getCatalogJsonEntries } from "../providers/catalog/loader.js";
5
+ import { AIProviderName, OpenAIModels, AnthropicModels, GoogleAIModels, BedrockModels, VertexModels, OllamaModels, AzureOpenAIModels, LiteLLMModels, SageMakerModels, OpenRouterModels, NvidiaNimModels, CohereModels, VoyageModels, JinaModels, StabilityModels, IdeogramModels, RecraftModels, TypeSafeModels, XorModels, PerplexityDeciderModels, CloudflareClefModels, ReplicateModels, } from "../constants/enums.js";
6
+ import { getCatalogJsonEntries, getDecideCatalogEntries, } from "../providers/catalog/loader.js";
7
7
  /**
8
8
  * Looks up the JSON catalog entry for a provider, if it is one of the 11
9
9
  * JSON-catalog providers. Used to short-circuit the hand tables below so a
10
10
  * catalog provider's models are always sourced from its catalog JSON, never
11
- * from stale hand-written duplicates.
11
+ * from stale hand-written duplicates. A decide provider's entry is searched
12
+ * after the chat catalog (the two never share an id), so its models need no
13
+ * hand-written rows either.
12
14
  */
13
15
  function catalogEntryFor(provider) {
14
- return getCatalogJsonEntries().find((entry) => entry.id === provider);
16
+ return (getCatalogJsonEntries().find((entry) => entry.id === provider) ??
17
+ getDecideCatalogEntries().find((entry) => entry.id === provider));
15
18
  }
16
19
  /**
17
20
  * Derives CLI-prompt entries for a catalog provider. Uses the curated
@@ -374,24 +377,6 @@ const TOP_MODELS_CONFIG = {
374
377
  description: "Preview build of Jev",
375
378
  },
376
379
  ],
377
- [AIProviderName.LAYA]: [
378
- {
379
- model: LayaModels.TYPED_DECISIONS,
380
- description: "Recommended - Laya fine-tuned for typed decisions",
381
- },
382
- {
383
- model: LayaModels.MULTILINGUAL,
384
- description: "Laya, 100+ languages",
385
- },
386
- {
387
- model: LayaModels.ENGLISH,
388
- description: "Laya, English base checkpoint (512-token window)",
389
- },
390
- {
391
- model: LayaModels.AUTO,
392
- description: "Let Laya pick a checkpoint by language",
393
- },
394
- ],
395
380
  [AIProviderName.XOR]: [
396
381
  {
397
382
  model: XorModels.XOR_1_1,
@@ -460,7 +445,6 @@ export const DEFAULT_MODELS = {
460
445
  [AIProviderName.IDEOGRAM]: IdeogramModels.IDEOGRAM_V3,
461
446
  [AIProviderName.RECRAFT]: RecraftModels.RECRAFT_V3,
462
447
  [AIProviderName.TYPESAFE]: TypeSafeModels.JEV_LATEST,
463
- [AIProviderName.LAYA]: LayaModels.TYPED_DECISIONS,
464
448
  [AIProviderName.XOR]: XorModels.XOR_1_1,
465
449
  [AIProviderName.PERPLEXITY_DECIDER]: PerplexityDeciderModels.PPLX_DECIDER_V1_27B,
466
450
  [AIProviderName.CLOUDFLARE_CLEF]: CloudflareClefModels.CLEF,
@@ -496,7 +480,6 @@ const MODEL_ENUMS = {
496
480
  [AIProviderName.IDEOGRAM]: IdeogramModels,
497
481
  [AIProviderName.RECRAFT]: RecraftModels,
498
482
  [AIProviderName.TYPESAFE]: TypeSafeModels,
499
- [AIProviderName.LAYA]: LayaModels,
500
483
  [AIProviderName.XOR]: XorModels,
501
484
  [AIProviderName.PERPLEXITY_DECIDER]: PerplexityDeciderModels,
502
485
  [AIProviderName.CLOUDFLARE_CLEF]: CloudflareClefModels,
@@ -695,7 +678,10 @@ export function getPopularModelsAcrossProviders() {
695
678
  }
696
679
  // JSON-catalog providers are excluded from TOP_MODELS_CONFIG above; merge
697
680
  // their models in here, derived the same way getTopModelChoices() does.
698
- for (const entry of getCatalogJsonEntries()) {
681
+ for (const entry of [
682
+ ...getCatalogJsonEntries(),
683
+ ...getDecideCatalogEntries(),
684
+ ]) {
699
685
  pushTop2(entry.id, catalogTopModels(entry));
700
686
  }
701
687
  return popularModels;
@@ -400,11 +400,6 @@ export declare function resolveOpenAICompatConfig(entry: OpenAICompatConfigInput
400
400
  * TypeSafe (Jev) — the `decide` inference type. Not a text provider.
401
401
  */
402
402
  export declare function createTypeSafeConfig(): ProviderConfigOptions;
403
- /**
404
- * Laya — the `decide` inference type. Not a text provider, and it has no
405
- * built-in endpoint: LAYA_BASE_URL is required alongside the key.
406
- */
407
- export declare function createLayaConfig(): ProviderConfigOptions;
408
403
  /**
409
404
  * XOR — the `decide` inference type. Not a text provider, and it has no
410
405
  * built-in endpoint: XOR_BASE_URL is required alongside the key.
@@ -1324,23 +1324,6 @@ export function createTypeSafeConfig() {
1324
1324
  ],
1325
1325
  };
1326
1326
  }
1327
- /**
1328
- * Laya — the `decide` inference type. Not a text provider, and it has no
1329
- * built-in endpoint: LAYA_BASE_URL is required alongside the key.
1330
- */
1331
- export function createLayaConfig() {
1332
- return {
1333
- providerName: "Laya",
1334
- envVarName: "LAYA_API_KEY",
1335
- setupUrl: "https://github.com/NandhaKishorM/laya",
1336
- description: "API key for your Laya endpoint",
1337
- instructions: [
1338
- "1. Run a Laya server, or use a LiteLLM proxy with a pass-through route to one",
1339
- "2. Set LAYA_BASE_URL to its base URL (NeuroLink calls <LAYA_BASE_URL>/predict)",
1340
- "3. Set LAYA_API_KEY to the key it accepts (on LiteLLM, a virtual key with that route in its Allowed Routes)",
1341
- ],
1342
- };
1343
- }
1344
1327
  /**
1345
1328
  * XOR — the `decide` inference type. Not a text provider, and it has no
1346
1329
  * built-in endpoint: XOR_BASE_URL is required alongside the key.
@@ -3,6 +3,7 @@
3
3
  * Parallel execution engine for ensemble workflows
4
4
  */
5
5
  import pLimit from "p-limit";
6
+ import { MAX_TIMER_MS } from "../../constants/timeouts.js";
6
7
  import { AIProviderFactory } from "../../core/factory.js";
7
8
  import { logger } from "../../utils/logger.js";
8
9
  import { SpanSerializer, SpanType, SpanStatus, getMetricsAggregator, } from "../../observability/index.js";
@@ -130,12 +131,13 @@ async function executeModel(options, workflowDefaultSystemPrompt) {
130
131
  const MAX_ATTEMPTS = 2;
131
132
  while (true) {
132
133
  attempts++;
133
- result = await executeWithTimeout(async () => {
134
+ result = await executeWithTimeout(async (abortSignal) => {
134
135
  return await provider.generate({
135
136
  prompt,
136
137
  systemPrompt: resolvedSystemPrompt,
137
138
  temperature: model.temperature,
138
139
  maxTokens: model.maxTokens,
140
+ abortSignal,
139
141
  });
140
142
  }, timeout, `Model ${model.provider}/${model.model} timed out after ${timeout}ms`);
141
143
  const contentLen = result?.content?.length ?? 0;
@@ -199,14 +201,29 @@ async function createProvider(model) {
199
201
  return await AIProviderFactory.createProvider(model.provider, model.model);
200
202
  }
201
203
  /**
202
- * Execute function with timeout
203
- * @param fn - Async function to execute
204
+ * Run an abortable operation under a deadline.
205
+ *
206
+ * The operation receives the signal of an `AbortController` that this function
207
+ * aborts the moment the deadline lapses, so the owner of the work (the provider
208
+ * call and its HTTP request) is cancelled rather than abandoned. `Promise.race`
209
+ * alone only stops this caller waiting; it cannot stop a promise that already
210
+ * exists, so cancelling is the operation's job once it is handed the signal.
211
+ *
212
+ * `timeoutMs` is held at `MAX_TIMER_MS`: `setTimeout` fires after 1 ms for a
213
+ * longer delay, which turned the longest deadline a workflow could ask for into
214
+ * an immediate failure.
215
+ * @param run - Operation to execute; observes the signal to stop early
204
216
  * @param timeoutMs - Timeout in milliseconds
205
217
  * @param timeoutMessage - Error message for timeout
206
- * @returns Result of function execution
218
+ * @returns Result of the operation
207
219
  */
208
- async function executeWithTimeout(fn, timeoutMs, timeoutMessage) {
220
+ async function executeWithTimeout(run, timeoutMs, timeoutMessage) {
221
+ const controller = new AbortController();
209
222
  let timeoutHandle;
223
+ // An explicit upper-bound comparison, and `>` rather than `<=`: NaN fails
224
+ // `>`, so NaN, zero and negative deadlines reach `setTimeout` as they always
225
+ // did, and only a value above the limit is replaced by it.
226
+ const delayMs = timeoutMs > MAX_TIMER_MS ? MAX_TIMER_MS : timeoutMs;
210
227
  const timeoutPromise = new Promise((_, reject) => {
211
228
  timeoutHandle = setTimeout(() => {
212
229
  const error = new WorkflowError(timeoutMessage, {
@@ -215,21 +232,17 @@ async function executeWithTimeout(fn, timeoutMs, timeoutMessage) {
215
232
  phase: "ensemble",
216
233
  retryable: true,
217
234
  });
235
+ // Reject before aborting: the abort can make the operation's own
236
+ // promise fail, and the race must already hold the deadline error.
218
237
  reject(error);
219
- }, timeoutMs);
238
+ controller.abort(error);
239
+ }, delayMs);
220
240
  });
221
241
  try {
222
- const result = await Promise.race([fn(), timeoutPromise]);
223
- if (timeoutHandle) {
224
- clearTimeout(timeoutHandle);
225
- }
226
- return result;
242
+ return await Promise.race([run(controller.signal), timeoutPromise]);
227
243
  }
228
- catch (error) {
229
- if (timeoutHandle) {
230
- clearTimeout(timeoutHandle);
231
- }
232
- throw error;
244
+ finally {
245
+ clearTimeout(timeoutHandle);
233
246
  }
234
247
  }
235
248
  /**
@@ -9745,18 +9745,18 @@
9745
9745
  {"objectID":"aaa9ef50d62dd989356ea2bb28e77e07f8bb8cbe1fcff8fb4c84600bee0c8474","title":"Architecture Decision Records — Provider Onboarding Redesign","url":"/docs/provider-integration/adr/README#architecture-decision-records-provider-onboarding-redesign","content":"Short, dated records of the load-bearing decisions behind the provider\nonboarding redesign (Plans 04–10, August 2026). Read these before arguing to\nchange the shape of ProviderDescriptor, the catalog, or the CI gate — the\ntradeoffs were already litigated once.\n\n| ADR | Decision | Status |\n| ---- | ------------------------------------------------------------------------------ | -------------------- |\n| 0001 | ProviderDescriptor is the single source of truth for provider identity | Accepted |\n| 0002 | OpenAI-wire-compatible providers default to a data-catalog row, not a subclass | Accepted and shipped |\n| 0003 | Mocked-fetch contract tests are the CI gate; live-API suites are not | Accepted and shipped |","hierarchy":{"lvl0":"Provider Integration","lvl1":"Architecture Decision Records — Provider Onboarding Redesign","lvl2":"Architecture Decision Records — Provider Onboarding Redesign","lvl3":""}},
9746
9746
  {"objectID":"1be076b3fd50fdb68b087798ec78c74d20575d4a39e3c54f8ee8e1cde744fa6a","title":"Catalog authoring and admission","url":"/docs/provider-integration/catalog-admission","content":"Catalog authoring and admission\n\nCatalog source completeness, PR provenance, installed-package behavior and\ncurrent vendor availability are separate results. The default\npnpm run verify:provider-onboarding remains an offline source check; it does\nnot certify the PR URL or refresh a dated roster/live record.\n\nCatalog model defaults for context and output must be positive safe\nintegers supported by source evidence. Unknown ceilings remain unresolved.\nThis authoring constraint does not alter caller maxTokens: 0 semantics or\noptional per-model overrides. A scaffold intentionally contains invalid zero\nceilings and TODO evidence until the author supplies real values.\n\nInspect an unfinished scaffold:\n\nThis reports incomplete_draft and its validation problems. Successful draft\ninspection is not admission; every admission result has zero provider-count,\npackage, publication, roster and live credit.\n\nAfter filling the source fields, use the exact PENDING_PR marker while no PR\nexists. A guessed URL or arbitrary string is not provenance:\n\nWithout a provider/file selector, draft/source stages inspect the whole\ncatalog. They stay offline. A canonical PR URL is only syntax at this stage;\nthe output explicitly leaves provenance unverified. Existing dated evidence\nis retained without relabeling or requiring a breaking record migration.\nPaired (--catalog-stage source) and equals (--catalog-stage=source) options\ncan be mixed; the same forms apply to provider, file and source-head selectors.\n\nWhen a real introduction PR exists, put its actual URL in addedInPR,\nregenerate and commit the source, then inspect the immutable PR head:\n\nThe explicitly selected provenance stage uses authenticated, read-only gh api\nqueries. It requires the actual repository/PR identity, an added-file association\nfor this provider, the exact local source/PR head and equal provider-file blob.\nUncommitted source, a stale/unrelated head, guessed/nonexistent PR or unrelated\nexisting PR fails. GitHub API access failures fail closed. Association proof\ndoes not certify reviewer approval or current candidate CI.\n\nAfter merge:\n\nThis reports historical_catalog_file_introduction_verified: actual added-file\nassociation, merged state, merge ancestry in the selected local head and all\nfive required checks completed SUCCESS at the introduction PR head. It does\nnot certify current product identity or content continuity after later edits,\ndeletion or replacement. Historical file introduction remains provable even if\na later product reuses that path; the current source bytes are not silently\nequated with the old introduced blob. Current integration/product acceptance\nneeds its separate evidence. The newest run of each required check must pass;\nmissing, skipped, neutral or pending runs fail.\n\nroster, live, package and release are deliberately not admission-command\nstages. Docs-only records cannot become authenticated roster or live receipts.\nThose claims require their separately authorized current account/model/route\nexecution or installed-consumer evidence. This command makes no vendor calls\nand never changes an account, PR, branch, catalog record or remote thread.","hierarchy":{"lvl0":"Provider Integration","lvl1":"Catalog authoring and admission","lvl2":"","lvl3":""}},
9747
9747
  {"objectID":"9af9cab8b3cff18ad3bf19995bd3b8e9e1264fa6f64d7659b6f7222e18d7ea34","title":"Catalog authoring and admission","url":"/docs/provider-integration/catalog-admission#catalog-authoring-and-admission","content":"Catalog source completeness, PR provenance, installed-package behavior and\ncurrent vendor availability are separate results. The default\npnpm run verify:provider-onboarding remains an offline source check; it does\nnot certify the PR URL or refresh a dated roster/live record.\n\nCatalog model defaults for context and output must be positive safe\nintegers supported by source evidence. Unknown ceilings remain unresolved.\nThis authoring constraint does not alter caller maxTokens: 0 semantics or\noptional per-model overrides. A scaffold intentionally contains invalid zero\nceilings and TODO evidence until the author supplies real values.\n\nInspect an unfinished scaffold:\n\nThis reports incomplete_draft and its validation problems. Successful draft\ninspection is not admission; every admission result has zero provider-count,\npackage, publication, roster and live credit.\n\nAfter filling the source fields, use the exact PENDING_PR marker while no PR\nexists. A guessed URL or arbitrary string is not provenance:\n\nWithout a provider/file selector, draft/source stages inspect the whole\ncatalog. They stay offline. A canonical PR URL is only syntax at this stage;\nthe output explicitly leaves provenance unverified. Existing dated evidence\nis retained without relabeling or requiring a breaking record migration.\nPaired (--catalog-stage source) and equals (--catalog-stage=source) options\ncan be mixed; the same forms apply to provider, file and source-head selectors.\n\nWhen a real introduction PR exists, put its actual URL in addedInPR,\nregenerate and commit the source, then inspect the immutable PR head:\n\nThe explicitly selected provenance stage uses authenticated, read-only gh api\nqueries. It requires the actual repository/PR identity, an added-file association\nfor this provider, the exact local source/PR head and equal provider-file blob.\nUncommitted source, a stale/unrelated head, guessed/nonexistent PR or unrelated\nexisting PR fails. GitHub API access failures fail closed.","hierarchy":{"lvl0":"Provider Integration","lvl1":"Catalog authoring and admission","lvl2":"Catalog authoring and admission","lvl3":""}},
9748
- {"objectID":"91f87bcd98e1883a00b720044811501a2f90818bf3c3eff22c5d71b2a5e7d80b","title":"Provider Manifests","url":"/docs/provider-integration/manifests/README","content":"Provider Manifests\n\nThis convention originally applied to every provider onboarded via Tier 2,\n3, or 4: one JSON file here, named <provider>.json where <provider> is\nthe exact AIProviderName enum value (e.g. cerebras.json for\nAIProviderName.CEREBRAS = \"cerebras\").\n\nTier 2 (JSON catalog) providers no longer use a manifest here\n\nAs of the provider-JSON-catalog refactor, Tier 2 providers are declared\nentirely in src/lib/providers/catalog/<id>.json, validated by the zod\nschema in src/lib/providers/catalog/schema.ts. That file's evidence\nobject — rosterVerified, addedInPR, and optionally authProbe,\nbillingProbe, liveMatrix — carries the same onboarding evidence a\nmanifest used to hold, so a separate manifest file would just duplicate\nit. tools/verify-provider-onboarding.ts reflects this: for any provider\nwith a matching src/lib/providers/catalog/<id>.json file, the gate\nchecks that the JSON file exists, parses via the real zod schema, and\n(via that same successful parse, since both fields are non-optional in\nthe schema) carries evidence.rosterVerified and evidence.addedInPR.\n\ncerebras.json and sambanova.json — the two manifests that used to\nlive in this directory — were removed for this reason: both providers\nare now JSON-catalog entries, and their onboarding evidence lives in\nsrc/lib/providers/catalog/cerebras.json and\nsrc/lib/providers/catalog/sambanova.json respectively.\n\nTier 3/4 (hand-written) providers still use a manifest here\n\nA provider onboarded outside the JSON catalog — a custom adapter (Tier 3)\nor fully custom integration (Tier 4) — has no catalog JSON file, so\ntools/verify-provider-onboarding.ts falls back to its original\nfour-check flow for it, including a manifest at\ndocs/provider-integration/manifests/<name>.json. The shape below still\napplies to those providers.\n\nThe block is annotated JSONC for documentation purposes only — the\n// comments and trailing comma explain each field but are not valid\nJSON. A real <provider>.json manifest file must be strict JSON: no\ncomments, no trailing commas.\n\nHow it's checked\n\npnpm run verify:provider-onboarding (tools/verify-provider-onboarding.ts)\nfails a PR that introduces a new AIProviderName member without matching\nonboarding evidence: a valid catalog JSON entry for Tier 2 providers (see\nabove), or a structurally valid manifest here for Tier 3/4 providers. A\nmanifest is structurally valid when it is a JSON object whose provider\nmatches the file name and which carries provider, tier, addedInPR,\naddedDate, filesTouched (an array of strings), mockedContractSection\nand manualTestStatus, plus tier4Justification when tier is 4. The gate\ndoes not retroactively require either for providers that predate the gate\n— see that tool's LEGACY_PROVIDERS list.","hierarchy":{"lvl0":"Provider Integration","lvl1":"Provider Manifests","lvl2":"","lvl3":""}},
9748
+ {"objectID":"91f87bcd98e1883a00b720044811501a2f90818bf3c3eff22c5d71b2a5e7d80b","title":"Provider Manifests","url":"/docs/provider-integration/manifests/README","content":"Provider Manifests\n\nThis convention originally applied to every provider onboarded via Tier 2,\n3, or 4: one JSON file here, named <provider>.json where <provider> is\nthe exact AIProviderName enum value (e.g. cerebras.json for\nAIProviderName.CEREBRAS = \"cerebras\").\n\nTier 2 (JSON catalog) providers no longer use a manifest here\n\nAs of the provider-JSON-catalog refactor, Tier 2 providers are declared\nentirely in src/lib/providers/catalog/<id>.json, validated by the zod\nschema in src/lib/providers/catalog/schema.ts. That file's evidence\nobject — rosterVerified, addedInPR, and optionally authProbe,\nbillingProbe, liveMatrix — carries the same onboarding evidence a\nmanifest used to hold, so a separate manifest file would just duplicate\nit. tools/verify-provider-onboarding.ts reflects this: for any provider\nwith a matching src/lib/providers/catalog/<id>.json file, the gate\nchecks that the JSON file exists, parses via the real zod schema, and\n(via that same successful parse, since both fields are non-optional in\nthe schema) carries evidence.rosterVerified and evidence.addedInPR.\n\ncerebras.json and sambanova.json — the two manifests that used to\nlive in this directory — were removed for this reason: both providers\nare now JSON-catalog entries, and their onboarding evidence lives in\nsrc/lib/providers/catalog/cerebras.json and\nsrc/lib/providers/catalog/sambanova.json respectively.\n\nlaya.json was removed the same way when Laya moved from a hand-written class onto a decide catalog file: src/lib/providers/catalog/laya.json carries its evidence block, and the gate checks it with parseCatalogEntry, which accepts chat and decide files alike and also requires the file's id to match its name.\n\nTier 3/4 (hand-written) providers still use a manifest here\n\nA provider onboarded outside the JSON catalog — a custom adapter (Tier 3)\nor fully custom integration (Tier 4) — has no catalog JSON file, so\ntools/verify-provider-onboarding.ts falls back to its original\nfour-check flow for it, including a manifest at\ndocs/provider-integration/manifests/<name>.json. The shape below still\napplies to those providers.\n\nThe block is annotated JSONC for documentation purposes only — the\n// comments and trailing comma explain each field but are not valid\nJSON. A real <provider>.json manifest file must be strict JSON: no\ncomments, no trailing commas.\n\nHow it's checked\n\npnpm run verify:provider-onboarding (tools/verify-provider-onboarding.ts)\nfails a PR that introduces a new AIProviderName member without matching\nonboarding evidence: a valid catalog JSON entry for Tier 2 providers (see\nabove), or a structurally valid manifest here for Tier 3/4 providers. A\nmanifest is structurally valid when it is a JSON object whose provider\nmatches the file name and which carries provider, tier, addedInPR,\naddedDate, filesTouched (an array of strings), mockedContractSection\nand manualTestStatus, plus tier4Justification when tier is 4. The gate\ndoes not retroactively require either for providers that predate the gate\n— see that tool's LEGACY_PROVIDERS list.","hierarchy":{"lvl0":"Provider Integration","lvl1":"Provider Manifests","lvl2":"","lvl3":""}},
9749
9749
  {"objectID":"f50e43e805f49ece132b5ced9027edcbcb232799cd12a67be9b04ccceb764437","title":"Provider Manifests","url":"/docs/provider-integration/manifests/README#provider-manifests","content":"This convention originally applied to every provider onboarded via Tier 2,\n3, or 4: one JSON file here, named <provider>.json where <provider> is\nthe exact AIProviderName enum value (e.g. cerebras.json for\nAIProviderName.CEREBRAS = \"cerebras\").","hierarchy":{"lvl0":"Provider Integration","lvl1":"Provider Manifests","lvl2":"Provider Manifests","lvl3":""}},
9750
- {"objectID":"a68c16599df99491c1e91d0857d3b92f4bd78a3caa88b6279381ff1ca665926f","title":"Tier 2 (JSON catalog) providers no longer use a manifest here","url":"/docs/provider-integration/manifests/README#tier-2-json-catalog-providers-no-longer-use-a-manifest-here","content":"As of the provider-JSON-catalog refactor, Tier 2 providers are declared\nentirely in src/lib/providers/catalog/<id>.json, validated by the zod\nschema in src/lib/providers/catalog/schema.ts. That file's evidence\nobject — rosterVerified, addedInPR, and optionally authProbe,\nbillingProbe, liveMatrix — carries the same onboarding evidence a\nmanifest used to hold, so a separate manifest file would just duplicate\nit. tools/verify-provider-onboarding.ts reflects this: for any provider\nwith a matching src/lib/providers/catalog/<id>.json file, the gate\nchecks that the JSON file exists, parses via the real zod schema, and\n(via that same successful parse, since both fields are non-optional in\nthe schema) carries evidence.rosterVerified and evidence.addedInPR.\n\ncerebras.json and sambanova.json — the two manifests that used to\nlive in this directory — were removed for this reason: both providers\nare now JSON-catalog entries, and their onboarding evidence lives in\nsrc/lib/providers/catalog/cerebras.json and\nsrc/lib/providers/catalog/sambanova.json respectively.","hierarchy":{"lvl0":"Provider Integration","lvl1":"Provider Manifests","lvl2":"Tier 2 (JSON catalog) providers no longer use a manifest here","lvl3":""}},
9750
+ {"objectID":"a68c16599df99491c1e91d0857d3b92f4bd78a3caa88b6279381ff1ca665926f","title":"Tier 2 (JSON catalog) providers no longer use a manifest here","url":"/docs/provider-integration/manifests/README#tier-2-json-catalog-providers-no-longer-use-a-manifest-here","content":"As of the provider-JSON-catalog refactor, Tier 2 providers are declared\nentirely in src/lib/providers/catalog/<id>.json, validated by the zod\nschema in src/lib/providers/catalog/schema.ts. That file's evidence\nobject — rosterVerified, addedInPR, and optionally authProbe,\nbillingProbe, liveMatrix — carries the same onboarding evidence a\nmanifest used to hold, so a separate manifest file would just duplicate\nit. tools/verify-provider-onboarding.ts reflects this: for any provider\nwith a matching src/lib/providers/catalog/<id>.json file, the gate\nchecks that the JSON file exists, parses via the real zod schema, and\n(via that same successful parse, since both fields are non-optional in\nthe schema) carries evidence.rosterVerified and evidence.addedInPR.\n\ncerebras.json and sambanova.json — the two manifests that used to\nlive in this directory — were removed for this reason: both providers\nare now JSON-catalog entries, and their onboarding evidence lives in\nsrc/lib/providers/catalog/cerebras.json and\nsrc/lib/providers/catalog/sambanova.json respectively.\n\nlaya.json was removed the same way when Laya moved from a hand-written class onto a decide catalog file: src/lib/providers/catalog/laya.json carries its evidence block, and the gate checks it with parseCatalogEntry, which accepts chat and decide files alike and also requires the file's id to match its name.","hierarchy":{"lvl0":"Provider Integration","lvl1":"Provider Manifests","lvl2":"Tier 2 (JSON catalog) providers no longer use a manifest here","lvl3":""}},
9751
9751
  {"objectID":"00fb88ca42034f14d0a87eea1d06b38350fc2cb9b5bed8bb3f544f10b3cd99ab","title":"Tier 3/4 (hand-written) providers still use a manifest here","url":"/docs/provider-integration/manifests/README#tier-34-hand-written-providers-still-use-a-manifest-here","content":"A provider onboarded outside the JSON catalog — a custom adapter (Tier 3)\nor fully custom integration (Tier 4) — has no catalog JSON file, so\ntools/verify-provider-onboarding.ts falls back to its original\nfour-check flow for it, including a manifest at\ndocs/provider-integration/manifests/<name>.json. The shape below still\napplies to those providers.\n\nThe block is annotated JSONC for documentation purposes only — the\n// comments and trailing comma explain each field but are not valid\nJSON. A real <provider>.json manifest file must be strict JSON: no\ncomments, no trailing commas.","hierarchy":{"lvl0":"Provider Integration","lvl1":"Provider Manifests","lvl2":"Tier 3/4 (hand-written) providers still use a manifest here","lvl3":""}},
9752
9752
  {"objectID":"4384ec258a233739ea35584f79662d5111be0b8defd38f75a6ab8068bf3a7e30","title":"How it's checked","url":"/docs/provider-integration/manifests/README#how-its-checked","content":"pnpm run verify:provider-onboarding (tools/verify-provider-onboarding.ts)\nfails a PR that introduces a new AIProviderName member without matching\nonboarding evidence: a valid catalog JSON entry for Tier 2 providers (see\nabove), or a structurally valid manifest here for Tier 3/4 providers. A\nmanifest is structurally valid when it is a JSON object whose provider\nmatches the file name and which carries provider, tier, addedInPR,\naddedDate, filesTouched (an array of strings), mockedContractSection\nand manualTestStatus, plus tier4Justification when tier is 4. The gate\ndoes not retroactively require either for providers that predate the gate\n— see that tool's LEGACY_PROVIDERS list.","hierarchy":{"lvl0":"Provider Integration","lvl1":"Provider Manifests","lvl2":"How it's checked","lvl3":""}},
9753
- {"objectID":"c533e90a3190ff6ad955d7e0c25b0b6370956144cb2b59f1c07b4ffdb1b1e452","title":"Provider Descriptor Migration Ledger","url":"/docs/provider-integration/migration-ledger","content":"Provider Descriptor Migration Ledger\n\nInventory taken at origin/release @ 2cefa3ae4115f817f75a415b6bc70fc3ecaed2d3. TypeSafe was added afterwards (#1761) and is included below; the 25-entry count matched origin/release @ f536fd091.\n\nmistral, huggingface and deepseek have since migrated to the JSON catalog and been removed from HAND_DESCRIPTORS (#1781) — see Migrated below — and laya, xor, perplexity-decider and cloudflare-clef (decision-only, like TypeSafe) were added as hand descriptors afterwards. Net effect: 25 minus the 3 migrated plus 4 (laya, xor, perplexity-decider, cloudflare-clef) leaves 26 entries currently in HAND_DESCRIPTORS (src/lib/factories/providerDescriptors.ts). The category counts below (Shared adapter needed / Must remain class / Must remain core class) cover exactly those 26; the 3 migrated providers are recorded separately as done and no longer count toward \"what remains.\"\n\nThis ledger records, per provider, why it is (or isn't) a JSON-catalog migration candidate, so \"add a provider\" work doesn't re-litigate the same analysis per PR.\n\nEvery verdict allows one thing regardless of class: the static descriptor metadata (name, aliases, default model, credential env var names, setup URL) can always move into a class-backed JSON record. \"Must remain class\" means the execution — the inference loop (generate/stream/decide, per the provider's inferenceKinds), auth, media pipelines — cannot be reduced to declarative catalog data; it does not mean the provider is exempt from descriptor consolidation.\n\nMigrated (3)\n\nAll three ran the same class-removal path this ledger recommended below: resolve the JSON/descriptor data conflicts, then delete the hand descriptor and hand-written subclass so the provider is fully catalog-derived.\n\n| Provider | Note |\n| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| mistral | Moved onto catalog/mistral.json. defaultModel now derives from the new optional models.registryDefaultModel field when present (only Mistral sets it), and the setupUrl/key-format/timeout/priority conflicts this ledger flagged were resolved in the JSON rather than carried forward. |\n| huggingface | Moved onto catalog/huggingface.json, which gained wire.apiKeyFallbackEnvVars (HF_TOKEN alongside HUGGINGFACE_API_KEY) and capabilities.tools: \"model-dependent\" — the two schema gaps this ledger flagged below before the extension landed. |\n| deepseek | Moved onto catalog/deepseek.json, using the new quirks.responseFormatDowngrade: \"json-schema-to-json-object\" field (DeepSeek 400s on json_schema) instead of an executable hook — the named-quirk approach this ledger recommended. It also sets quirks.replayReasoningContent: true. |\n\nShared adapter needed (9)\n\nThree adapter families, not nine one-off migrations:\n\nLocal-runtime adapter (ollama, lm-studio, llamacpp) — one OpenAI-compatible local-runtime adapter with optional auth, model discovery/probe, actionable transport errors, timeout policy, optional embeddings. Vendor-specific pull/diagnostic guidance becomes adapter data.\n\nEmbedding-only adapter (voyage, jina, cohere) — one embedding-only provider adapter parameterized by path, request shape, batch size, response-extraction/index policy, unsupported-surface messages. jina extends it with an optional rerank operation profile (don't force reranking into the boolean capability shape). cohere composes this with a generic OpenAI chat adapter (it has both /compatibility/v1 chat and native /v2/embed); preserve its batch limit and body-field quirks in the embedding profile.\n\nImage-generation adapter (stability, ideogram, recraft) — one image-generation adapter family: explicit multipart vs JSON request profile, base64-vs-URL response profile, custom auth-header support, model-path mapping as data. SSRF and bounded-read policy remain shared mandatory behavior, not per-vendor opt-outs.\n\nMust remain class (16)\n\n| Provider | Why |\n| -------------------- | -----------------------------------------------","hierarchy":{"lvl0":"Provider Integration","lvl1":"Provider Descriptor Migration Ledger","lvl2":"","lvl3":""}},
9754
- {"objectID":"b5d36740e9c9cb41c6ccb01db0a9fb1f9c0ac8ac59f1084b9a92332dd79c3e4d","title":"Provider Descriptor Migration Ledger","url":"/docs/provider-integration/migration-ledger#provider-descriptor-migration-ledger","content":"Inventory taken at origin/release @ 2cefa3ae4115f817f75a415b6bc70fc3ecaed2d3. TypeSafe was added afterwards (#1761) and is included below; the 25-entry count matched origin/release @ f536fd091.\n\nmistral, huggingface and deepseek have since migrated to the JSON catalog and been removed from HAND_DESCRIPTORS (#1781) — see Migrated below — and laya, xor, perplexity-decider and cloudflare-clef (decision-only, like TypeSafe) were added as hand descriptors afterwards. Net effect: 25 minus the 3 migrated plus 4 (laya, xor, perplexity-decider, cloudflare-clef) leaves 26 entries currently in HAND_DESCRIPTORS (src/lib/factories/providerDescriptors.ts). The category counts below (Shared adapter needed / Must remain class / Must remain core class) cover exactly those 26; the 3 migrated providers are recorded separately as done and no longer count toward \"what remains.\"\n\nThis ledger records, per provider, why it is (or isn't) a JSON-catalog migration candidate, so \"add a provider\" work doesn't re-litigate the same analysis per PR.\n\nEvery verdict allows one thing regardless of class: the static descriptor metadata (name, aliases, default model, credential env var names, setup URL) can always move into a class-backed JSON record. \"Must remain class\" means the execution — the inference loop (generate/stream/decide, per the provider's inferenceKinds), auth, media pipelines — cannot be reduced to declarative catalog data; it does not mean the provider is exempt from descriptor consolidation.","hierarchy":{"lvl0":"Provider Integration","lvl1":"Provider Descriptor Migration Ledger","lvl2":"Provider Descriptor Migration Ledger","lvl3":""}},
9755
- {"objectID":"fb3b171594dd4553aaf04fd301572eb1db1e459c15541867bd93a67ece8cfadb","title":"Migrated (3)","url":"/docs/provider-integration/migration-ledger#migrated-3","content":"All three ran the same class-removal path this ledger recommended below: resolve the JSON/descriptor data conflicts, then delete the hand descriptor and hand-written subclass so the provider is fully catalog-derived.\n\n| Provider | Note |\n| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| mistral | Moved onto catalog/mistral.json. defaultModel now derives from the new optional models.registryDefaultModel field when present (only Mistral sets it), and the setupUrl/key-format/timeout/priority conflicts this ledger flagged were resolved in the JSON rather than carried forward. |\n| huggingface | Moved onto catalog/huggingface.json, which gained wire.apiKeyFallbackEnvVars (HF_TOKEN alongside HUGGINGFACE_API_KEY) and capabilities.tools: \"model-dependent\" — the two schema gaps this ledger flagged below before the extension landed. |\n| deepseek | Moved onto catalog/deepseek.json, using the new quirks.responseFormatDowngrade: \"json-schema-to-json-object\" field (DeepSeek 400s on json_schema) instead of an executable hook — the named-quirk approach this ledger recommended. It also sets quirks.replayReasoningContent: true. |","hierarchy":{"lvl0":"Provider Integration","lvl1":"Provider Descriptor Migration Ledger","lvl2":"Migrated (3)","lvl3":""}},
9753
+ {"objectID":"c533e90a3190ff6ad955d7e0c25b0b6370956144cb2b59f1c07b4ffdb1b1e452","title":"Provider Descriptor Migration Ledger","url":"/docs/provider-integration/migration-ledger","content":"Provider Descriptor Migration Ledger\n\nInventory taken at origin/release @ 2cefa3ae4115f817f75a415b6bc70fc3ecaed2d3. TypeSafe was added afterwards (#1761) and is included below; the 25-entry count matched origin/release @ f536fd091.\n\nmistral, huggingface, deepseek and laya have since migrated to the JSON catalog and been removed from HAND_DESCRIPTORS (#1781; laya in the PR that added src/lib/providers/catalog/laya.json) — see Migrated below — and xor, perplexity-decider and cloudflare-clef (decision-only, like TypeSafe) were added as hand descriptors afterwards. Net effect: 25 minus the 3 migrated plus 3 (xor, perplexity-decider, cloudflare-clef) leaves 25 entries currently in HAND_DESCRIPTORS (src/lib/factories/providerDescriptors.ts). The category counts below (Shared adapter needed / Must remain class / Must remain core class) cover exactly those 25; the 4 migrated providers are recorded separately as done and no longer count toward \"what remains.\"\n\nThis ledger records, per provider, why it is (or isn't) a JSON-catalog migration candidate, so \"add a provider\" work doesn't re-litigate the same analysis per PR.\n\nEvery verdict allows one thing regardless of class: the static descriptor metadata (name, aliases, default model, credential env var names, setup URL) can always move into a class-backed JSON record. \"Must remain class\" means the execution — the inference loop (generate/stream/decide, per the provider's inferenceKinds), auth, media pipelines — cannot be reduced to declarative catalog data; it does not mean the provider is exempt from descriptor consolidation.\n\nMigrated (4)\n\nThe first three ran the same class-removal path this ledger recommended below: resolve the JSON/descriptor data conflicts, then delete the hand descriptor and hand-written subclass so the provider is fully catalog-derived.\n\n| Provider | Note |\n| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| mistral | Moved onto catalog/mistral.json. defaultModel now derives from the new optional models.registryDefaultModel field when present (only Mistral sets it), and the setupUrl/key-format/timeout/priority conflicts this ledger flagged were resolved in the JSON rather than carried forward. |\n| huggingface | Moved onto catalog/huggingface.json, which gained wire.apiKeyFallbackEnvVars (HF_TOKEN alongside HUGGINGFACE_API_KEY) and capabilities.tools: \"model-dependent\" — the two schema gaps this ledger flagged below before the extension landed. |\n| deepseek | Moved onto catalog/deepseek.json, using the new quirks.responseFormatDowngrade: \"json-schema-to-json-object\" field (DeepSeek 400s on json_schema) instead of an executable hook — the named-quirk approach this ledger recommended. It also sets quirks.replayReasoningContent: true. |\n| laya | Moved onto catalog/laya.json, the first decide entry. Its wire (endpoint, the auto model rule, routing.model, the request-id header, error rules and body shapes) is declarative data that ConfiguredDecisionProvider reads; the LayaProvider class was deleted. It keeps a model manifest, built from the entry, so neurolink models still lists it. |\n\nShared adapter needed (9)\n\nThree adapter families, not nine one-off migrations:\n\nLocal-runtime adapter (ollama, lm-studio, llamacpp) — one OpenAI-compatible local-runtime adapter with optional auth, model discovery/probe, actionable transport errors, timeout policy, optional embeddings. Vendor-specific pull/diagnostic guidance becomes adapter data.\n\nEmbedding-only adapter (voyage, jina, cohere) — one embedding-only provider adapter parameterized by path, request shape, batch size, response-extraction/index policy, unsupported-surface messages. jina extends it with an optional rerank operation profile (don't force reranking into the boolean capability shape). cohere composes this with a generic OpenAI chat adapter (it has both /compatibility/v1 chat and native /v2/embed); preserve its batch limit and body-field quirks in the embedding profile.\n\nImage-generation adapter (stability, ideogram, recraft) — one image-generation adapter family: explici","hierarchy":{"lvl0":"Provider Integration","lvl1":"Provider Descriptor Migration Ledger","lvl2":"","lvl3":""}},
9754
+ {"objectID":"b5d36740e9c9cb41c6ccb01db0a9fb1f9c0ac8ac59f1084b9a92332dd79c3e4d","title":"Provider Descriptor Migration Ledger","url":"/docs/provider-integration/migration-ledger#provider-descriptor-migration-ledger","content":"Inventory taken at origin/release @ 2cefa3ae4115f817f75a415b6bc70fc3ecaed2d3. TypeSafe was added afterwards (#1761) and is included below; the 25-entry count matched origin/release @ f536fd091.\n\nmistral, huggingface, deepseek and laya have since migrated to the JSON catalog and been removed from HAND_DESCRIPTORS (#1781; laya in the PR that added src/lib/providers/catalog/laya.json) — see Migrated below — and xor, perplexity-decider and cloudflare-clef (decision-only, like TypeSafe) were added as hand descriptors afterwards. Net effect: 25 minus the 3 migrated plus 3 (xor, perplexity-decider, cloudflare-clef) leaves 25 entries currently in HAND_DESCRIPTORS (src/lib/factories/providerDescriptors.ts). The category counts below (Shared adapter needed / Must remain class / Must remain core class) cover exactly those 25; the 4 migrated providers are recorded separately as done and no longer count toward \"what remains.\"\n\nThis ledger records, per provider, why it is (or isn't) a JSON-catalog migration candidate, so \"add a provider\" work doesn't re-litigate the same analysis per PR.\n\nEvery verdict allows one thing regardless of class: the static descriptor metadata (name, aliases, default model, credential env var names, setup URL) can always move into a class-backed JSON record. \"Must remain class\" means the execution — the inference loop (generate/stream/decide, per the provider's inferenceKinds), auth, media pipelines — cannot be reduced to declarative catalog data; it does not mean the provider is exempt from descriptor consolidation.","hierarchy":{"lvl0":"Provider Integration","lvl1":"Provider Descriptor Migration Ledger","lvl2":"Provider Descriptor Migration Ledger","lvl3":""}},
9755
+ {"objectID":"ff6dac10527e9e03a2da5e5a12274cdcd3d49262fbb4fe837cd96d400049ca6f","title":"Migrated (4)","url":"/docs/provider-integration/migration-ledger#migrated-4","content":"The first three ran the same class-removal path this ledger recommended below: resolve the JSON/descriptor data conflicts, then delete the hand descriptor and hand-written subclass so the provider is fully catalog-derived.\n\n| Provider | Note |\n| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| mistral | Moved onto catalog/mistral.json. defaultModel now derives from the new optional models.registryDefaultModel field when present (only Mistral sets it), and the setupUrl/key-format/timeout/priority conflicts this ledger flagged were resolved in the JSON rather than carried forward. |\n| huggingface | Moved onto catalog/huggingface.json, which gained wire.apiKeyFallbackEnvVars (HF_TOKEN alongside HUGGINGFACE_API_KEY) and capabilities.tools: \"model-dependent\" — the two schema gaps this ledger flagged below before the extension landed. |\n| deepseek | Moved onto catalog/deepseek.json, using the new quirks.responseFormatDowngrade: \"json-schema-to-json-object\" field (DeepSeek 400s on json_schema) instead of an executable hook — the named-quirk approach this ledger recommended. It also sets quirks.replayReasoningContent: true.","hierarchy":{"lvl0":"Provider Integration","lvl1":"Provider Descriptor Migration Ledger","lvl2":"Migrated (4)","lvl3":""}},
9756
9756
  {"objectID":"cbc4193e5e9ffb4c77dd167fe137bc62cfbab5adbe808e814a374670bef12175","title":"Shared adapter needed (9)","url":"/docs/provider-integration/migration-ledger#shared-adapter-needed-9","content":"Three adapter families, not nine one-off migrations:\n\nLocal-runtime adapter (ollama, lm-studio, llamacpp) — one OpenAI-compatible local-runtime adapter with optional auth, model discovery/probe, actionable transport errors, timeout policy, optional embeddings. Vendor-specific pull/diagnostic guidance becomes adapter data.\n\nEmbedding-only adapter (voyage, jina, cohere) — one embedding-only provider adapter parameterized by path, request shape, batch size, response-extraction/index policy, unsupported-surface messages. jina extends it with an optional rerank operation profile (don't force reranking into the boolean capability shape). cohere composes this with a generic OpenAI chat adapter (it has both /compatibility/v1 chat and native /v2/embed); preserve its batch limit and body-field quirks in the embedding profile.\n\nImage-generation adapter (stability, ideogram, recraft) — one image-generation adapter family: explicit multipart vs JSON request profile, base64-vs-URL response profile, custom auth-header support, model-path mapping as data. SSRF and bounded-read policy remain shared mandatory behavior, not per-vendor opt-outs.","hierarchy":{"lvl0":"Provider Integration","lvl1":"Provider Descriptor Migration Ledger","lvl2":"Shared adapter needed (9)","lvl3":""}},
9757
- {"objectID":"b9d3b5b409f7d1545aa51c86379108fea5e57bbc3b19cb9f4aa352a00e7bc23b","title":"Must remain class (16)","url":"/docs/provider-integration/migration-ledger#must-remain-class-16","content":"| Provider | Why |\n| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| bedrock | AWS SigV4/Converse protocol, custom loop, not OpenAI-compatible. Tier 4. |\n| openai | Chat portion is catalog-shaped, but embeddings, image generation, and OpenAI-specific telemetry/errors are executable behavior. |\n| vertex | Two native protocols (Google GenAI + Anthropic-on-Vertex) and multiple modality pipelines behind one provider identity.","hierarchy":{"lvl0":"Provider Integration","lvl1":"Provider Descriptor Migration Ledger","lvl2":"Must remain class (16)","lvl3":""}},
9757
+ {"objectID":"68866693ac3b73ea496e4c12c1b7d8fe2de069553e5d38707095387d8f39faba","title":"Must remain class (15)","url":"/docs/provider-integration/migration-ledger#must-remain-class-15","content":"| Provider | Why |\n| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| bedrock | AWS SigV4/Converse protocol, custom loop, not OpenAI-compatible. Tier 4. |\n| openai | Chat portion is catalog-shaped, but embeddings, image generation, and OpenAI-specific telemetry/errors are executable behavior. |\n| vertex | Two native protocols (Google GenAI + Anthropic-on-Vertex) and multiple modality pipelines behind one provider identity.","hierarchy":{"lvl0":"Provider Integration","lvl1":"Provider Descriptor Migration Ledger","lvl2":"Must remain class (15)","lvl3":""}},
9758
9758
  {"objectID":"a4f69e09308f075830f2efa7c1b0126236c5ec0fc12bfe08be21764a627e47a5","title":"Must remain core class (1)","url":"/docs/provider-integration/migration-ledger#must-remain-core-class-1","content":"| Provider | Why |\n| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| openai-compatible | This is the generic protocol surface itself, not a direct vendor. It should consume shared descriptor/config primitives but must never become a fixed-vendor JSON row — collapsing it into the catalog would conflate \"the adapter\" with \"an entry in the adapter's catalog.\" |","hierarchy":{"lvl0":"Provider Integration","lvl1":"Provider Descriptor Migration Ledger","lvl2":"Must remain core class (1)","lvl3":""}},
9759
- {"objectID":"235850085d9102413140920b9d5e5e2ff6113d007c865f5d78012af80fbf1843","title":"Suggested execution order","url":"/docs/provider-integration/migration-ledger#suggested-execution-order","content":"The former steps 1–2 below (mistral; huggingface, deepseek) are complete — see Migrated above. What remains:\nThree shared adapters (local-runtime, embedding-only, image-generation) — each unlocks 3 providers at once; build once, migrate three.\ngoogle-ai descriptor-only JSON metadata — proof of concept for moving static descriptor data out of a \"must remain class\" provider without touching its execution.\nDirect-vendor \"must remain class\" providers (bedrock, openai, vertex, anthropic, azure, sagemaker, nvidia-nim) get descriptor-only JSON metadata migrations, execution untouched — lower priority, cosmetic consolidation only.\nAggregators (litellm, openrouter), the core adapter (openai-compatible), and the decision-only providers (typesafe, laya, xor, perplexity-decider, cloudflare-clef) are excluded from the first-class-count migration priority entirely — the first three add no direct-vendor coverage however they're implemented, and typesafe/laya/xor/perplexity-decider/cloudflare-clef are decide-only providers with no generate/stream surface to migrate at all.","hierarchy":{"lvl0":"Provider Integration","lvl1":"Provider Descriptor Migration Ledger","lvl2":"Suggested execution order","lvl3":""}},
9759
+ {"objectID":"235850085d9102413140920b9d5e5e2ff6113d007c865f5d78012af80fbf1843","title":"Suggested execution order","url":"/docs/provider-integration/migration-ledger#suggested-execution-order","content":"The former steps 1–2 below (mistral; huggingface, deepseek) are complete, and laya has moved too — see Migrated above. What remains:\nThree shared adapters (local-runtime, embedding-only, image-generation) — each unlocks 3 providers at once; build once, migrate three.\ngoogle-ai descriptor-only JSON metadata — proof of concept for moving static descriptor data out of a \"must remain class\" provider without touching its execution.\nDirect-vendor \"must remain class\" providers (bedrock, openai, vertex, anthropic, azure, sagemaker, nvidia-nim) get descriptor-only JSON metadata migrations, execution untouched — lower priority, cosmetic consolidation only.\nAggregators (litellm, openrouter), the core adapter (openai-compatible), and the decision-only providers (typesafe, xor, perplexity-decider, cloudflare-clef) are excluded from the first-class-count migration priority entirely — the first three add no direct-vendor coverage however they're implemented, and typesafe/xor/perplexity-decider/cloudflare-clef are decide-only providers with no generate/stream surface to migrate at all.","hierarchy":{"lvl0":"Provider Integration","lvl1":"Provider Descriptor Migration Ledger","lvl2":"Suggested execution order","lvl3":""}},
9760
9760
  {"objectID":"6122b2e83358a45c64da87c1ca80a2d579cdfb087a6d9dc58b1e02805296b14a","title":"OpenAI-Compatible Provider Catalog","url":"/docs/provider-integration/openai-compat-catalog","content":"OpenAI-Compatible Provider Catalog\n\nEvery OpenAI-compatible provider in the catalog (80 as of 2026-09-29; the\ndirectory is the list) is registered from one JSON file each, under\nsrc/lib/providers/catalog/<id>.json, and served by one generic class,\nConfiguredOpenAICompatProvider\n(src/lib/providers/configuredOpenAICompat.ts). Adding another provider to\nthis family means adding one JSON file — no subclass, no registry edit, no\nhand-written enum member, no test edit.\n\nThe JSON is the source of truth\n\nOPENAI_COMPAT_CATALOG still exists and keeps its name and element type,\nbut it is now built by the loader (src/lib/providers/catalog/loader.ts)\nfrom the JSON files rather than hand-written. Two consumers read the JSON:\nCodegen (pnpm run codegen:catalog) writes the compile-time\n artifacts into marked regions — the AIProviderName member, the\n <Name>Models enum, the NeurolinkCredentials key — plus the generated\n index. Pre-commit and CI fail on stale output.\nThe loader builds the runtime entry; the descriptor, config options,\n context windows, pricing, vision map and model-choice tables all derive\n from it, as do the provider test suites' rows and counts.\n\nEach file is validated by a zod schema (src/lib/providers/catalog/schema.ts)\nwith a mirrored provider-catalog.schema.json for editor squiggles.\nProbe evidence (roster/auth/billing dates, live-matrix result, PR URL)\nlives in the file's evidence block — the old\ndocs/provider-integration/manifests/<id>.json files were folded into it.\n\nField-by-field reference and the escape hatches:\ntiers/tier-2-catalog-entry.md. Design rationale and the approved rulings:\ndocs/superpowers/plans/2026-08-28-provider-json-catalog-spec.md.\n\nWhen a provider belongs in the catalog\n\nA provider belongs in the JSON catalog if it needs only:\na credential (API key, optionally an extra field like Cloudflare's account id)\na base URL (static default + optional env override, or computed from an\n extra credential field)\na default/fallback model\nerror-message classification (auth / rate-limit / invalid-model / generic)\na named, closed catalog quirk. DeepSeek is the worked example: it 400s on\n json_schema structured-output requests, so deepseek.json sets\n quirks.responseFormatDowngrade: \"json-schema-to-json-object\" and the\n generic ConfiguredOpenAICompatProvider downgrades to json_object before\n sending — no subclass. It also sets quirks.replayReasoningContent: true:\n DeepSeek documents that each assistant turn's reasoning_content must go\n back on later requests once tools are in play, so the shared message\n converter and the streaming tool loop send it — for this quirk only, since\n strict OpenAI-compatible backends reject the unknown field.\na wire-proven capability such as\n capabilities.structuredOutputWithTools. The generic provider suppresses\n response_format when tools are attached by default; an explicit true\n keeps it on the same generate() or stream() request. Set this only after\n a combined tools-plus-schema request returned successfully. Separate tool\n and structured-output probes are not evidence for the combined capability.\n A stale opt-in is still protected by the runtime conflict retry, which drops\n structured output and retries rather than losing the turn.\n\nThe currently opted-in providers are Baseten, DeepSeek, Fireworks AI, GMI\nCloud, Inception Labs, io.net Intelligence, Novita AI, Together AI, Upstage and\nxAI. API Route remains opted out because its evidence verifies the features\nseparately, not together in one request. Mistral remains opted out because its\ncombined probes returned 429 twice, not a successful capability response.\n\nWhen a provider needs a dedicated subclass instead\n\nAzure OpenAI is deliberately not in the catalog because it overrides real\nrequest-shaping behavior that no named catalog quirk expresses:\nAzure OpenAI (src/lib/providers/azureOpenai.ts) overrides four hooks:\n getChatCompletionsURL (deployment-name URL routing across two Azure\n endpoint schemes), getAuthHeaders (Azure's api-key header instead of\n Authorization: Bearer), adjustRequestBody (renames max_tokens to\n max_completion_tokens for o-series/gpt-5+ deployments), and\n suppressResponseFormatWithTools (Azure supports both at once).\n\nIf a future provider needs any hook beyond the 3 mandatory ones\n(getProviderName, getDefaultModel, formatProviderError) or the 2\npurely-declarative optional ones (getFallbackModelName,\ngetFallbackModels) and that hook is not already a named catalog quirk, it\nneeds either a new closed quirk (the DeepSeek route) or a dedicated subclass\n(the Azure OpenAI route).\n\nError-message fidelity\n\nEach entry's errorRules is a direct, order-preserving translation of its\noriginal subclass's formatProviderError if/else ladder into rule data\n(status code and/or case-insensitive pattern), classified via\nclassifyProviderError()\n(src/lib/utils/errorClassifier.ts). Every bespoke message string is\npreserved verbatim — including xAI's \"top up your account\" quota URL and\nGroq's d","hierarchy":{"lvl0":"Provider Integration","lvl1":"OpenAI-Compatible Provider Catalog","lvl2":"","lvl3":""}},
9761
9761
  {"objectID":"9a91a5b4a32a9f1e89c6a28f762c3594c4a39d96d32cc9e6e1fa0f5cb3f609e4","title":"OpenAI-Compatible Provider Catalog","url":"/docs/provider-integration/openai-compat-catalog#openai-compatible-provider-catalog","content":"Every OpenAI-compatible provider in the catalog (80 as of 2026-09-29; the\ndirectory is the list) is registered from one JSON file each, under\nsrc/lib/providers/catalog/<id>.json, and served by one generic class,\nConfiguredOpenAICompatProvider\n(src/lib/providers/configuredOpenAICompat.ts). Adding another provider to\nthis family means adding one JSON file — no subclass, no registry edit, no\nhand-written enum member, no test edit.","hierarchy":{"lvl0":"Provider Integration","lvl1":"OpenAI-Compatible Provider Catalog","lvl2":"OpenAI-Compatible Provider Catalog","lvl3":""}},
9762
9762
  {"objectID":"16e60ee66171009a6638b5110cc8693f90c1a2cfde796c6932c3512d7f7421fb","title":"The JSON is the source of truth","url":"/docs/provider-integration/openai-compat-catalog#the-json-is-the-source-of-truth","content":"OPENAI_COMPAT_CATALOG still exists and keeps its name and element type,\nbut it is now built by the loader (src/lib/providers/catalog/loader.ts)\nfrom the JSON files rather than hand-written. Two consumers read the JSON:\nCodegen (pnpm run codegen:catalog) writes the compile-time\n artifacts into marked regions — the AIProviderName member, the\n <Name>Models enum, the NeurolinkCredentials key — plus the generated\n index. Pre-commit and CI fail on stale output.\nThe loader builds the runtime entry; the descriptor, config options,\n context windows, pricing, vision map and model-choice tables all derive\n from it, as do the provider test suites' rows and counts.\n\nEach file is validated by a zod schema (src/lib/providers/catalog/schema.ts)\nwith a mirrored provider-catalog.schema.json for editor squiggles.\nProbe evidence (roster/auth/billing dates, live-matrix result, PR URL)\nlives in the file's evidence block — the old\ndocs/provider-integration/manifests/<id>.json files were folded into it.\n\nField-by-field reference and the escape hatches:\ntiers/tier-2-catalog-entry.md. Design rationale and the approved rulings:\ndocs/superpowers/plans/2026-08-28-provider-json-catalog-spec.md.","hierarchy":{"lvl0":"Provider Integration","lvl1":"OpenAI-Compatible Provider Catalog","lvl2":"The JSON is the source of truth","lvl3":""}},