@juspay/neurolink 10.3.0 → 10.3.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 (68) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/browser/neurolink.min.js +420 -417
  3. package/dist/cli/loop/optionsSchema.js +4 -0
  4. package/dist/constants/contextWindows.d.ts +24 -0
  5. package/dist/constants/contextWindows.js +50 -0
  6. package/dist/context/errorDetection.d.ts +6 -0
  7. package/dist/context/errorDetection.js +18 -0
  8. package/dist/context/stepBudgetGuard.d.ts +17 -3
  9. package/dist/context/stepBudgetGuard.js +53 -14
  10. package/dist/core/baseProvider.d.ts +12 -0
  11. package/dist/core/baseProvider.js +18 -0
  12. package/dist/core/modules/GenerationHandler.js +208 -42
  13. package/dist/core/modules/ToolsManager.d.ts +17 -0
  14. package/dist/core/modules/ToolsManager.js +99 -1
  15. package/dist/lib/constants/contextWindows.d.ts +24 -0
  16. package/dist/lib/constants/contextWindows.js +50 -0
  17. package/dist/lib/context/errorDetection.d.ts +6 -0
  18. package/dist/lib/context/errorDetection.js +18 -0
  19. package/dist/lib/context/stepBudgetGuard.d.ts +17 -3
  20. package/dist/lib/context/stepBudgetGuard.js +53 -14
  21. package/dist/lib/core/baseProvider.d.ts +12 -0
  22. package/dist/lib/core/baseProvider.js +18 -0
  23. package/dist/lib/core/modules/GenerationHandler.js +208 -42
  24. package/dist/lib/core/modules/ToolsManager.d.ts +17 -0
  25. package/dist/lib/core/modules/ToolsManager.js +99 -1
  26. package/dist/lib/mcp/toolDiscoveryService.js +59 -20
  27. package/dist/lib/neurolink.js +30 -9
  28. package/dist/lib/providers/litellm.d.ts +27 -19
  29. package/dist/lib/providers/litellm.js +171 -92
  30. package/dist/lib/providers/openaiChatCompletionsBase.d.ts +37 -1
  31. package/dist/lib/providers/openaiChatCompletionsBase.js +201 -33
  32. package/dist/lib/providers/openaiChatCompletionsClient.d.ts +23 -5
  33. package/dist/lib/providers/openaiChatCompletionsClient.js +94 -14
  34. package/dist/lib/proxy/proxyFetch.d.ts +17 -0
  35. package/dist/lib/proxy/proxyFetch.js +42 -4
  36. package/dist/lib/types/generate.d.ts +18 -2
  37. package/dist/lib/types/openaiCompatible.d.ts +2 -0
  38. package/dist/lib/types/providers.d.ts +9 -0
  39. package/dist/lib/utils/errorHandling.js +8 -2
  40. package/dist/lib/utils/schemaConversion.d.ts +16 -0
  41. package/dist/lib/utils/schemaConversion.js +165 -8
  42. package/dist/lib/utils/timeout.d.ts +22 -0
  43. package/dist/lib/utils/timeout.js +72 -12
  44. package/dist/lib/utils/tokenLimits.js +22 -0
  45. package/dist/lib/utils/toolCallRepair.d.ts +8 -0
  46. package/dist/lib/utils/toolCallRepair.js +4 -1
  47. package/dist/mcp/toolDiscoveryService.js +59 -20
  48. package/dist/neurolink.js +30 -9
  49. package/dist/providers/litellm.d.ts +27 -19
  50. package/dist/providers/litellm.js +171 -92
  51. package/dist/providers/openaiChatCompletionsBase.d.ts +37 -1
  52. package/dist/providers/openaiChatCompletionsBase.js +201 -33
  53. package/dist/providers/openaiChatCompletionsClient.d.ts +23 -5
  54. package/dist/providers/openaiChatCompletionsClient.js +94 -14
  55. package/dist/proxy/proxyFetch.d.ts +17 -0
  56. package/dist/proxy/proxyFetch.js +42 -4
  57. package/dist/types/generate.d.ts +18 -2
  58. package/dist/types/openaiCompatible.d.ts +2 -0
  59. package/dist/types/providers.d.ts +9 -0
  60. package/dist/utils/errorHandling.js +8 -2
  61. package/dist/utils/schemaConversion.d.ts +16 -0
  62. package/dist/utils/schemaConversion.js +165 -8
  63. package/dist/utils/timeout.d.ts +22 -0
  64. package/dist/utils/timeout.js +72 -12
  65. package/dist/utils/tokenLimits.js +22 -0
  66. package/dist/utils/toolCallRepair.d.ts +8 -0
  67. package/dist/utils/toolCallRepair.js +4 -1
  68. package/package.json +3 -1
