@juspay/neurolink 12.55.2 → 12.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,9 +13,9 @@ import type { ProviderConfigOptions, SetupArgs } from "../../types/index.js";
13
13
  * perplexity, sambanova, together-ai, xai — mistral is the 9th catalog
14
14
  * provider but keeps its own handleMistralSetup() flow above, so it's
15
15
  * excluded here), spread directly from the catalog JSON via
16
- * buildCatalogConfigOptions(); decide-kind catalog entries (laya) are spread
17
- * the same way from getDecideCatalogEntries(). The hand rows listed first
18
- * reuse the existing createXConfig() factories in providerConfig.ts; the
16
+ * buildCatalogConfigOptions(); decide-kind catalog entries (laya, xor) are
17
+ * spread the same way from getDecideCatalogEntries(). The hand rows listed
18
+ * first reuse the existing createXConfig() factories in providerConfig.ts; the
19
19
  * remaining 5 (ollama, litellm, sagemaker, lm-studio, llamacpp) don't have a
20
20
  * factory and are defined inline using their real env var names. lm-studio
21
21
  * and llamacpp previously
@@ -21,7 +21,7 @@ import { handleHuggingFaceSetup } from "./setup-huggingface.js";
21
21
  import { handleMistralSetup } from "./setup-mistral.js";
22
22
  import { OpenRouterModels, } from "../../constants/enums.js";
23
23
  import { PROVIDER_DESCRIPTORS_BY_NAME } from "../../factories/providerDescriptors.js";
24
- import { createCohereConfig, createDeepSeekConfig, createIdeogramConfig, createJinaConfig, createNvidiaNimConfig, createOpenAICompatibleConfig, createRecraftConfig, createReplicateConfig, createStabilityConfig, createVoyageConfig, createTypeSafeConfig, createXorConfig, createPerplexityDeciderConfig, createCloudflareClefConfig, satisfiesFallbacks, } from "../../utils/providerConfig.js";
24
+ import { createCohereConfig, createDeepSeekConfig, createIdeogramConfig, createJinaConfig, createNvidiaNimConfig, createOpenAICompatibleConfig, createRecraftConfig, createReplicateConfig, createStabilityConfig, createVoyageConfig, createTypeSafeConfig, createPerplexityDeciderConfig, createCloudflareClefConfig, satisfiesFallbacks, } from "../../utils/providerConfig.js";
25
25
  import { getCatalogJsonEntries, getDecideCatalogEntries, buildCatalogConfigOptions, } from "../../providers/catalog/loader.js";
26
26
  // Provider information database
