@juspay/neurolink 12.23.3 → 12.24.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +3 -4
  2. package/dist/browser/neurolink.min.js +367 -367
  3. package/dist/constants/enums.d.ts +61 -69
  4. package/dist/constants/enums.js +63 -96
  5. package/dist/factories/providerDescriptors.d.ts +1 -1
  6. package/dist/factories/providerDescriptors.js +50 -83
  7. package/dist/factories/providerRegistry.js +9 -16
  8. package/dist/models/manifestRegistry.js +11 -2
  9. package/dist/providers/catalog/deepseek.json +84 -0
  10. package/dist/providers/catalog/huggingface.json +405 -0
  11. package/dist/providers/catalog/index.generated.d.ts +1 -1
  12. package/dist/providers/catalog/index.generated.js +6 -0
  13. package/dist/providers/catalog/loader.js +20 -5
  14. package/dist/providers/catalog/mistral.json +8 -3
  15. package/dist/providers/catalog/provider-catalog.schema.json +31 -1
  16. package/dist/providers/catalog/schema.d.ts +8 -1
  17. package/dist/providers/catalog/schema.js +9 -1
  18. package/dist/providers/configuredOpenAICompat.d.ts +14 -5
  19. package/dist/providers/configuredOpenAICompat.js +19 -4
  20. package/dist/providers/embeddingResponseParsing.d.ts +1 -0
  21. package/dist/providers/embeddingResponseParsing.js +47 -0
  22. package/dist/providers/ideogram.js +7 -1
  23. package/dist/providers/jina.js +6 -15
  24. package/dist/providers/llamaCpp.js +4 -15
  25. package/dist/providers/lmStudio.js +4 -15
  26. package/dist/providers/localRuntimeOpenAICompat.d.ts +16 -0
  27. package/dist/providers/localRuntimeOpenAICompat.js +26 -0
  28. package/dist/providers/ollama/client.js +3 -9
  29. package/dist/providers/openaiChatCompletionsBase.d.ts +3 -3
  30. package/dist/providers/openaiChatCompletionsBase.js +3 -3
  31. package/dist/providers/openaiCompatCatalog.d.ts +16 -12
  32. package/dist/providers/openaiCompatCatalog.js +16 -12
  33. package/dist/providers/voyage.js +5 -18
  34. package/dist/types/providerCatalog.d.ts +46 -1
  35. package/dist/types/providerCatalog.generated.d.ts +2 -2
  36. package/dist/types/providers.d.ts +12 -8
  37. package/dist/utils/modelChoices.d.ts +2 -2
  38. package/dist/utils/modelChoices.js +10 -38
  39. package/dist/utils/sizeGuard.d.ts +8 -5
  40. package/dist/utils/sizeGuard.js +32 -9
  41. package/docs-site/static/search-index.json +1 -1
  42. package/package.json +1 -1
  43. package/dist/providers/deepseek.d.ts +0 -47
  44. package/dist/providers/deepseek.js +0 -120
  45. package/dist/providers/huggingFace/client.d.ts +0 -54
  46. package/dist/providers/huggingFace/client.js +0 -120
  47. package/dist/providers/huggingFace/index.d.ts +0 -1
  48. package/dist/providers/huggingFace/index.js +0 -1
@@ -1,24 +1,28 @@
1
1
  import { buildCatalogEntries } from "./catalog/loader.js";