@@ -118,5 +118,9 @@ export const textGenerationOptionsSchema = {
118
118
  type: "boolean",
119
119
  description: "Disable tool result caching for this request (overrides global mcp.cache.enabled).",
120
120
  },
121
+ disableToolCallRepair: {
122
+ type: "boolean",
123
+ description: "Disable the schema-driven tool call repair mechanism (near-miss tool names, mis-typed arguments). Repair is enabled by default.",
124
+ },
121
125
  };
122
126
  //# sourceMappingURL=optionsSchema.js.map
@@ -30,6 +30,30 @@ export declare const MODEL_CONTEXT_WINDOWS: Record<string, Record<string, number
30
30
  export declare function registerRuntimeContextWindow(provider: string, model: string, contextWindow: number): void;
31
31
  /** Test hook: clear runtime-discovered windows (state is module-global). */
32
32
  export declare function clearRuntimeContextWindows(): void;
33
+ /**
34
+ * Runtime-discovered window for an exact provider/model pair, or undefined.
35
+ *
36
+ * Unlike {@link getContextWindowSize} this NEVER falls back to static table
37
+ * values. Callers use it to distinguish "the serving infrastructure told us
38
+ * the real window" (safe to hard-enforce: clamp max_tokens, fail fast) from
39
+ * "static guess" (advisory only — hard-enforcing a guessed window would
40
+ * falsely reject requests that the real deployment accepts).
41
+ */
42
+ export declare function getRuntimeContextWindow(provider: string, model?: string): number | undefined;
43
+ /**
44
+ * Register a runtime-discovered output-token ceiling for a provider/model
45
+ * pair. Later registrations overwrite earlier ones (rediscovery refreshes
46
+ * values). Non-positive/non-finite ceilings are ignored so a malformed
47
+ * discovery source can never shrink an output budget to zero.
48
+ */
49
+ export declare function registerRuntimeOutputCeiling(provider: string, model: string, maxOutputTokens: number): void;
50
+ /**
51
+ * Runtime-discovered output ceiling for an exact provider/model pair, or
52
+ * undefined when the serving infrastructure has not advertised one.
53
+ */
54
+ export declare function getRuntimeOutputCeiling(provider: string, model?: string): number | undefined;
55
+ /** Test hook: clear runtime-discovered output ceilings (state is module-global). */
56
+ export declare function clearRuntimeOutputCeilings(): void;
33
57
  /**
34
58
  * Resolve context window size for a provider/model combination.
35
59
  *
@@ -420,6 +420,56 @@ export function registerRuntimeContextWindow(provider, model, contextWindow) {
420
420
  export function clearRuntimeContextWindows() {
421
421
  RUNTIME_CONTEXT_WINDOWS.clear();
422
422
  }
423
+ /**
424
+ * Runtime-discovered window for an exact provider/model pair, or undefined.
425
+ *
426
+ * Unlike {@link getContextWindowSize} this NEVER falls back to static table
427
+ * values. Callers use it to distinguish "the serving infrastructure told us
428
+ * the real window" (safe to hard-enforce: clamp max_tokens, fail fast) from
429
+ * "static guess" (advisory only — hard-enforcing a guessed window would
430
+ * falsely reject requests that the real deployment accepts).
431
+ */
432
+ export function getRuntimeContextWindow(provider, model) {
433
+ if (!model) {
434
+ return undefined;
435
+ }
436
+ return RUNTIME_CONTEXT_WINDOWS.get(`${provider}:${model}`);
437
+ }
438
+ /**
439
+ * Runtime-discovered output-token ceilings, keyed `${provider}:${model}` —
440
+ * the `max_output_tokens` the serving infrastructure advertises for a model
441
+ * (e.g. LiteLLM `/model/info`). Same async-populate/sync-read contract as
442
+ * {@link registerRuntimeContextWindow}. Consumed by `getSafeMaxTokens` so
443
+ * requested maxTokens is clamped to the deployed model's real cap instead of
444
+ * a static per-provider table value.
445
+ */
446
+ const RUNTIME_OUTPUT_CEILINGS = new Map();
447
+ /**
448
+ * Register a runtime-discovered output-token ceiling for a provider/model
449
+ * pair. Later registrations overwrite earlier ones (rediscovery refreshes
450
+ * values). Non-positive/non-finite ceilings are ignored so a malformed
451
+ * discovery source can never shrink an output budget to zero.
452
+ */
453
+ export function registerRuntimeOutputCeiling(provider, model, maxOutputTokens) {
454
+ if (!Number.isFinite(maxOutputTokens) || maxOutputTokens <= 0) {
455
+ return;
456
+ }
457
+ RUNTIME_OUTPUT_CEILINGS.set(`${provider}:${model}`, maxOutputTokens);
458
+ }
459
+ /**
460
+ * Runtime-discovered output ceiling for an exact provider/model pair, or
461
+ * undefined when the serving infrastructure has not advertised one.
462
+ */
463
+ export function getRuntimeOutputCeiling(provider, model) {
464
+ if (!model) {
465
+ return undefined;
466
+ }
467
+ return RUNTIME_OUTPUT_CEILINGS.get(`${provider}:${model}`);
468
+ }
469
+ /** Test hook: clear runtime-discovered output ceilings (state is module-global). */
470
+ export function clearRuntimeOutputCeilings() {
471
+ RUNTIME_OUTPUT_CEILINGS.clear();
472
+ }
423
473
  /**
424
474
  * Resolve context window size for a provider/model combination.
425
475
  *
@@ -23,6 +23,12 @@ export declare function getContextOverflowProvider(error: unknown): string | nul
23
23
  export declare function parseProviderOverflowDetails(error: unknown): {
24
24
  actualTokens: number;
25
25
  budgetTokens: number;
26
+ /**
27
+ * Output tokens the rejected request asked for, when the message states
28
+ * them separately (vllm/LiteLLM phrasing). Lets recovery re-fit
29
+ * max_tokens instead of shrinking the input.
30
+ */
31
+ requestedOutputTokens?: number;
26
32
  } | null;