27
27
  const PROVIDERS = [
@@ -150,9 +150,9 @@ const PROVIDERS = [
150
150
  * perplexity, sambanova, together-ai, xai — mistral is the 9th catalog
151
151
  * provider but keeps its own handleMistralSetup() flow above, so it's
152
152
  * excluded here), spread directly from the catalog JSON via
153
- * buildCatalogConfigOptions(); decide-kind catalog entries (laya) are spread
154
- * the same way from getDecideCatalogEntries(). The hand rows listed first
155
- * reuse the existing createXConfig() factories in providerConfig.ts; the
153
+ * buildCatalogConfigOptions(); decide-kind catalog entries (laya, xor) are
154
+ * spread the same way from getDecideCatalogEntries(). The hand rows listed
155
+ * first reuse the existing createXConfig() factories in providerConfig.ts; the
156
156
  * remaining 5 (ollama, litellm, sagemaker, lm-studio, llamacpp) don't have a
157
157
  * factory and are defined inline using their real env var names. lm-studio
158
158
  * and llamacpp previously
@@ -172,7 +172,6 @@ export const EXTRA_PROVIDER_CONFIGS = {
172
172
  ideogram: createIdeogramConfig(),
173
173
  recraft: createRecraftConfig(),
174
174
  typesafe: createTypeSafeConfig(),
175
- xor: createXorConfig(),
176
175
  "perplexity-decider": createPerplexityDeciderConfig(),
177
176
  "cloudflare-clef": createCloudflareClefConfig(),
178
177
  ...Object.fromEntries(getDecideCatalogEntries().map((e) => [e.id, buildCatalogConfigOptions(e)])),
@@ -103,6 +103,7 @@ export declare enum AIProviderName {
103
103
  WANDB_INFERENCE = "wandb-inference",
104
104
  XAI = "xai",
105
105
  XIAOMI_MIMO = "xiaomi-mimo",
106
+ XOR = "xor",
106
107
  Z_AI = "z-ai",
107
108
  COHERE = "cohere",
108
109
  REPLICATE = "replicate",
@@ -113,8 +114,6 @@ export declare enum AIProviderName {
113
114
  RECRAFT = "recraft",
114
115
  /** TypeSafe (Jev) — serves the `decide` inference type only. */
115
116
  TYPESAFE = "typesafe",
116
- /** XOR (Juspay, open weights) — serves the `decide` inference type only. */
117
- XOR = "xor",
118
117
  /**
119
118
  * Perplexity Decisions API (`pplx-decider-v1-27b`) — serves the `decide`
120
119
  * inference type only. Distinct from `PERPLEXITY`, the Sonar text provider.
@@ -1763,6 +1762,9 @@ export declare enum XaiModels {
1763
1762
  export declare enum XiaomiMimoModels {
1764
1763
  MIMO_V2_6_FLASH = "mimo-v2.6-flash"
1765
1764
  }
1765
+ export declare enum XorModels {
1766
+ XOR_1_1 = "xor-1.1"
1767
+ }
1766
1768
  export declare enum ZAiModels {
1767
1769
  GLM_5_3 = "glm-5.3",
1768
1770
  GLM_5_3_FLASH = "glm-5.3-flash",
@@ -1788,13 +1790,6 @@ export declare enum TypeSafeModels {
1788
1790
  JEV_LATEST = "jev-latest",
1789
1791
  JEV_PREVIEW = "jev-preview"
1790
1792
  }
1791
- /**
1792
- * XOR decision models. Hand-written: XOR is a Tier-3 provider, so it is not in
1793
- * the provider catalog and codegen never touches this.
1794
- */
1795
- export declare enum XorModels {
1796
- XOR_1_1 = "xor-1.1"
1797
- }
1798
1793
  /**
1799
1794
  * Perplexity decision models. Hand-written: the Decisions API is a Tier-3
1800
1795
  * provider, so it is not in the provider catalog and codegen never touches
@@ -108,6 +108,7 @@ export var AIProviderName;
108
108
  AIProviderName["WANDB_INFERENCE"] = "wandb-inference";
109
109
  AIProviderName["XAI"] = "xai";
110
110
  AIProviderName["XIAOMI_MIMO"] = "xiaomi-mimo";
111
+ AIProviderName["XOR"] = "xor";
111
112
  AIProviderName["Z_AI"] = "z-ai";
112
113
  // ── END GENERATED(provider-members) ──
113
114
  AIProviderName["COHERE"] = "cohere";
@@ -119,8 +120,6 @@ export var AIProviderName;
119
120
  AIProviderName["RECRAFT"] = "recraft";
120
121
  /** TypeSafe (Jev) — serves the `decide` inference type only. */
121
122
  AIProviderName["TYPESAFE"] = "typesafe";
122
- /** XOR (Juspay, open weights) — serves the `decide` inference type only. */
123
- AIProviderName["XOR"] = "xor";
124
123
  /**
125
124
  * Perplexity Decisions API (`pplx-decider-v1-27b`) — serves the `decide`
126
125
  * inference type only. Distinct from `PERPLEXITY`, the Sonar text provider.
@@ -2086,6 +2085,10 @@ export var XiaomiMimoModels;
2086
2085
  (function (XiaomiMimoModels) {
2087
2086
  XiaomiMimoModels["MIMO_V2_6_FLASH"] = "mimo-v2.6-flash";
2088
2087
  })(XiaomiMimoModels || (XiaomiMimoModels = {}));
2088
+ export var XorModels;
2089
+ (function (XorModels) {
2090
+ XorModels["XOR_1_1"] = "xor-1.1";
2091
+ })(XorModels || (XorModels = {}));
2089
2092
  export var ZAiModels;
2090
2093
  (function (ZAiModels) {
2091
2094
  ZAiModels["GLM_5_3"] = "glm-5.3";
@@ -2114,14 +2117,6 @@ export var TypeSafeModels;
2114
2117
  TypeSafeModels["JEV_LATEST"] = "jev-latest";
2115
2118
  TypeSafeModels["JEV_PREVIEW"] = "jev-preview";
2116
2119
  })(TypeSafeModels || (TypeSafeModels = {}));
2117
- /**
2118
- * XOR decision models. Hand-written: XOR is a Tier-3 provider, so it is not in
2119
- * the provider catalog and codegen never touches this.
2120
- */
2121
- export var XorModels;
2122
- (function (XorModels) {
2123
- XorModels["XOR_1_1"] = "xor-1.1";
2124
- })(XorModels || (XorModels = {}));
2125
2120
  /**
2126
2121
  * Perplexity decision models. Hand-written: the Decisions API is a Tier-3
2127
2122
  * provider, so it is not in the provider catalog and codegen never touches
@@ -1,5 +1,5 @@
1
1
  import { AIProviderName } from "../constants/enums.js";
2
- import { GoogleAIModels, OpenAIModels, AnthropicModels, VertexModels, OllamaModels, LiteLLMModels, NvidiaNimModels, OpenRouterModels, CohereModels, VoyageModels, TypeSafeModels, XorModels, PerplexityDeciderModels, CloudflareClefModels, JinaModels, StabilityModels, IdeogramModels, RecraftModels, ReplicateModels, } from "../constants/enums.js";
2
+ import { GoogleAIModels, OpenAIModels, AnthropicModels, VertexModels, OllamaModels, LiteLLMModels, NvidiaNimModels, OpenRouterModels, CohereModels, VoyageModels, TypeSafeModels, PerplexityDeciderModels, CloudflareClefModels, JinaModels, StabilityModels, IdeogramModels, RecraftModels, ReplicateModels, } from "../constants/enums.js";
3
3
  import { API_KEY_FORMATS } from "../utils/providerConfig.js";
4
4
  import { getCatalogJsonEntries, getDecideCatalogEntries, catalogCredentialsKey, catalogEnvVar, } from "../providers/catalog/loader.js";
5
5
  import { DEFAULT_INFERENCE_KINDS } from "../types/index.js";
@@ -467,64 +467,23 @@ const HAND_DESCRIPTORS = [
467
467
  },
468
468
  setupUrl: "https://console.typesafe.ai/keys",
469
469
  },
470
- // Decision precedence is decideOrder, not position: typesafe 1, laya 2 (from
471
- // catalog/laya.json), xor 3, perplexity-decider 4, cloudflare-clef 5.
470
+ // Decision precedence is decideOrder, not position: typesafe 1, laya 2 and
471
+ // xor 3 (from catalog/laya.json and catalog/xor.json), perplexity-decider 4,
472
+ // cloudflare-clef 5.
472
473
  // resolveDefaultDecisionProvider() returns the first configured
473
474
  // DECISION_PROVIDERS entry in that order, so a host configured for several
474
475
  // keeps Jev for every built-in consumer and reaches the others only by
475
476
  // naming them. Renumbering silently changes which model routes, compacts
476
477
  // and plans for every such host.
477
- {
478
- name: AIProviderName.XOR,
479
- aliases: [],
480
- credentialsKey: "xor",
481
- envVars: {
482
- apiKey: "XOR_API_KEY",
483
- baseURL: "XOR_BASE_URL",
484
- // XOR has no built-in endpoint (it is reached at a deployment or behind a
485
- // proxy), so a key is useless without a base URL. Both are required for
486
- // it to count as configured, from the environment or credentials.xor.
487
- extraRequired: ["XOR_BASE_URL"],
488
- model: "XOR_MODEL",
489
- },
490
- defaultModel: XorModels.XOR_1_1,
491
- // Serves only `decide`, like TypeSafe and Laya — the one declaration that
492
- // keeps it out of every generation code path.
493
- inferenceKinds: ["decide"],
494
- decideOrder: 3,
495
- toolSupport: "none",
496
- localRuntime: false,
497
- healthCheck: "env-only",
498
- // Deliberately NO autoSelectPriority / autoSelectPreference /
499
- // defaultHealthSweepPriority, for the same reason as TypeSafe above.
500
- timeouts: { decideMs: 5000 },
501
- // The deployment's prefill is 250,000 tokens, shared by the state, the
502
- // questions and any media. 200,000 leaves 20% for those, and a state
503
- // between this and the server's 4 MB would otherwise pass validation and
504
- // fail inside the engine as a 500, which the base class would retry. There
505
- // is no question cap (the server only limits a choice to 2..255 options).
506
- // Non-ASCII text is charged per character because the four-characters-
507
- // per-token estimate is several times too generous for CJK on this
508
- // tokenizer. One token per character is a deliberate upper bound, to be
509
- // replaced by a live measurement.
510
- decisionLimits: {
511
- maxStateTokens: 200_000,
512
- nonAsciiTokensPerChar: 1,
513
- // The server takes 1..8 images and one video, and refuses a body over
514
- // 8 MB.
515
- media: { maxImages: 8, video: true, maxRequestBytes: 8 * 1024 * 1024 },
516
- },
517
- setupUrl: "https://huggingface.co/juspay/xor",
518
- },
519
478
  {
520
479
  name: AIProviderName.PERPLEXITY_DECIDER,
521
480
  aliases: [],
522
481
  credentialsKey: "perplexityDecider",
523
482
  envVars: {
524
483
  // The same key the `perplexity` text provider reads, so an ambient
525
- // PERPLEXITY_API_KEY configures this provider too. It sits after XOR in
526
- // this list, which is what keeps that from displacing any other decision
527
- // provider a host has configured.
484
+ // PERPLEXITY_API_KEY configures this provider too. Its decideOrder (4)
485
+ // sits after XOR's (3), which is what keeps that from displacing any other
486
+ // decision provider a host has configured.
528
487
  apiKey: "PERPLEXITY_API_KEY",
529
488
  // Optional: the API is hosted at a public endpoint, so a key alone is
530
489
  // enough and there is no extraRequired.
@@ -1,6 +1,6 @@
1
1
  import { ProviderFactory } from "./providerFactory.js";
2
2
  import { logger } from "../utils/logger.js";
3
- import { AIProviderName, GoogleAIModels, OpenAIModels, AnthropicModels, VertexModels, OllamaModels, LiteLLMModels, NvidiaNimModels, OpenRouterModels, CohereModels, VoyageModels, JinaModels, StabilityModels, IdeogramModels, RecraftModels, TypeSafeModels, XorModels, PerplexityDeciderModels, CloudflareClefModels, ReplicateModels, } from "../constants/enums.js";
3
+ import { AIProviderName, GoogleAIModels, OpenAIModels, AnthropicModels, VertexModels, OllamaModels, LiteLLMModels, NvidiaNimModels, OpenRouterModels, CohereModels, VoyageModels, JinaModels, StabilityModels, IdeogramModels, RecraftModels, TypeSafeModels, PerplexityDeciderModels, CloudflareClefModels, ReplicateModels, } from "../constants/enums.js";
4
4
  import { PROVIDER_DESCRIPTORS_BY_NAME } from "./providerDescriptors.js";
5
5
  import { OPENAI_COMPAT_CATALOG } from "../providers/openaiCompatCatalog.js";
6
6
  import { providerChoicesFor } from "./mediaHandlerCatalog.js";
@@ -250,14 +250,6 @@ export class ProviderRegistry {
250
250
  const { TypeSafeProvider } = await import("../providers/typesafe.js");
251
251
  return new TypeSafeProvider(modelName, sdk, undefined, typesafeCreds);
252
252
  }, process.env.TYPESAFE_MODEL || TypeSafeModels.JEV_LATEST, ["jev", "typesafe-ai"], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.TYPESAFE));
253
- // Register XOR — the third `decide` provider, reached at whatever base
254
- // URL its config names. Its descriptor declares inferenceKinds:
255
- // ["decide"], so nothing in the generation fallback chain can reach it.
256
- ProviderFactory.registerProvider(AIProviderName.XOR, async (modelName, _providerName, sdk, _region, credentials) => {
257
- const xorCreds = credentials;
258
- const { XorProvider } = await import("../providers/xor.js");
259
- return new XorProvider(modelName, sdk, undefined, xorCreds);
260
- }, process.env.XOR_MODEL || XorModels.XOR_1_1, [], PROVIDER_DESCRIPTORS_BY_NAME.get(AIProviderName.XOR));
261
253
  // Register the Perplexity Decisions API — a `decide` provider reached at
262
254
  // Perplexity's public endpoint. Its descriptor declares inferenceKinds:
263
255
  // ["decide"], so nothing in the generation fallback chain can reach it,
@@ -20,7 +20,6 @@ import { llamacppManifest } from "./manifests/llamacpp.js";
20
20
  import { xaiManifest } from "./manifests/xai.js";
21
21
  import { groqManifest } from "./manifests/groq.js";
22
22
  import { typesafeManifest } from "./manifests/typesafe.js";
23
- import { xorManifest } from "./manifests/xor.js";
24
23
  import { perplexityDeciderManifest } from "./manifests/perplexityDecider.js";
25
24
  import { cloudflareClefManifest } from "./manifests/cloudflareClef.js";
26
25
  import { cohereManifest } from "./manifests/cohere.js";
@@ -160,7 +159,6 @@ export const MANIFEST_REGISTRY = {
160
159
  groq: groqManifest,
161
160
  typesafe: typesafeManifest,
162
161
  ...Object.fromEntries(getDecideCatalogEntries().map((e) => [e.id, buildDecideManifest(e)])),
163
- xor: xorManifest,
164
162
  "perplexity-decider": perplexityDeciderManifest,
165
163
  "cloudflare-clef": cloudflareClefManifest,
166
164
  cerebras: catalogManifest("cerebras"),
@@ -2,4 +2,4 @@ import type { DecideCatalogJson, ProviderCatalogJson } from "../../types/index.j
2
2
  export declare const CATALOG_JSON_ENTRIES: ProviderCatalogJson[];
3
3
  export declare const CATALOG_PROVIDER_IDS: readonly ["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", "longcat", "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", "runinfra", "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", "xiaomi-mimo", "z-ai"];
4
4
  export declare const DECIDE_CATALOG_JSON_ENTRIES: DecideCatalogJson[];
5
- export declare const DECIDE_CATALOG_PROVIDER_IDS: readonly ["laya"];
5
+ export declare const DECIDE_CATALOG_PROVIDER_IDS: readonly ["laya", "xor"];
@@ -87,6 +87,7 @@ import waferJson from "./wafer.json" with { type: "json" };
87
87
  import wandbInferenceJson from "./wandb-inference.json" with { type: "json" };
88
88
  import xaiJson from "./xai.json" with { type: "json" };
89
89
  import xiaomiMimoJson from "./xiaomi-mimo.json" with { type: "json" };
90
+ import xorJson from "./xor.json" with { type: "json" };
90
91
  import zAiJson from "./z-ai.json" with { type: "json" };
91
92
  import { isDecideCatalogEntry, parseCatalogEntry } from "./schema.js";
92
93
  const asDecideEntry = (raw) => {
@@ -274,5 +275,8 @@ export const CATALOG_PROVIDER_IDS = [
274
275
  "xiaomi-mimo",
275
276
  "z-ai",
276
277
  ];
277
- export const DECIDE_CATALOG_JSON_ENTRIES = [layaJson].map(asDecideEntry);
278
- export const DECIDE_CATALOG_PROVIDER_IDS = ["laya"];
278
+ export const DECIDE_CATALOG_JSON_ENTRIES = [
279
+ layaJson,
280
+ xorJson,
281
+ ].map(asDecideEntry);
282
+ export const DECIDE_CATALOG_PROVIDER_IDS = ["laya", "xor"];
@@ -60,6 +60,24 @@
60
60
  "symbolTokensPerChar": { "type": "number", "exclusiveMinimum": 0 },
61
61
  "astralTokensPerChar": { "type": "number", "exclusiveMinimum": 0 },
62
62
  "advisory": { "type": "boolean" },
63
+ "media": {
64
+ "type": "object",
65
+ "additionalProperties": false,
66
+ "required": ["maxImages", "video", "maxRequestBytes"],
67
+ "description": "What a request may carry besides text. Requires decide.media, which names the body keys the prepared media is written under.",
68
+ "properties": {
69
+ "maxImages": { "type": "integer", "minimum": 1 },
70
+ "video": {
71
+ "type": "boolean",
72
+ "description": "Whether one video is accepted. True requires decide.media.videoKey; false forbids it."
73
+ },
74
+ "maxRequestBytes": {
75
+ "type": "integer",
76
+ "minimum": 1,
77
+ "description": "Ceiling on the encoded request body, in bytes."
78
+ }
79
+ }
80
+ },
63
81
  "models": {
64
82
  "type": "object",
65
83
  "description": "Per-model limits keyed by a models.catalog key (enforced by the zod schema).",
@@ -96,7 +114,12 @@
96
114
  },
97
115
  "baseURLNormalize": {
98
116
  "type": "array",
99
- "items": { "enum": ["strip-trailing-slash"] }
117
+ "items": { "enum": ["strip-trailing-slash", "strip-v1-suffix"] },
118
+ "description": "Applied to the base URL in list order. strip-v1-suffix removes one trailing /v1, in either case."
119
+ },
120
+ "baseURLShape": {
121
+ "enum": ["origin"],
122
+ "description": "Refuses a normalised base URL that is not an http(s) URL without userinfo, a query string or a fragment. Omitted means no check."
100
123
  },
101
124
  "endpoint": {
102
125
  "type": "string",
@@ -116,6 +139,16 @@
116
139
  }
117
140
  }
118
141
  },
142
+ "media": {
143
+ "type": "object",
144
+ "additionalProperties": false,
145
+ "required": ["imagesKey"],
146
+ "description": "Body keys prepared media is written under, after model, state and questions: imagesKey only when the request carries an image, videoKey only when it carries a video, and the two differ. Requires decisionLimits.media; videoKey is required exactly when that allowance's video is true.",
147
+ "properties": {
148
+ "imagesKey": { "$ref": "#/definitions/decideMediaKey" },
149
+ "videoKey": { "$ref": "#/definitions/decideMediaKey" }
150
+ }
151
+ },
119
152
  "requestIdHeaders": {
120
153
  "type": "array",
121
154
  "items": {
@@ -156,6 +189,12 @@
156
189
  ]
157
190
  },
158
191
  "statusMin": { "$ref": "#/definitions/httpStatus" },
192
+ "messagePattern": {
193
+ "type": "string",
194
+ "minLength": 1,
195
+ "maxLength": 200,
196
+ "description": "The rule also needs the error message to match this case-insensitive regular expression (first 2,000 characters, key removed). Bounded: outside a character class no unescaped *, + or {n,}, no quantifier on a group, no count above 255, and the counts side by side multiply to at most 4,096 ways (each ? is 2, each {n,m} is m-n+1)."
197
+ },
159
198
  "kind": { "$ref": "#/definitions/decisionErrorKind" },
160
199
  "retryable": { "type": "boolean" }
161
200
  }
@@ -595,6 +634,20 @@
595
634
  },
596
635
  "definitions": {
597
636
  "httpStatus": { "type": "integer", "minimum": 100, "maximum": 599 },
637
+ "decideMediaKey": {
638
+ "type": "string",
639
+ "pattern": "^[A-Za-z][A-Za-z0-9_]*$",
640
+ "not": {
641
+ "enum": [
642
+ "model",
643
+ "state",
644
+ "questions",
645
+ "__proto__",
646
+ "constructor",
647
+ "prototype"
648
+ ]
649
+ }
650
+ },
598
651
  "decisionErrorKind": {
599
652
  "enum": [
600
653
  "authentication",
@@ -397,11 +397,97 @@ const decideErrorKindSchema = z.enum([
397
397
  "timeout",
398
398
  "network",
399
399
  ]);
400
+ // The most steps one `{n}` or `{n,m}` may stand for.
401
+ const MAX_PATTERN_COUNT = 255;
402
+ // The most ways a pattern's counts may multiply: each `?` is 2 and each `{n,m}`
403
+ // is m-n+1. Adjacent bounded counts multiply on a miss (`.{0,255}` three times
404
+ // took 38 s on one 2,000-character message), and a miss on a 5xx is retried.
405
+ // XOR's pattern is 3,362.
406
+ const MAX_PATTERN_WAYS = 4096;
407
+ // What can make a pattern backtrack catastrophically on an error message, so
408
+ // that a catalog author cannot write it: `*`, `+` and an open-ended `{n,}` have
409
+ // no upper bound; any quantifier on a group (`(a|a){0,30}`, `(ab)?`) repeats
410
+ // whatever its alternatives match, and quantifiers nested that way multiply; a
411
+ // count above MAX_PATTERN_COUNT is as good as no bound. A quantifier on a plain
412
+ // character, class or escape is fine. Inside a character class all of them are
413
+ // plain characters, and a backslash makes the next one plain everywhere.
414
+ // Counts that sit side by side multiply, so their product has a budget too
415
+ // (MAX_PATTERN_WAYS); within it a pattern is still not a guarantee of linear
416
+ // time, and wants reading.
417
+ function hasRiskyQuantifier(pattern) {
418
+ const count = /\{(\d+)(?:,(\d*))?\}/y;
419
+ let inClass = false;
420
+ let afterGroup = false;
421
+ let ways = 1;
422
+ for (let index = 0; index < pattern.length; index += 1) {
423
+ const char = pattern[index];
424
+ const onGroup = afterGroup;
425
+ afterGroup = false;
426
+ if (char === "\\") {
427
+ index += 1;
428
+ }
429
+ else if (inClass) {
430
+ inClass = char !== "]";
431
+ }
432
+ else if (char === "[") {
433
+ inClass = true;
434
+ }
435
+ else if (char === ")") {
436
+ afterGroup = true;
437
+ }
438
+ else if (char === "*" || char === "+" || (char === "?" && onGroup)) {
439
+ return true;
440
+ }
441
+ else if (char === "?") {
442
+ // The `?` that opens a group (`(?:`, `(?=`) is not a quantifier.
443
+ if (pattern[index - 1] !== "(") {
444
+ ways *= 2;
445
+ }
446
+ }
447
+ else if (char === "{") {
448
+ count.lastIndex = index;
449
+ const found = count.exec(pattern);
450
+ if (found &&
451
+ (onGroup ||
452
+ found[2] === "" ||
453
+ Math.max(Number(found[1]), Number(found[2] ?? 0)) > MAX_PATTERN_COUNT)) {
454
+ return true;
455
+ }
456
+ if (found && found[2] !== undefined) {
457
+ ways *= Number(found[2]) - Number(found[1]) + 1;
458
+ }
459
+ }
460
+ if (ways > MAX_PATTERN_WAYS) {
461
+ return true;
462
+ }
463
+ }
464
+ return false;
465
+ }
466
+ function compilesAsPattern(pattern) {
467
+ try {
468
+ new RegExp(pattern, "i");
469
+ return true;
470
+ }
471
+ catch {
472
+ return false;
473
+ }
474
+ }
475
+ const decideMessagePatternSchema = z
476
+ .string()
477
+ .min(1)
478
+ .max(200)
479
+ .refine(compilesAsPattern, {
480
+ message: "messagePattern is not a valid regular expression",
481
+ })
482
+ .refine((pattern) => !hasRiskyQuantifier(pattern), {
483
+ message: "messagePattern must be bounded: an unescaped * or +, an open-ended {n,}, a {n,m} count above 255, or any quantifier on a group, outside a character class, can backtrack catastrophically on an error message. Put ?, or {n,m} up to 255, only on a plain character, a character class or an escape, and keep the counts side by side to 4096 ways in all (each ? is 2, each {n,m} is m-n+1)",
484
+ });
400
485
  const decideErrorRuleSchema = z.strictObject({
401
486
  status: z
402
487
  .union([decideStatusSchema, z.array(decideStatusSchema).min(1)])
403
488
  .optional(),
404
489
  statusMin: decideStatusSchema.optional(),
490
+ messagePattern: decideMessagePatternSchema.optional(),
405
491
  kind: decideErrorKindSchema,
406
492
  retryable: z.boolean().optional(),
407
493
  });
@@ -434,16 +520,45 @@ const decideBodyShapeSchema = z
434
520
  path: ["formatter"],
435
521
  message: "a body shape sets isArray: true together with a formatter, or neither: an array without a formatter never matches, and a formatter on a body that is not an array reads the text as an empty list",
436
522
  });
523
+ // The keys the body already uses, and the names an object must not take as an
524
+ // own key from data.
525
+ const RESERVED_MEDIA_KEYS = new Set([
526
+ "model",
527
+ "state",
528
+ "questions",
529
+ "__proto__",
530
+ "constructor",
531
+ "prototype",
532
+ ]);
533
+ const decideMediaKeySchema = z
534
+ .string()
535
+ .regex(/^[A-Za-z][A-Za-z0-9_]*$/, "a media key must start with a letter and use only letters, digits and underscores")
536
+ .refine((key) => !RESERVED_MEDIA_KEYS.has(key), {
537
+ message: "a media key must not be model, state, questions, __proto__, constructor or prototype",
538
+ });
539
+ const decideMediaSchema = z
540
+ .strictObject({
541
+ imagesKey: decideMediaKeySchema,
542
+ videoKey: decideMediaKeySchema.optional(),
543
+ })
544
+ .refine((media) => media.imagesKey !== media.videoKey, {
545
+ path: ["videoKey"],
546
+ message: "decide.media.videoKey must differ from imagesKey: one body key cannot carry both",
547
+ });
437
548
  const decideBlockSchema = z.strictObject({
438
549
  dialect: z.enum(["system-one"]),
439
550
  order: decidePositiveInt,
440
551
  baseURLRequired: z.boolean().optional(),
441
- baseURLNormalize: z.array(z.enum(["strip-trailing-slash"])).optional(),
552
+ baseURLNormalize: z
553
+ .array(z.enum(["strip-trailing-slash", "strip-v1-suffix"]))
554
+ .optional(),
555
+ baseURLShape: z.enum(["origin"]).optional(),
442
556
  endpoint: decideEndpointSchema,
443
557
  model: z.strictObject({
444
558
  placement: z.enum(["body", "body-unless-alias"]),
445
559
  alias: z.string().min(1).optional(),
446
560
  }),
561
+ media: decideMediaSchema.optional(),
447
562
  requestIdHeaders: z.array(decideHeaderNameSchema).optional(),
448
563
  response: z
449
564
  .strictObject({ modelPaths: z.array(decidePathSchema).optional() })
@@ -487,7 +602,9 @@ const decideModelsSchema = z
487
602
  }
488
603
  }
489
604
  });
490
- // `media` is deliberately absent: no decide provider migrated yet needs it.
605
+ // `media` is the allowance the base class prepares and gates a request's images
606
+ // and video against; `decide.media` names the body keys they go out under, and
607
+ // the entry refinement below requires the two together.
491
608
  const decideLimitsSchema = z.strictObject({
492
609
  maxStateTokens: decidePositiveInt,
493
610
  maxQuestions: decidePositiveInt.optional(),
@@ -501,6 +618,13 @@ const decideLimitsSchema = z.strictObject({
501
618
  nonAsciiTokensPerChar: decidePositiveNumber.optional(),
502
619
  }))
503
620
  .optional(),
621
+ media: z
622
+ .strictObject({
623
+ maxImages: decidePositiveInt,
624
+ video: z.boolean(),
625
+ maxRequestBytes: decidePositiveInt,
626
+ })
627
+ .optional(),
504
628
  advisory: z.boolean().optional(),
505
629
  });
506
630
  const decideCatalogJsonSchema = z
@@ -523,6 +647,8 @@ const decideCatalogJsonSchema = z
523
647
  const issue = (path, message) => {
524
648
  ctx.addIssue({ code: "custom", path, message });
525
649
  };
650
+ // Two rules collide only on the same status AND the same pattern: XOR
651
+ // lists a pattern rule and a plain rule at one statusMin.
526
652
  const seenStatus = new Set();
527
653
  const seenStatusMin = new Set();
528
654
  decide.errors.rules.forEach((rule, index) => {
@@ -531,17 +657,21 @@ const decideCatalogJsonSchema = z
531
657
  issue(at, "errors.rules entry needs exactly one of status or statusMin");
532
658
  }
533
659
  const statuses = typeof rule.status === "number" ? [rule.status] : (rule.status ?? []);
660
+ const sameAs = rule.messagePattern === undefined
661
+ ? ""
662
+ : " with the same messagePattern";
663
+ const keyOf = (value) => JSON.stringify([value, rule.messagePattern ?? null]);
534
664
  for (const status of statuses) {
535
- if (seenStatus.has(status)) {
536
- issue([...at, "status"], `errors.rules lists status ${status} more than once`);
665
+ if (seenStatus.has(keyOf(status))) {
666
+ issue([...at, "status"], `errors.rules lists status ${status} more than once${sameAs}`);
537
667
  }
538
- seenStatus.add(status);
668
+ seenStatus.add(keyOf(status));
539
669
  }
540
670
  if (rule.statusMin !== undefined) {
541
- if (seenStatusMin.has(rule.statusMin)) {
542
- issue([...at, "statusMin"], `errors.rules lists statusMin ${rule.statusMin} more than once`);
671
+ if (seenStatusMin.has(keyOf(rule.statusMin))) {
672
+ issue([...at, "statusMin"], `errors.rules lists statusMin ${rule.statusMin} more than once${sameAs}`);
543
673
  }
544
- seenStatusMin.add(rule.statusMin);
674
+ seenStatusMin.add(keyOf(rule.statusMin));
545
675
  }
546
676
  });
547
677
  const modelKeys = new Set(Object.keys(entry.models.catalog));
@@ -573,6 +703,22 @@ const decideCatalogJsonSchema = z
573
703
  issue(["decisionLimits", "models", id], "decisionLimits.models key must be a key of models.catalog");
574
704
  }
575
705
  }
706
+ const mediaKeys = decide.media;
707
+ const mediaAllowance = entry.decisionLimits?.media;
708
+ if (mediaKeys !== undefined && mediaAllowance === undefined) {
709
+ issue(["decide", "media"], "decide.media needs decisionLimits.media: without an allowance the base class refuses every image and video, so these keys would never be written");
710
+ }
711
+ if (mediaKeys === undefined && mediaAllowance !== undefined) {
712
+ issue(["decisionLimits", "media"], "decisionLimits.media needs decide.media: without body keys the engine would prepare a request's media and then leave it out of the body");
713
+ }
714
+ if (mediaKeys !== undefined && mediaAllowance !== undefined) {
715
+ if (mediaAllowance.video && mediaKeys.videoKey === undefined) {
716
+ issue(["decide", "media", "videoKey"], "decide.media.videoKey is required when decisionLimits.media.video is true");
717
+ }
718
+ if (!mediaAllowance.video && mediaKeys.videoKey !== undefined) {
719
+ issue(["decide", "media", "videoKey"], "decide.media.videoKey must be omitted when decisionLimits.media.video is false");
720
+ }
721
+ }
576
722
  if (decide.baseURLRequired === true &&
577
723
  !decide.endpoint.includes("{baseURL}")) {
578
724
  issue(["decide", "endpoint"], "decide.endpoint must contain {baseURL} when decide.baseURLRequired is true");