2
2
  /**
3
- * Config-driven catalog of the 9 zero-quirk OpenAI-compatible providers.
4
- * Each entry fully replaces what used to be a hand-written
5
- * OpenAIChatCompletionsProvider subclass — see ConfiguredOpenAICompatProvider
6
- * for the class that reads these entries, and providerRegistry.ts for the
7
- * registration loop that consumes this array.
3
+ * Config-driven catalog of the 10 OpenAI-compatible providers whose only
4
+ * differences from each other are expressible as named, closed catalog
5
+ * quirks (messageContentFormat, timeoutErrorClass, responseFormatDowngrade)
6
+ * rather than real code. Each entry fully replaces what used to be a
7
+ * hand-written OpenAIChatCompletionsProvider subclass — see
8
+ * ConfiguredOpenAICompatProvider for the class that reads these entries,
9
+ * and providerRegistry.ts for the registration loop that consumes this
10
+ * array.
8
11
  *
9
12
  * The entries themselves are derived from the JSON catalog
10
13
  * (src/lib/providers/catalog/<id>.json) via buildCatalogEntries() — that
11
- * JSON is now the single source of truth for these 9 providers' identity,
14
+ * JSON is now the single source of truth for these 10 providers' identity,
12
15
  * models, error rules and wire config. See
13
16
  * src/lib/providers/catalog/loader.ts for the derivation logic and
14
17
  * docs/superpowers/plans/2026-08-28-provider-json-catalog-spec.md for the
15
18
  * authoring format.
16
19
  *
17
- * To add a new zero-quirk OpenAI-compatible provider: add one JSON file
18
- * under src/lib/providers/catalog/ (see provider-catalog.schema.json) and
19
- * run `pnpm run codegen:catalog`. Do NOT add a provider here if it needs
20
- * any hook override beyond the 3 mandatory ones
21
- * (getProviderName/getDefaultModel/formatProviderError) — write a
22
- * dedicated subclass instead (see deepseek.ts, azureOpenai.ts).
20
+ * To add a new OpenAI-compatible provider: add one JSON file under
21
+ * src/lib/providers/catalog/ (see provider-catalog.schema.json) and run
22
+ * `pnpm run codegen:catalog`. Do NOT add a provider here if it needs a real
23
+ * hook override beyond the 3 mandatory ones
24
+ * (getProviderName/getDefaultModel/formatProviderError) and the named
25
+ * catalog quirks above — write a dedicated subclass instead (see
26
+ * azureOpenai.ts).
23
27
  */
24
28
  export const OPENAI_COMPAT_CATALOG = buildCatalogEntries();
@@ -1,6 +1,7 @@
1
1
  import { VoyageModels } from "../constants/enums.js";
2
2
  import { BaseProvider } from "../core/baseProvider.js";
3
3
  import { isNeuroLink } from "../neurolink.js";
4
+ import { parseIndexedEmbeddingsResponse } from "./embeddingResponseParsing.js";
4
5
  import { createProxyFetch } from "../proxy/proxyFetch.js";
5
6
  import { AuthenticationError, InvalidModelError, ProviderError, RateLimitError, } from "../types/index.js";
6
7
  import { withTimeout } from "../utils/errorHandling.js";
@@ -146,24 +147,10 @@ export class VoyageProvider extends BaseProvider {
146
147
  throw this.formatProviderError(new Error(`Voyage embeddings failed: ${response.status} — ${text}`));
147
148
  }
148
149
  const data = (await response.json());
149
- if (!data.data || data.data.length === 0) {
150
- throw new ProviderError("Voyage embeddings response missing data", "voyage");
151
- }
152
- // Validate that the response covers all requested inputs.
153
- // Voyage may return partial results in edge-case error scenarios.
154
- if (data.data.length !== inputs.length) {
155
- throw new ProviderError(`Voyage embeddings response count mismatch: expected ${inputs.length}, got ${data.data.length}`, "voyage");
156
- }
157
- // Sort by index (Voyage returns out-of-order under some conditions)
158
- // and drop to a flat number[][] result.
159
- const sorted = data.data.slice().sort((a, b) => a.index - b.index);
160
- // Verify that index coverage is complete (0 … n-1).
161
- for (let i = 0; i < sorted.length; i++) {
162
- if (sorted[i].index !== i) {
163
- throw new ProviderError(`Voyage embeddings response has unexpected index ordering: position ${i} has index ${sorted[i].index}`, "voyage");
164
- }
165
- }
166
- return sorted.map((d) => d.embedding);
150
+ // Sorts by index (Voyage returns out-of-order under some conditions) and
151
+ // verifies complete 0..n-1 coverage (Voyage may return partial results
152
+ // in edge-case error scenarios).
153
+ return parseIndexedEmbeddingsResponse(data, inputs.length, "Voyage", (message) => new ProviderError(message, "voyage"));
167
154
  }