27
33
  /**
28
34
  * Extract error message from various error formats.
@@ -113,6 +113,24 @@ export function parseProviderOverflowDetails(error) {
113
113
  budgetTokens: parseInt(openaiMax[1].replace(/,/g, ""), 10),
114
114
  };
115
115
  }
116
+ // vllm / LiteLLM-proxied backends: "This model's maximum context length is
117
+ // N tokens. However, you requested X output tokens and your prompt contains
118
+ // at least Y input tokens..." — input and requested-output are stated
119
+ // separately (no "resulted in"), so recovery can re-fit max_tokens to
120
+ // N − Y instead of shrinking the input.
121
+ const vllmInput = message.match(/prompt\s+contains\s+at\s+least\s+(\d[\d,]{0,19})\s+input\s+tokens/i);
122
+ if (vllmInput && openaiMax) {
123
+ const requestedOutput = message.match(/requested\s+(\d[\d,]{0,19})\s+output\s+tokens/i);
124
+ return {
125
+ actualTokens: parseInt(vllmInput[1].replace(/,/g, ""), 10),
126
+ budgetTokens: parseInt(openaiMax[1].replace(/,/g, ""), 10),
127
+ ...(requestedOutput
128
+ ? {
129
+ requestedOutputTokens: parseInt(requestedOutput[1].replace(/,/g, ""), 10),
130
+ }
131
+ : {}),
132
+ };
133
+ }
116
134
  // Anthropic pattern: "X tokens > Y token limit" or "X tokens, limit Y"
117
135
  // Use single character-class number groups to prevent ReDoS (CodeQL: js/polynomial-redos)
118
136
  const anthropicMatch = message.match(/(\d[\d,]{0,19})\s*tokens?\s*[>:]\s*(\d[\d,]{0,19})/i);
@@ -41,7 +41,21 @@ export declare function estimateStepMessagesTokens(messages: readonly ModelMessa
41
41
  export declare function estimateFixedOverheadTokens(system: unknown, tools: Record<string, unknown> | undefined, provider?: string): number;
42
42
  /**
43
43
  * Create a per-step budget guard. Returns a function that, given the step's
44
- * messages, returns a compacted replacement array when the projected request
45
- * exceeds the threshold or `undefined` when no change is needed.
44
+ * messages (and optionally the REAL input-token count the provider reported
45
+ * for the previous step), returns a compacted replacement array when the
46
+ * projected request exceeds the threshold — or `undefined` when no change is
47
+ * needed.
48
+ *
49
+ * Two dynamic behaviours:
50
+ * - the available-input budget is re-resolved on EVERY invocation, so
51
+ * runtime window discovery (`/model/info`, overflow self-healing) that
52
+ * lands mid-loop takes effect immediately instead of the guard staying
53
+ * frozen on the value captured at loop start;
54
+ * - usage feedback calibrates the estimator: the ratio between the real
55
+ * prompt tokens of the previous step and this guard's own estimate for
56
+ * what that step sent scales later estimates (only UP — underestimates
57
+ * overflow, overestimates merely compact earlier), eliminating the
58
+ * char-based estimator's drift on dense code/diff content without
59
+ * shipping a tokenizer.
46
60
  */