168
155
  async validateConfiguration() {
169
156
  return typeof this.apiKey === "string" && this.apiKey.trim().length > 0;
@@ -10,6 +10,18 @@ export type CatalogPricingPerMTok = {
10
10
  cachedInput?: number;
11
11
  };
12
12
  export type CatalogModelStatus = "production" | "preview" | "retired";
13
+ /**
14
+ * Descriptor-only per-provider turn-budget override — mirrors
15
+ * ProviderDescriptor.timeouts field-for-field (src/lib/types/providers.ts).
16
+ * Consumed only by buildCatalogDescriptor() (providerDescriptors.ts), never
17
+ * by the OpenAI-compat runtime path (loader.ts's buildCatalogEntries()).
18
+ * Optional and absent for every catalog provider except mistral, whose
19
+ * value preserves its pre-migration hand-typed descriptor exactly.
20
+ */
21
+ export type CatalogTimeouts = {
22
+ generateMs?: number;
23
+ streamMs?: number;
24
+ };
13
25
  export type CatalogModelSpec = {
14
26
  contextWindow?: number;
15
27
  maxOutputTokens?: number;
@@ -34,6 +46,11 @@ export type CatalogWire = {
34
46
  baseURL?: string;
35
47
  model?: string;
36
48
  };
49
+ /** Additional env vars validateApiKey() tries, in order, when the primary
50
+ * apiKeyEnvVar is unset (e.g. HuggingFace's HF_TOKEN alongside
51
+ * HUGGINGFACE_API_KEY). Consumed by buildCatalogConfigOptions() via
52
+ * ProviderConfigOptions.fallbackEnvVars. */
53
+ apiKeyFallbackEnvVars?: string[];
37
54
  };
38
55
  export type CatalogErrorRuleClass = "authentication" | "rate-limit" | "invalid-model" | "network" | "provider";
39
56
  export type CatalogErrorRuleJson = {
@@ -51,6 +68,11 @@ export type CatalogQuirks = {
51
68
  * ConfiguredOpenAICompatProvider so tool round-trips work. */
52
69
  messageContentFormat?: "string";
53
70
  registryDefaultIgnoresModelEnvVar?: boolean;
71
+ /** Vendor rejects `response_format: { type: "json_schema" }` outright but
72
+ * accepts `{ type: "json_object" }`. Normalized by
73
+ * ConfiguredOpenAICompatProvider so `generate({ schema })` keeps working
74
+ * (mirrors the pre-catalog `supportsStructuredOutputs: false` behavior). */
75
+ responseFormatDowngrade?: "json-schema-to-json-object";
54
76
  };
55
77
  export type CatalogBillingPolicy = "free-tier" | "free-with-card" | "no-free-tier";
56
78
  export type CatalogSetup = {
@@ -85,7 +107,14 @@ export type CatalogEvidence = {
85
107
  export type CatalogCapabilities = {
86
108
  text: boolean;
87
109
  streaming: boolean;
88
- tools: boolean;
110
+ /** "model-dependent" when tool support varies per served model and the
111
+ * vendor doesn't reject `tools` for unsupported ones (the model just
112
+ * never emits tool_calls) — e.g. HuggingFace's router. Maps to
113
+ * ProviderDescriptor.toolSupport's own "model-dependent" member and
114
+ * leaves OpenAICompatCatalogEntry.supportsTools unset so
115
+ * ConfiguredOpenAICompatProvider falls through to the model-registry
116
+ * default, exactly like an entry that never set supportsTools at all. */
117
+ tools: boolean | "model-dependent";
89
118
  toolsWithStreaming: boolean;
90
119
  structuredOutput: boolean;
91
120
  structuredOutputWithTools: boolean;
@@ -150,6 +179,22 @@ export type ProviderCatalogJson = {
150
179
  capabilities: CatalogCapabilities;
151
180
  errorRules: CatalogErrorRuleJson[];
152
181
  quirks?: CatalogQuirks;
182
+ /**
183
+ * Descriptor-only generate/stream turn-budget override — see
184
+ * CatalogTimeouts. Absent = buildCatalogDescriptor() omits
185
+ * ProviderDescriptor.timeouts entirely, matching every catalog provider's
186
+ * behavior before this field existed.
187
+ */
188
+ timeouts?: CatalogTimeouts;
189
+ /**
190
+ * Descriptor-only ascending auto-select priority override — mirrors
191
+ * ProviderDescriptor.autoSelectPriority field-for-field (lower = tried
192
+ * first in getBestProvider()'s fallback chain). Absent = not part of the
193
+ * auto-select chain, matching every catalog provider's behavior before
194
+ * this field existed (only mistral sets it today, preserving its
195
+ * pre-migration hand-typed value).
196
+ */
197
+ autoSelectPriority?: number;
153
198
  setup: CatalogSetup;
154
199
  evidence: CatalogEvidence;
155
200
  };
@@ -1,2 +1,2 @@
1
- export type CatalogProviderName = "api-route" | "baseten" | "cerebras" | "cloudflare" | "fireworks" | "friendli" | "gmicloud" | "groq" | "inception-labs" | "io-intelligence" | "mancer" | "mistral" | "perplexity" | "sambanova" | "together-ai" | "upstage" | "xai";
2
- export type CatalogCredentialKey = "apiRoute" | "baseten" | "cerebras" | "cloudflare" | "fireworks" | "friendli" | "gmicloud" | "groq" | "inceptionLabs" | "ioIntelligence" | "mancer" | "mistral" | "perplexity" | "sambanova" | "together" | "upstage" | "xai";
1
+ export type CatalogProviderName = "api-route" | "baseten" | "cerebras" | "cloudflare" | "deepseek" | "fireworks" | "friendli" | "gmicloud" | "groq" | "huggingface" | "inception-labs" | "io-intelligence" | "mancer" | "mistral" | "perplexity" | "sambanova" | "together-ai" | "upstage" | "xai";
2
+ export type CatalogCredentialKey = "apiRoute" | "baseten" | "cerebras" | "cloudflare" | "deepseek" | "fireworks" | "friendli" | "gmicloud" | "groq" | "huggingFace" | "inceptionLabs" | "ioIntelligence" | "mancer" | "mistral" | "perplexity" | "sambanova" | "together" | "upstage" | "xai";
@@ -143,10 +143,6 @@ export type NeurolinkCredentials = {
143
143
  apiVersion?: string;
144
144
  useMaxCompletionTokens?: boolean;
145
145
  };
146
- huggingFace?: {
147
- apiKey?: string;
148
- baseURL?: string;
149
- };
150
146
  openrouter?: {
151
147
  apiKey?: string;
152
148
  baseURL?: string;
@@ -163,10 +159,6 @@ export type NeurolinkCredentials = {
163
159
  baseURL?: string;
164
160
  apiKey?: string;
165
161
  };
166
- deepseek?: {
167
- apiKey?: string;
168
- baseURL?: string;
169
- };
170
162
  nvidiaNim?: {
171
163
  apiKey?: string;
172
164
  baseURL?: string;
@@ -196,6 +188,10 @@ export type NeurolinkCredentials = {
196
188
  baseURL?: string;
197
189
  accountId?: string;
198
190
  };
191
+ deepseek?: {
192
+ apiKey?: string;
193
+ baseURL?: string;
194
+ };
199
195
  fireworks?: {
200
196
  apiKey?: string;
201
197
  baseURL?: string;
@@ -212,6 +208,10 @@ export type NeurolinkCredentials = {
212
208
  apiKey?: string;
213
209
  baseURL?: string;
214
210
  };
211
+ huggingFace?: {
212
+ apiKey?: string;
213
+ baseURL?: string;
214
+ };
215
215
  inceptionLabs?: {
216
216
  apiKey?: string;
217
217
  baseURL?: string;
@@ -771,6 +771,10 @@ export type OpenAICompatCatalogEntry = {
771
771
  /** See CatalogQuirks.messageContentFormat — a vendor that accepts
772
772
  * `messages[].content` only as a plain string. */
773
773
  messageContentFormat?: "string";
774
+ /** See CatalogQuirks.responseFormatDowngrade — a vendor that rejects
775
+ * `response_format: { type: "json_schema" }` but accepts
776
+ * `{ type: "json_object" }`. */
777
+ responseFormatDowngrade?: "json-schema-to-json-object";
774
778
  };
775
779
  /** The subset of OpenAICompatCatalogEntry that resolveOpenAICompatConfig()
776
780
  * needs — lets call sites pass a minimal object without the full catalog
@@ -7,9 +7,9 @@ import type { CatalogProviderName, ModelChoice } from "../types/index.js";
7
7
  /**
8
8
  * Default models per provider (first choice/recommended).
9
9
  *
10
- * Covers every provider EXCEPT the 9 JSON-catalog providers — those default
10
+ * Covers every provider EXCEPT the 11 JSON-catalog providers — those default
11
11
  * models come from `models.default` in the catalog JSON, read directly by
12
- * `getDefaultModel()` below. (Two of the 9 — cerebras, sambanova — never had
12
+ * `getDefaultModel()` below. (Two of the 11 — cerebras, sambanova — never had
13
13
  * a hand entry here at all; `getDefaultModel()` now resolves them too.)
14
14
  *
15
15
  * AUTO is also excluded — it never had an entry here either (matches
@@ -2,10 +2,10 @@
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, HuggingFaceModels, SageMakerModels, OpenRouterModels, DeepSeekModels, NvidiaNimModels, CohereModels, VoyageModels, JinaModels, StabilityModels, IdeogramModels, RecraftModels, TypeSafeModels, ReplicateModels, } from "../constants/enums.js";
5
+ import { AIProviderName, OpenAIModels, AnthropicModels, GoogleAIModels, BedrockModels, VertexModels, OllamaModels, AzureOpenAIModels, LiteLLMModels, SageMakerModels, OpenRouterModels, NvidiaNimModels, CohereModels, VoyageModels, JinaModels, StabilityModels, IdeogramModels, RecraftModels, TypeSafeModels, ReplicateModels, } from "../constants/enums.js";
6
6
  import { getCatalogJsonEntries } from "../providers/catalog/loader.js";
7
7
  /**
8
- * Looks up the JSON catalog entry for a provider, if it is one of the 9
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
11
  * from stale hand-written duplicates.
@@ -36,11 +36,11 @@ function catalogTopModels(entry) {
36
36
  * Top models per provider with descriptions for CLI prompts
37
37
  * These are curated lists of the most commonly used/recommended models
38
38
  *
39
- * Covers every provider EXCEPT the 9 JSON-catalog providers (cerebras,
40
- * cloudflare, fireworks, groq, mistral, perplexity, sambanova, together-ai,
41
- * xai) — their entries are derived from `models.catalog[*].description` by
42
- * `catalogTopModels()` and merged in at the accessor-function level below,
43
- * never hand-duplicated here.
39
+ * Covers every provider EXCEPT the 11 JSON-catalog providers (cerebras,
40
+ * cloudflare, deepseek, fireworks, groq, huggingface, mistral, perplexity,
41
+ * sambanova, together-ai, xai) — their entries are derived from
42
+ * `models.catalog[*].description` by `catalogTopModels()` and merged in at
43
+ * the accessor-function level below, never hand-duplicated here.
44
44
  */
45
45
  const TOP_MODELS_CONFIG = {
46
46
  [AIProviderName.OPENAI]: [
@@ -194,23 +194,6 @@ const TOP_MODELS_CONFIG = {
194
194
  description: "Vertex via LiteLLM",
195
195
  },
196
196
  ],
197
- [AIProviderName.HUGGINGFACE]: [
198
- {
199
- model: HuggingFaceModels.LLAMA_3_3_70B_INSTRUCT,
200
- description: "Recommended - Latest Llama",
201
- },
202
- {
203
- model: HuggingFaceModels.MISTRAL_LARGE_3_675B,
204
- description: "Mistral Large",
205
- },
206
- { model: HuggingFaceModels.DEEPSEEK_R1, description: "Advanced reasoning" },
207
- {
208
- model: HuggingFaceModels.QWEN_2_5_72B_INSTRUCT,
209
- description: "Qwen flagship",
210
- },
211
- { model: HuggingFaceModels.PHI_4, description: "Microsoft Phi-4" },
212
- { model: HuggingFaceModels.GEMMA_3_27B_IT, description: "Google Gemma 3" },
213
- ],
214
197
  [AIProviderName.SAGEMAKER]: [
215
198
  {
216
199
  model: SageMakerModels.LLAMA_4_MAVERICK_17B_128E,
@@ -245,13 +228,6 @@ const TOP_MODELS_CONFIG = {
245
228
  { model: "gpt-4-turbo", description: "Turbo compatible model" },
246
229
  { model: "gpt-3.5-turbo", description: "Legacy compatible model" },
247
230
  ],
248
- [AIProviderName.DEEPSEEK]: [
249
- { model: "deepseek-chat", description: "DeepSeek-V3 general chat" },
250
- {
251
- model: "deepseek-reasoner",
252
- description: "DeepSeek-R1 reasoning (slower, deeper)",
253
- },
254
- ],
255
231
  [AIProviderName.NVIDIA_NIM]: [
256
232
  {
257
233
  model: "meta/llama-3.3-70b-instruct",
@@ -408,9 +384,9 @@ const TOP_MODELS_CONFIG = {
408
384
  /**
409
385
  * Default models per provider (first choice/recommended).
410
386
  *
411
- * Covers every provider EXCEPT the 9 JSON-catalog providers — those default
387
+ * Covers every provider EXCEPT the 11 JSON-catalog providers — those default
412
388
  * models come from `models.default` in the catalog JSON, read directly by
413
- * `getDefaultModel()` below. (Two of the 9 — cerebras, sambanova — never had
389
+ * `getDefaultModel()` below. (Two of the 11 — cerebras, sambanova — never had
414
390
  * a hand entry here at all; `getDefaultModel()` now resolves them too.)
415
391
  *
416
392
  * AUTO is also excluded — it never had an entry here either (matches
@@ -425,11 +401,9 @@ export const DEFAULT_MODELS = {
425
401
  [AIProviderName.AZURE]: AzureOpenAIModels.GPT_4O,
426
402
  [AIProviderName.OLLAMA]: OllamaModels.LLAMA4_LATEST,
427
403
  [AIProviderName.LITELLM]: LiteLLMModels.OPENAI_GPT_4O,
428
- [AIProviderName.HUGGINGFACE]: HuggingFaceModels.LLAMA_3_3_70B_INSTRUCT,
429
404
  [AIProviderName.SAGEMAKER]: SageMakerModels.LLAMA_4_MAVERICK_17B_128E,
430
405
  [AIProviderName.OPENROUTER]: OpenRouterModels.CLAUDE_SONNET_4_5,
431
406
  [AIProviderName.OPENAI_COMPATIBLE]: "gpt-4o",
432
- [AIProviderName.DEEPSEEK]: DeepSeekModels.DEEPSEEK_CHAT,
433
407
  [AIProviderName.NVIDIA_NIM]: NvidiaNimModels.GPT_OSS_20B,
434
408
  // LM Studio + llama.cpp auto-discover their loaded model from /v1/models;
435
409
  // an empty default is the documented signal to use that path.
@@ -447,7 +421,7 @@ export const DEFAULT_MODELS = {
447
421
  /**
448
422
  * Model enum mappings for getAllModels.
449
423
  *
450
- * Covers every provider EXCEPT the 9 JSON-catalog providers — those model
424
+ * Covers every provider EXCEPT the 11 JSON-catalog providers — those model
451
425
  * lists come from `Object.keys(models.catalog)` in the catalog JSON, read
452
426
  * directly by `getAllModels()` below.
453
427
  */
@@ -460,11 +434,9 @@ const MODEL_ENUMS = {
460
434
  [AIProviderName.AZURE]: AzureOpenAIModels,
461
435
  [AIProviderName.OLLAMA]: OllamaModels,
462
436
  [AIProviderName.LITELLM]: LiteLLMModels,
463
- [AIProviderName.HUGGINGFACE]: HuggingFaceModels,
464
437
  [AIProviderName.SAGEMAKER]: SageMakerModels,
465
438
  [AIProviderName.OPENROUTER]: OpenRouterModels,
466
439
  [AIProviderName.OPENAI_COMPATIBLE]: null,
467
- [AIProviderName.DEEPSEEK]: DeepSeekModels,
468
440
  [AIProviderName.NVIDIA_NIM]: NvidiaNimModels,
469
441
  [AIProviderName.LM_STUDIO]: null,
470
442
  [AIProviderName.LLAMACPP]: null,
@@ -2,9 +2,9 @@
2
2
  * Size Guard Utility
3
3
  *
4
4
  * Provides bounded binary downloads to prevent OOM when fetching generated
5
- * media from external providers. Applies a Content-Length pre-check and a
6
- * post-buffer guard so multi-GB responses are rejected before they fully
7
- * materialise in process memory.
5
+ * media from external providers. Applies a Content-Length pre-check and
6
+ * counts bytes while streaming, so multi-GB responses are rejected before
7
+ * they fully materialise in process memory.
8
8
  *
9
9
  * @module utils/sizeGuard
10
10
  */
@@ -21,8 +21,11 @@ export declare const MAX_IMAGE_BYTES: number;
21
21
  * Two checks are performed:
22
22
  * 1. If the response includes a `Content-Length` header that exceeds
23
23
  * `maxBytes`, the download is rejected immediately (no data is read).
24
- * 2. After buffering, the actual buffer size is verified against `maxBytes`.
25
- * This catches chunked transfers where no `Content-Length` was provided.
24
+ * 2. The body is read as a stream and the reader is cancelled as soon as
25
+ * more than `maxBytes` arrive. This catches chunked transfers where no
26
+ * `Content-Length` was provided (or it understated the body) without
27
+ * buffering the excess. A response without a readable stream falls back
28
+ * to buffering and checking afterwards.
26
29
  *
27
30
  * @param response The fetch {@link Response} to drain.
28
31
  * @param maxBytes Maximum number of bytes allowed.
@@ -2,9 +2,9 @@
2
2
  * Size Guard Utility
3
3
  *
4
4
  * Provides bounded binary downloads to prevent OOM when fetching generated
5
- * media from external providers. Applies a Content-Length pre-check and a
6
- * post-buffer guard so multi-GB responses are rejected before they fully
7
- * materialise in process memory.
5
+ * media from external providers. Applies a Content-Length pre-check and
6
+ * counts bytes while streaming, so multi-GB responses are rejected before
7
+ * they fully materialise in process memory.
8
8
  *
9
9
  * @module utils/sizeGuard
10
10
  */
@@ -21,8 +21,11 @@ export const MAX_IMAGE_BYTES = 25 * 1024 * 1024;
21
21
  * Two checks are performed:
22
22
  * 1. If the response includes a `Content-Length` header that exceeds
23
23
  * `maxBytes`, the download is rejected immediately (no data is read).
24
- * 2. After buffering, the actual buffer size is verified against `maxBytes`.
25
- * This catches chunked transfers where no `Content-Length` was provided.
24
+ * 2. The body is read as a stream and the reader is cancelled as soon as
25
+ * more than `maxBytes` arrive. This catches chunked transfers where no
26
+ * `Content-Length` was provided (or it understated the body) without
27
+ * buffering the excess. A response without a readable stream falls back
28
+ * to buffering and checking afterwards.
26
29
  *
27
30
  * @param response The fetch {@link Response} to drain.
28
31
  * @param maxBytes Maximum number of bytes allowed.
@@ -36,9 +39,29 @@ export async function readBoundedBuffer(response, maxBytes, label) {
36
39
  if (contentLength > 0 && contentLength > maxBytes) {
37
40
  throw new Error(`${label} download too large: ${contentLength} bytes (max ${maxBytes})`);
38
41
  }
39
- const buffer = Buffer.from(await response.arrayBuffer());
40
- if (buffer.length > maxBytes) {
41
- throw new Error(`${label} download exceeded size cap after fetch: ${buffer.length} bytes (max ${maxBytes})`);
42
+ const exceeded = (bytes) => new Error(`${label} download exceeded size cap after fetch: ${bytes} bytes (max ${maxBytes})`);
43
+ const body = response.body;
44
+ if (!body || typeof body.getReader !== "function") {
45
+ const buffer = Buffer.from(await response.arrayBuffer());
46
+ if (buffer.length > maxBytes) {
47
+ throw exceeded(buffer.length);
48
+ }
49
+ return buffer;
42
50
  }
43
- return buffer;
51
+ const reader = body.getReader();
52
+ const chunks = [];
53
+ let received = 0;
54
+ for (;;) {
55
+ const { done, value } = await reader.read();
56
+ if (done) {
57
+ break;
58
+ }
59
+ received += value.byteLength;
60
+ if (received > maxBytes) {
61
+ await reader.cancel().catch(() => undefined);
62
+ throw exceeded(received);
63
+ }
64
+ chunks.push(value);
65
+ }
66
+ return Buffer.concat(chunks, received);
44
67
  }