47
- export declare function createStepBudgetGuard(config: StepBudgetGuardConfig): (messages: readonly ModelMessage[]) => ModelMessage[] | undefined;
61
+ export declare function createStepBudgetGuard(config: StepBudgetGuardConfig): (messages: readonly ModelMessage[], observedInputTokensLastStep?: number) => ModelMessage[] | undefined;
@@ -37,6 +37,13 @@ import { generateToolOutputPreview } from "./toolOutputLimits.js";
37
37
  import { logger } from "../utils/logger.js";
38
38
  /** Estimated tokens for a tool definition that fails to serialize. */
39
39
  const TOKENS_PER_TOOL_DEFINITION = 200;
40
+ /**
41
+ * Upper bound on the usage-feedback calibration ratio. Real tokenizers count
42
+ * dense code/diff content at up to ~1.3× the char-based estimate; anything
43
+ * far beyond that indicates inconsistent provider usage reporting, and an
44
+ * unbounded ratio would compact the loop into uselessness.
45
+ */
46
+ const MAX_CALIBRATION_RATIO = 3;
40
47
  /** Messages at the end of the conversation the guard never modifies. */
41
48
  const PROTECTED_TAIL_MESSAGES = 4;
42
49
  /** Stage-1 preview budget for an old tool output (bytes). */
@@ -230,19 +237,45 @@ function dropOldestToolExchanges(messages, budgetTokens, fixedOverheadTokens, pr
230
237
  }
231
238
  /**
232
239
  * Create a per-step budget guard. Returns a function that, given the step's
233
- * messages, returns a compacted replacement array when the projected request
234
- * exceeds the threshold or `undefined` when no change is needed.
240
+ * messages (and optionally the REAL input-token count the provider reported
241
+ * for the previous step), returns a compacted replacement array when the
242
+ * projected request exceeds the threshold — or `undefined` when no change is
243
+ * needed.
244
+ *
245
+ * Two dynamic behaviours:
246
+ * - the available-input budget is re-resolved on EVERY invocation, so
247
+ * runtime window discovery (`/model/info`, overflow self-healing) that
248
+ * lands mid-loop takes effect immediately instead of the guard staying
249
+ * frozen on the value captured at loop start;
250
+ * - usage feedback calibrates the estimator: the ratio between the real
251
+ * prompt tokens of the previous step and this guard's own estimate for
252
+ * what that step sent scales later estimates (only UP — underestimates
253
+ * overflow, overestimates merely compact earlier), eliminating the
254
+ * char-based estimator's drift on dense code/diff content without
255
+ * shipping a tokenizer.
235
256
  */
236
257
  export function createStepBudgetGuard(config) {
237
258
  const { provider, model, maxTokens, fixedOverheadTokens = 0, getFixedOverheadTokens, thresholdRatio = DEFAULT_CONTEXT_GUARD_RATIO, } = config;
238
- const availableInput = getAvailableInputTokens(provider, model, maxTokens);
239
- const thresholdTokens = Math.floor(availableInput * thresholdRatio);
240
- return function guardStepMessages(messages) {
259
+ // Calibration state: raw estimate for the messages the PREVIOUS guard
260
+ // invocation let through (what was actually sent), and the current ratio.
261
+ let lastRawEstimate = 0;
262
+ let calibration = 1;
263
+ return function guardStepMessages(messages, observedInputTokensLastStep) {
264
+ const availableInput = getAvailableInputTokens(provider, model, maxTokens);
265
+ const thresholdTokens = Math.floor(availableInput * thresholdRatio);
241
266
  // Resolve overhead per invocation: the tool set can GROW mid-loop
242
267
  // (search_tools hydration adds discovered tools between steps), so a
243
268
  // once-captured value would undercount later steps.
244
269
  const overheadTokens = getFixedOverheadTokens?.() ?? fixedOverheadTokens;
245
- const estimate = overheadTokens + estimateStepMessagesTokens(messages, provider);
270
+ const rawEstimate = overheadTokens + estimateStepMessagesTokens(messages, provider);
271
+ if (observedInputTokensLastStep !== undefined &&
272
+ observedInputTokensLastStep > 0 &&
273
+ lastRawEstimate > 0) {
274
+ calibration = Math.min(MAX_CALIBRATION_RATIO, Math.max(1, observedInputTokensLastStep / lastRawEstimate));
275
+ }
276
+ // Apply calibration to the THRESHOLD instead of every estimate so the
277
+ // compaction stages keep operating on raw numbers.
278
+ const effectiveThreshold = Math.floor(thresholdTokens / calibration);
246
279
  // Logger Guard: per-step diagnostics for debugging why a long run does
247
280
  // (or does not) trigger compaction — gated so nothing is serialized when
248
281
  // debug logging is off.
@@ -251,12 +284,15 @@ export function createStepBudgetGuard(config) {
251
284
  provider,
252
285
  model,
253
286
  messageCount: messages.length,
254
- estimatedTokens: estimate,
255
- thresholdTokens,
256
- willCompact: estimate > thresholdTokens,
287
+ estimatedTokens: rawEstimate,
288
+ thresholdTokens: effectiveThreshold,
289
+ calibration,
290
+ observedInputTokensLastStep,
291
+ willCompact: rawEstimate > effectiveThreshold,
257
292
  });
258
293
  }
259
- if (estimate <= thresholdTokens) {
294
+ if (rawEstimate <= effectiveThreshold) {
295
+ lastRawEstimate = rawEstimate;
260
296
  return undefined;
261
297
  }
262
298
  // Stage 1: shrink old tool outputs to previews.
@@ -265,25 +301,28 @@ export function createStepBudgetGuard(config) {
265
301
  let newEstimate = overheadTokens + estimateStepMessagesTokens(compacted, provider);
266
302
  // Stage 2: drop oldest complete tool exchanges if still over.
267
303
  let droppedExchanges = 0;
268
- if (newEstimate > thresholdTokens) {
269
- const stage2 = dropOldestToolExchanges(compacted, thresholdTokens, overheadTokens, provider);
304
+ if (newEstimate > effectiveThreshold) {
305
+ const stage2 = dropOldestToolExchanges(compacted, effectiveThreshold, overheadTokens, provider);
270
306
  compacted = stage2.messages;
271
307
  droppedExchanges = stage2.droppedExchanges;
272
308
  newEstimate =
273
309
  overheadTokens + estimateStepMessagesTokens(compacted, provider);
274
310
  }
275
311
  if (stage1.truncated === 0 && droppedExchanges === 0) {
312
+ lastRawEstimate = rawEstimate;
276
313
  return undefined; // nothing actionable (already all-protected)
277
314
  }
278
315
  logger.info("[StepBudgetGuard] Compacted agent-loop step messages", {
279
316
  provider,
280
317
  model,
281
- estimatedTokens: estimate,
282
- thresholdTokens,
318
+ estimatedTokens: rawEstimate,
319
+ thresholdTokens: effectiveThreshold,
320
+ calibration,
283
321
  afterTokens: newEstimate,
284
322
  toolOutputsTruncated: stage1.truncated,
285
323
  exchangesDropped: droppedExchanges,
286
324
  });
325
+ lastRawEstimate = newEstimate;
287
326
  return compacted;
288
327
  };
289
328
  }
@@ -185,6 +185,18 @@ export declare abstract class BaseProvider implements AIProvider {
185
185
  * IMPLEMENTATION NOTE: Uses streamText() under the hood and accumulates results
186
186
  * for consistency and better performance
187
187
  */
188
+ /**
189
+ * Ensure runtime-discovered model limits (context window, output-token
190
+ * ceiling) are registered before any budget math runs. Generation
191
+ * pipelines await this BEFORE `checkContextBudget`, and generate()/stream()
192
+ * await it before options normalization so `getSafeMaxTokens` sees the
193
+ * discovered output ceiling.
194
+ *
195
+ * Default no-op. Providers with a runtime discovery source override it
196
+ * (e.g. LiteLLM's `/model/info`). Implementations must NEVER reject —
197
+ * discovery failure degrades to the static defaults.
198
+ */
199
+ ensureModelLimits(): Promise<void>;
188
200
  generate(optionsOrPrompt: TextGenerationOptions | string, _analysisSchema?: ValidationSchema): Promise<EnhancedGenerateResult | null>;
189
201
  /**
190
202
  * Alias for generate method - implements AIProvider interface
@@ -118,6 +118,9 @@ export class BaseProvider {
118
118
  * When tools are involved, falls back to generate() with synthetic streaming
119
119
  */
120
120
  async stream(optionsOrPrompt, analysisSchema) {
121
+ // Runtime model limits must land before normalizeStreamOptions resolves
122
+ // maxTokens (getSafeMaxTokens consults the discovered output ceiling).
123
+ await this.ensureModelLimits();
121
124
  let options = this.normalizeStreamOptions(optionsOrPrompt);
122
125
  logger.info(`Starting stream`, {
123
126
  provider: this.providerName,
@@ -855,7 +858,22 @@ export class BaseProvider {
855
858
  * IMPLEMENTATION NOTE: Uses streamText() under the hood and accumulates results
856
859
  * for consistency and better performance
857
860
  */
861
+ /**
862
+ * Ensure runtime-discovered model limits (context window, output-token
863
+ * ceiling) are registered before any budget math runs. Generation
864
+ * pipelines await this BEFORE `checkContextBudget`, and generate()/stream()
865
+ * await it before options normalization so `getSafeMaxTokens` sees the
866
+ * discovered output ceiling.
867
+ *
868
+ * Default no-op. Providers with a runtime discovery source override it
869
+ * (e.g. LiteLLM's `/model/info`). Implementations must NEVER reject —
870
+ * discovery failure degrades to the static defaults.
871
+ */
872
+ async ensureModelLimits() { }
858
873
  async generate(optionsOrPrompt, _analysisSchema) {
874
+ // Runtime model limits must land before normalizeTextOptions resolves
875
+ // maxTokens (getSafeMaxTokens consults the discovered output ceiling).
876
+ await this.ensureModelLimits();
859
877
  const options = this.normalizeTextOptions(optionsOrPrompt);
860
878
  this.validateOptions(options);
861
879
  const startTime = Date.now();