@juspay/neurolink 10.3.0 → 10.4.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 (127) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/browser/neurolink.min.js +430 -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/index.d.ts +1 -0
  16. package/dist/index.js +3 -0
  17. package/dist/knowledge/context.d.ts +18 -0
  18. package/dist/knowledge/context.js +91 -0
  19. package/dist/knowledge/defaults.d.ts +24 -0
  20. package/dist/knowledge/defaults.js +29 -0
  21. package/dist/knowledge/engine.d.ts +35 -0
  22. package/dist/knowledge/engine.js +179 -0
  23. package/dist/knowledge/index.d.ts +15 -0
  24. package/dist/knowledge/index.js +15 -0
  25. package/dist/knowledge/indexCache.d.ts +19 -0
  26. package/dist/knowledge/indexCache.js +109 -0
  27. package/dist/knowledge/knowledgeIndex.d.ts +41 -0
  28. package/dist/knowledge/knowledgeIndex.js +204 -0
  29. package/dist/knowledge/normalize.d.ts +32 -0
  30. package/dist/knowledge/normalize.js +74 -0
  31. package/dist/knowledge/resolve.d.ts +18 -0
  32. package/dist/knowledge/resolve.js +156 -0
  33. package/dist/knowledge/retrieval.d.ts +16 -0
  34. package/dist/knowledge/retrieval.js +221 -0
  35. package/dist/lib/constants/contextWindows.d.ts +24 -0
  36. package/dist/lib/constants/contextWindows.js +50 -0
  37. package/dist/lib/context/errorDetection.d.ts +6 -0
  38. package/dist/lib/context/errorDetection.js +18 -0
  39. package/dist/lib/context/stepBudgetGuard.d.ts +17 -3
  40. package/dist/lib/context/stepBudgetGuard.js +53 -14
  41. package/dist/lib/core/baseProvider.d.ts +12 -0
  42. package/dist/lib/core/baseProvider.js +18 -0
  43. package/dist/lib/core/modules/GenerationHandler.js +208 -42
  44. package/dist/lib/core/modules/ToolsManager.d.ts +17 -0
  45. package/dist/lib/core/modules/ToolsManager.js +99 -1
  46. package/dist/lib/files/fileTools.d.ts +1 -1
  47. package/dist/lib/index.d.ts +1 -0
  48. package/dist/lib/index.js +3 -0
  49. package/dist/lib/knowledge/context.d.ts +18 -0
  50. package/dist/lib/knowledge/context.js +92 -0
  51. package/dist/lib/knowledge/defaults.d.ts +24 -0
  52. package/dist/lib/knowledge/defaults.js +30 -0
  53. package/dist/lib/knowledge/engine.d.ts +35 -0
  54. package/dist/lib/knowledge/engine.js +180 -0
  55. package/dist/lib/knowledge/index.d.ts +15 -0
  56. package/dist/lib/knowledge/index.js +16 -0
  57. package/dist/lib/knowledge/indexCache.d.ts +19 -0
  58. package/dist/lib/knowledge/indexCache.js +110 -0
  59. package/dist/lib/knowledge/knowledgeIndex.d.ts +41 -0
  60. package/dist/lib/knowledge/knowledgeIndex.js +205 -0
  61. package/dist/lib/knowledge/normalize.d.ts +32 -0
  62. package/dist/lib/knowledge/normalize.js +75 -0
  63. package/dist/lib/knowledge/resolve.d.ts +18 -0
  64. package/dist/lib/knowledge/resolve.js +157 -0
  65. package/dist/lib/knowledge/retrieval.d.ts +16 -0
  66. package/dist/lib/knowledge/retrieval.js +222 -0
  67. package/dist/lib/mcp/toolDiscoveryService.js +59 -20
  68. package/dist/lib/neurolink.d.ts +14 -1
  69. package/dist/lib/neurolink.js +142 -12
  70. package/dist/lib/providers/litellm.d.ts +27 -19
  71. package/dist/lib/providers/litellm.js +171 -92
  72. package/dist/lib/providers/openaiChatCompletionsBase.d.ts +37 -1
  73. package/dist/lib/providers/openaiChatCompletionsBase.js +201 -33
  74. package/dist/lib/providers/openaiChatCompletionsClient.d.ts +23 -5
  75. package/dist/lib/providers/openaiChatCompletionsClient.js +94 -14
  76. package/dist/lib/proxy/proxyFetch.d.ts +17 -0
  77. package/dist/lib/proxy/proxyFetch.js +42 -4
  78. package/dist/lib/types/config.d.ts +12 -0
  79. package/dist/lib/types/conversation.d.ts +1 -1
  80. package/dist/lib/types/dynamic.d.ts +12 -0
  81. package/dist/lib/types/generate.d.ts +32 -2
  82. package/dist/lib/types/index.d.ts +1 -0
  83. package/dist/lib/types/index.js +1 -0
  84. package/dist/lib/types/knowledge.d.ts +342 -0
  85. package/dist/lib/types/knowledge.js +30 -0
  86. package/dist/lib/types/openaiCompatible.d.ts +2 -0
  87. package/dist/lib/types/providers.d.ts +9 -0
  88. package/dist/lib/types/stream.d.ts +14 -0
  89. package/dist/lib/utils/errorHandling.js +8 -2
  90. package/dist/lib/utils/schemaConversion.d.ts +16 -0
  91. package/dist/lib/utils/schemaConversion.js +165 -8
  92. package/dist/lib/utils/timeout.d.ts +22 -0
  93. package/dist/lib/utils/timeout.js +72 -12
  94. package/dist/lib/utils/tokenLimits.js +22 -0
  95. package/dist/lib/utils/toolCallRepair.d.ts +8 -0
  96. package/dist/lib/utils/toolCallRepair.js +4 -1
  97. package/dist/mcp/toolDiscoveryService.js +59 -20
  98. package/dist/neurolink.d.ts +14 -1
  99. package/dist/neurolink.js +142 -12
  100. package/dist/providers/litellm.d.ts +27 -19
  101. package/dist/providers/litellm.js +171 -92
  102. package/dist/providers/openaiChatCompletionsBase.d.ts +37 -1
  103. package/dist/providers/openaiChatCompletionsBase.js +201 -33
  104. package/dist/providers/openaiChatCompletionsClient.d.ts +23 -5
  105. package/dist/providers/openaiChatCompletionsClient.js +94 -14
  106. package/dist/proxy/proxyFetch.d.ts +17 -0
  107. package/dist/proxy/proxyFetch.js +42 -4
  108. package/dist/types/config.d.ts +12 -0
  109. package/dist/types/conversation.d.ts +1 -1
  110. package/dist/types/dynamic.d.ts +12 -0
  111. package/dist/types/generate.d.ts +32 -2
  112. package/dist/types/index.d.ts +1 -0
  113. package/dist/types/index.js +1 -0
  114. package/dist/types/knowledge.d.ts +342 -0
  115. package/dist/types/knowledge.js +29 -0
  116. package/dist/types/openaiCompatible.d.ts +2 -0
  117. package/dist/types/providers.d.ts +9 -0
  118. package/dist/types/stream.d.ts +14 -0
  119. package/dist/utils/errorHandling.js +8 -2
  120. package/dist/utils/schemaConversion.d.ts +16 -0
  121. package/dist/utils/schemaConversion.js +165 -8
  122. package/dist/utils/timeout.d.ts +22 -0
  123. package/dist/utils/timeout.js +72 -12
  124. package/dist/utils/tokenLimits.js +22 -0
  125. package/dist/utils/toolCallRepair.d.ts +8 -0
  126. package/dist/utils/toolCallRepair.js +4 -1
  127. package/package.json +4 -1
@@ -17,6 +17,9 @@
17
17
  * Nothing here imports from "ai" or "@ai-sdk/*". The base class is a
18
18
  * direct HTTP client + multi-step tool-execution loop driven by SSE.
19
19
  */
20
+ import { getRuntimeContextWindow, getRuntimeOutputCeiling, registerRuntimeContextWindow, } from "../constants/contextWindows.js";
21
+ import { isContextOverflowError, parseProviderOverflowDetails, } from "../context/errorDetection.js";
22
+ import { ContextBudgetExceededError } from "../context/errors.js";
20
23
  import { BaseProvider } from "../core/baseProvider.js";
21
24
  import { DEFAULT_MAX_STEPS } from "../core/constants.js";
22
25
  import { streamAnalyticsCollector } from "../core/streamAnalytics.js";
@@ -24,11 +27,17 @@ import { createProxyFetch } from "../proxy/proxyFetch.js";
24
27
  import { logger } from "../utils/logger.js";
25
28
  import { NoOutputGeneratedError } from "../utils/generationErrors.js";
26
29
  import { buildNoOutputSentinel, stampNoOutputSpan, } from "../utils/noOutputSentinel.js";
27
- import { composeAbortSignals, createTimeoutController, mergeAbortSignals, } from "../utils/timeout.js";
30
+ import { composeAbortSignalsScoped, createTimeoutController, mergeAbortSignals, } from "../utils/timeout.js";
28
31
  import { emitToolEndFromStepFinish } from "../utils/toolEndEmitter.js";
29
32
  import { resolveToolChoice } from "../utils/toolChoice.js";
30
33
  import { transformToolExecutions } from "../utils/transformationUtils.js";
31
- import { buildAPIError, buildBody, buildToolsForOpenAI, createChunkQueue, createDeferredAnalytics, ensureJsonWordInBody, mapNeuroLinkToolChoice, mergeUsage, messageBuilderToOpenAI, parseSSEStream, stringifyToolOutput, stripTrailingSlash, v3ResponseFormatToOpenAI, v3ToolChoiceToOpenAI, v3ToolsToOpenAI, } from "./openaiChatCompletionsClient.js";
34
+ import { buildAPIError, buildBody, buildToolsForOpenAI, buildWireToolNameMaps, createChunkQueue, createDeferredAnalytics, ensureJsonWordInBody, estimateWireTokens, mapNeuroLinkToolChoice, mergeUsage, messageBuilderToOpenAI, parseSSEStream, stringifyToolOutput, stripTrailingSlash, v3ResponseFormatToOpenAI, v3ToolChoiceToOpenAI, v3ToolsToOpenAI, } from "./openaiChatCompletionsClient.js";
35
+ /**
36
+ * Safety margin (tokens) when fitting `max_tokens` to a runtime-discovered
37
+ * context window: the char-based input estimate and the backend's own prompt
38
+ * framing differ by a few hundred tokens, so an exact fit would still 400.
39
+ */
40
+ const WINDOW_FIT_MARGIN_TOKENS = 512;
32
41
  /**
33
42
  * Abstract HTTP+SSE provider for OpenAI chat-completions-shaped endpoints.
34
43
  */
@@ -114,6 +123,106 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
114
123
  adjustBodyAfter400(_body, _error) {
115
124
  return undefined;
116
125
  }
126
+ /**
127
+ * Fit the outgoing `max_tokens` to what the target deployment can actually
128
+ * accept, using ONLY runtime-discovered limits — static table values are
129
+ * guesses, and hard-enforcing a guess would falsely reject requests the
130
+ * real deployment accepts:
131
+ *
132
+ * effective = min(requested, discovered output ceiling,
133
+ * discovered window − estimated input − margin)
134
+ *
135
+ * Returns the caller's value untouched when nothing was discovered, and
136
+ * `undefined` when the caller sent nothing and no ceiling is known (the
137
+ * wire then omits max_tokens and the backend applies its own default — no
138
+ * invented numbers). Throws ContextBudgetExceededError when the estimated
139
+ * input ALONE exceeds a discovered window: that request cannot succeed,
140
+ * and failing fast with honest numbers beats a guaranteed provider 400
141
+ * (plus any proxy-side fallback cascade) after a full round-trip.
142
+ */
143
+ resolveWireMaxTokens(modelId, requested, messages, tools) {
144
+ const ceiling = getRuntimeOutputCeiling(this.providerName, modelId);
145
+ let effective = requested;
146
+ if (ceiling !== undefined &&
147
+ (effective === undefined || effective > ceiling)) {
148
+ if (effective !== undefined) {
149
+ logger.debug(`${this.providerName}: clamping max_tokens ${effective} to the advertised ${modelId} output ceiling ${ceiling}`);
150
+ }
151
+ effective = ceiling;
152
+ }
153
+ const window = getRuntimeContextWindow(this.providerName, modelId);
154
+ if (window !== undefined) {
155
+ const estimatedInput = estimateWireTokens(messages, tools, this.providerName);
156
+ const fit = window - estimatedInput - WINDOW_FIT_MARGIN_TOKENS;
157
+ if (fit <= 0) {
158
+ throw new ContextBudgetExceededError(`Estimated input (${estimatedInput} tokens) alone exceeds the ` +
159
+ `${this.providerName}/${modelId} context window advertised by ` +
160
+ `the serving infrastructure (${window} tokens). Reduce the ` +
161
+ `prompt/conversation size — no max_tokens value can make this ` +
162
+ `request fit.`, {
163
+ estimatedTokens: estimatedInput,
164
+ availableTokens: Math.max(0, window - WINDOW_FIT_MARGIN_TOKENS),
165
+ stagesUsed: [],
166
+ breakdown: {},
167
+ });
168
+ }
169
+ if (effective !== undefined && effective > fit) {
170
+ logger.warn(`${this.providerName}: max_tokens ${effective} cannot fit the ` +
171
+ `${modelId} window (${window}) with ~${estimatedInput} input ` +
172
+ `tokens — re-fitting to ${fit}`);
173
+ effective = fit;
174
+ }
175
+ }
176
+ return effective;
177
+ }
178
+ /**
179
+ * Learn from a provider context-overflow 400 and, when possible, produce a
180
+ * corrected body for the one-shot retry slot. Two dynamic effects, zero
181
+ * static data:
182
+ *
183
+ * - the window stated in the error is registered with the runtime
184
+ * resolver, so every later budget check / compaction / max_tokens fit
185
+ * uses the backend's own number — self-healing even when a discovery
186
+ * endpoint (`/model/info`) is absent or unauthorized;
187
+ * - when the error also states the real input size (vllm/LiteLLM
188
+ * phrasing) and the body carried `max_tokens`, it is re-fit to
189
+ * `window − input − margin` and the request retried once.
190
+ *
191
+ * Returns undefined when the error is not an overflow, or when no smaller
192
+ * `max_tokens` can make the request fit (input alone too large) — the
193
+ * original error then propagates unchanged.
194
+ */
195
+ correctBodyAfterContextOverflow(body, error) {
196
+ if (!isContextOverflowError(error)) {
197
+ return undefined;
198
+ }
199
+ const details = parseProviderOverflowDetails(error) ??
200
+ parseProviderOverflowDetails(error.responseBody);
201
+ if (!details || details.budgetTokens <= 0) {
202
+ return undefined;
203
+ }
204
+ registerRuntimeContextWindow(this.providerName, body.model, details.budgetTokens);
205
+ // When the body carried no max_tokens (server-defaulted output), the
206
+ // vllm/LiteLLM phrasing still states what the backend counted — use it
207
+ // so those requests get a re-fit retry too instead of propagating.
208
+ const previousMaxTokens = typeof body.max_tokens === "number"
209
+ ? body.max_tokens
210
+ : details.requestedOutputTokens;
211
+ if (previousMaxTokens === undefined || details.actualTokens <= 0) {
212
+ return undefined;
213
+ }
214
+ const refit = details.budgetTokens - details.actualTokens - WINDOW_FIT_MARGIN_TOKENS;
215
+ if (refit <= 0 || refit >= previousMaxTokens) {
216
+ return undefined;
217
+ }
218
+ logger.warn(`${this.providerName}: ${body.model} rejected the request as over-window — retrying once with max_tokens re-fit from the provider's own numbers`, {
219
+ window: details.budgetTokens,
220
+ inputTokens: details.actualTokens,
221
+ previousMaxTokens,
222
+ refitMaxTokens: refit,
223
+ });
224
+ return { ...body, max_tokens: refit };
225
+ }
117
226
  /**
118
227
  * Hook called once at the start of every `executeStream` invocation.
119
228
  * Return lifecycle listeners (onUsage / onFinish) to receive deferred
@@ -238,6 +347,8 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
238
347
  const adjustResponseFormat = this.adjustResponseFormat.bind(this);
239
348
  const adjustRequestBody = this.adjustRequestBody.bind(this);
240
349
  const adjustBodyAfter400 = this.adjustBodyAfter400.bind(this);
350
+ const correctBodyAfterContextOverflow = this.correctBodyAfterContextOverflow.bind(this);
351
+ const resolveWireMaxTokens = this.resolveWireMaxTokens.bind(this);
241
352
  const suppressResponseFormatWithTools = this.suppressResponseFormatWithTools.bind(this);
242
353
  const getTimeoutForOptions = (opts) => this.getTimeout((opts ?? {}));
243
354
  return {
@@ -246,7 +357,13 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
246
357
  modelId,
247
358
  supportedUrls: {},
248
359
  doGenerate: async (options) => {
249
- const baseMessages = messageBuilderToOpenAI(options.prompt);
360
+ // Wire-name mapping: only materializes when at least one tool name
361
+ // is outside the OpenAI-compatible alphabet (see
362
+ // buildWireToolNameMaps) — the common all-valid case is a no-op.
363
+ const wireNameMaps = buildWireToolNameMaps((options.tools ?? [])
364
+ .filter((t) => t.type === "function")
365
+ .map((t) => t.name));
366
+ const baseMessages = messageBuilderToOpenAI(options.prompt, wireNameMaps?.toWire);
250
367
  const hasTools = Array.isArray(options.tools) && options.tools.length > 0;
251
368
  const responseFormat = options.responseFormat &&
252
369
  !(hasTools && suppressResponseFormatWithTools())
@@ -256,11 +373,15 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
256
373
  // so the json_object word guard reflects whatever a subclass left on
257
374
  // the wire (it may rewrite response_format/messages), not an
258
375
  // intermediate state.
376
+ const wireTools = v3ToolsToOpenAI(options.tools, wireNameMaps?.toWire);
377
+ // Fit max_tokens to the runtime-discovered output ceiling and
378
+ // context window (no-op when nothing was discovered).
379
+ const wireMaxTokens = resolveWireMaxTokens(modelId, options.maxOutputTokens, baseMessages, wireTools);
259
380
  const body = ensureJsonWordInBody(adjustRequestBody(buildBody({
260
381
  modelId,
261
382
  messages: baseMessages,
262
383
  options: adjustBuildBodyOptions(modelId, {
263
- maxTokens: options.maxOutputTokens,
384
+ maxTokens: wireMaxTokens,
264
385
  temperature: options.temperature,
265
386
  topP: options.topP,
266
387
  presencePenalty: options.presencePenalty,
@@ -268,18 +389,33 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
268
389
  seed: options.seed,
269
390
  stopSequences: options.stopSequences,
270
391
  }),
271
- tools: v3ToolsToOpenAI(options.tools),
392
+ tools: wireTools,
272
393
  ...(options.toolChoice
273
- ? { toolChoice: v3ToolChoiceToOpenAI(options.toolChoice) }
394
+ ? {
395
+ toolChoice: v3ToolChoiceToOpenAI(options.toolChoice, wireNameMaps?.toWire),
396
+ }
274
397
  : {}),
275
398
  streaming: false,
276
399
  ...(responseFormat ? { responseFormat } : {}),
277
400
  }), modelId));
278
- const timeoutController = createTimeoutController(getTimeoutForOptions(options), providerName, "generate");
279
- const composedSignal = composeAbortSignals(options.abortSignal, timeoutController?.controller.signal);
280
- let res;
401
+ // Per-step timeout: the AI-SDK V3 call options never carry `timeout`,
402
+ // so resolving from them always returned the provider default — a
403
+ // caller's explicit `timeout: "15m"` bounded the outer loop but each
404
+ // step request stayed capped at the default (litellm: 5m). The
405
+ // orchestrator forwards the caller's resolved timeout via
406
+ // providerOptions.neurolink.timeoutMs; prefer it when present.
407
+ const nlStepTimeoutMs = options.providerOptions?.neurolink?.timeoutMs;
408
+ const timeoutController = createTimeoutController(typeof nlStepTimeoutMs === "number"
409
+ ? nlStepTimeoutMs
410
+ : getTimeoutForOptions(options), providerName, "generate");
411
+ // Scoped composition: a plain AbortSignal.any per step accumulates
412
+ // registrations on the long-lived generate-call signal for the whole
413
+ // turn (MaxListenersExceededWarning at 10+ steps); dispose() detaches
414
+ // this step's listeners the moment the request settles.
415
+ const { signal: composedSignal, dispose: disposeComposedSignal } = composeAbortSignalsScoped(options.abortSignal, timeoutController?.controller.signal);
416
+ let json;
281
417
  try {
282
- res = await fetchImpl(url, {
418
+ let res = await fetchImpl(url, {
283
419
  method: "POST",
284
420
  headers: {
285
421
  "Content-Type": "application/json",
@@ -290,14 +426,17 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
290
426
  });
291
427
  if (!res.ok) {
292
428
  const apiErr = await buildAPIError(url, body, res);
293
- // One-shot 400 retry: a subclass may strip a rejected field and
294
- // return a modified body (e.g. NIM's chat_template /
295
- // reasoning_budget). The retry runs under the SAME timeout
296
- // controller as the first attempt, so the configured timeout caps
297
- // the overall call matching the streaming path, which reuses
298
- // its composed signal for the retry.
429
+ // One-shot 400 retry. The overflow corrector runs FIRST (it can
430
+ // re-fit max_tokens from the provider's own numbers and also
431
+ // self-heals the runtime window registry); otherwise a subclass
432
+ // may strip a rejected field and return a modified body (e.g.
433
+ // NIM's chat_template / reasoning_budget). The retry runs under
434
+ // the SAME timeout controller as the first attempt, so the
435
+ // configured timeout caps the overall call — matching the
436
+ // streaming path, which reuses its composed signal for the retry.
299
437
  const retryBody = res.status === 400
300
- ? adjustBodyAfter400(body, apiErr)
438
+ ? (correctBodyAfterContextOverflow(body, apiErr) ??
439
+ adjustBodyAfter400(body, apiErr))
301
440
  : undefined;
302
441
  if (!retryBody) {
303
442
  throw apiErr;
@@ -315,11 +454,16 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
315
454
  throw await buildAPIError(url, retryBody, res);
316
455
  }
317
456
  }
457
+ // Body read stays INSIDE the timeout window: it previously ran
458
+ // after cleanup(), so a response whose headers arrived but whose
459
+ // body stalled mid-transfer was bounded by nothing but the caller's
460
+ // outer wall-clock.
461
+ json = (await res.json());
318
462
  }
319
463
  finally {
320
464
  timeoutController?.cleanup();
465
+ disposeComposedSignal();
321
466
  }
322
- const json = (await res.json());
323
467
  const choice = json.choices?.[0];
324
468
  const text = (typeof choice?.message?.content === "string"
325
469
  ? choice.message.content
@@ -341,7 +485,9 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
341
485
  content.push({
342
486
  type: "tool-call",
343
487
  toolCallId: tc.id,
344
- toolName: tc.function.name,
488
+ // Reverse-map wire names so tool lookup/execution and results
489
+ // reported to the caller use the registered names.
490
+ toolName: wireNameMaps?.fromWire.get(tc.function.name) ?? tc.function.name,
345
491
  input: tc.function.arguments ?? "",
346
492
  });
347
493
  }
@@ -412,6 +558,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
412
558
  ]).signal;
413
559
  let modelId;
414
560
  let toolsRecord;
561
+ let wireNameMaps;
415
562
  let openAITools;
416
563
  let openAIToolChoice;
417
564
  let conversation;
@@ -421,12 +568,17 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
421
568
  toolsRecord = shouldUseTools
422
569
  ? options.tools || (await this.getAllTools())
423
570
  : {};
571
+ // Wire-name mapping: only materializes when a registered tool name is
572
+ // outside the OpenAI-compatible alphabet (see buildWireToolNameMaps).
573
+ wireNameMaps = shouldUseTools
574
+ ? buildWireToolNameMaps(Object.keys(toolsRecord))
575
+ : undefined;
424
576
  openAITools = shouldUseTools
425
- ? buildToolsForOpenAI(toolsRecord)
577
+ ? buildToolsForOpenAI(toolsRecord, wireNameMaps?.toWire)
426
578
  : undefined;
427
- openAIToolChoice = mapNeuroLinkToolChoice(resolveToolChoice(options, toolsRecord, shouldUseTools));
579
+ openAIToolChoice = mapNeuroLinkToolChoice(resolveToolChoice(options, toolsRecord, shouldUseTools), wireNameMaps?.toWire);
428
580
  const initialMessages = await this.buildMessagesForStream(options);
429
- conversation = messageBuilderToOpenAI(initialMessages);
581
+ conversation = messageBuilderToOpenAI(initialMessages, wireNameMaps?.toWire);
430
582
  }
431
583
  catch (setupErr) {
432
584
  timeoutController?.cleanup();
@@ -453,6 +605,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
453
605
  openAITools,
454
606
  openAIToolChoice,
455
607
  toolsRecord,
608
+ toolNameFromWire: wireNameMaps?.fromWire,
456
609
  emitter,
457
610
  toolsUsed,
458
611
  toolExecutionSummaries,
@@ -573,7 +726,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
573
726
  return result;
574
727
  }
575
728
  async runStreamLoop(args) {
576
- const { maxSteps, modelId, url, fetchImpl, abortSignal, options, conversation, openAITools, openAIToolChoice, toolsRecord, emitter, toolsUsed, toolExecutionSummaries, pushChunk, resolveUsage, resolveFinish, } = args;
729
+ const { maxSteps, modelId, url, fetchImpl, abortSignal, options, conversation, openAITools, openAIToolChoice, toolsRecord, toolNameFromWire, emitter, toolsUsed, toolExecutionSummaries, pushChunk, resolveUsage, resolveFinish, } = args;
577
730
  try {
578
731
  let stepFinish = null;
579
732
  let stepUsage;
@@ -600,6 +753,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
600
753
  stepResult,
601
754
  conversation,
602
755
  toolsRecord,
756
+ toolNameFromWire,
603
757
  emitter,
604
758
  toolsUsed,
605
759
  toolExecutionSummaries,
@@ -629,10 +783,17 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
629
783
  }
630
784
  }
631
785
  async streamOneStep(args) {
786
+ // Per-step max_tokens fit: the conversation grows every step of the
787
+ // tool loop, so the window fit is recomputed per request (no-op when
788
+ // nothing was runtime-discovered).
789
+ const wireMaxTokens = this.resolveWireMaxTokens(args.modelId, args.options.maxTokens ?? undefined, args.conversation, args.openAITools);
790
+ const stepOptions = wireMaxTokens !== args.options.maxTokens
791
+ ? { ...args.options, maxTokens: wireMaxTokens }
792
+ : args.options;
632
793
  const body = ensureJsonWordInBody(this.adjustRequestBody(buildBody({
633
794
  modelId: args.modelId,
634
795
  messages: args.conversation,
635
- options: this.adjustBuildBodyOptions(args.modelId, args.options),
796
+ options: this.adjustBuildBodyOptions(args.modelId, stepOptions),
636
797
  tools: args.openAITools,
637
798
  ...(args.openAIToolChoice !== undefined
638
799
  ? { toolChoice: args.openAIToolChoice }
@@ -650,10 +811,13 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
650
811
  });
651
812
  if (!res.ok) {
652
813
  const apiErr = await buildAPIError(args.url, body, res);
653
- // One-shot 400 retry — same hook as the doGenerate path (e.g. NIM
654
- // strips chat_template/reasoning_budget when a model rejects them).
814
+ // One-shot 400 retry — overflow corrector first (re-fits max_tokens
815
+ // from the provider's own numbers + self-heals the window registry),
816
+ // then the subclass hook (e.g. NIM strips chat_template /
817
+ // reasoning_budget when a model rejects them).
655
818
  const retryBody = res.status === 400
656
- ? this.adjustBodyAfter400(body, apiErr)
819
+ ? (this.correctBodyAfterContextOverflow(body, apiErr) ??
820
+ this.adjustBodyAfter400(body, apiErr))
657
821
  : undefined;
658
822
  if (!retryBody) {
659
823
  throw apiErr;
@@ -683,7 +847,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
683
847
  });
684
848
  }
685
849
  async executeToolBatch(args) {
686
- const { stepResult, conversation, toolsRecord, emitter, toolsUsed, toolExecutionSummaries, options, } = args;
850
+ const { stepResult, conversation, toolsRecord, toolNameFromWire, emitter, toolsUsed, toolExecutionSummaries, options, } = args;
687
851
  const toolCallsForMessage = [];
688
852
  for (const [, t] of stepResult.toolCalls) {
689
853
  toolCallsForMessage.push({
@@ -708,14 +872,18 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
708
872
  }
709
873
  let output;
710
874
  let errorMsg;
711
- const toolDef = toolsRecord[t.name];
875
+ // The model calls tools by their WIRE names; execution, events, and
876
+ // reporting use the registered names (reverse-mapped when a wire-name
877
+ // map is in effect — see buildWireToolNameMaps).
878
+ const registryName = toolNameFromWire?.get(t.name) ?? t.name;
879
+ const toolDef = toolsRecord[registryName];
712
880
  emitter?.emit("tool:start", {
713
- toolName: t.name,
881
+ toolName: registryName,
714
882
  toolCallId: t.id,
715
883
  input,
716
884
  });
717
885
  if (!toolDef || typeof toolDef.execute !== "function") {
718
- errorMsg = `Tool '${t.name}' is not registered.`;
886
+ errorMsg = `Tool '${registryName}' is not registered.`;
719
887
  output = { error: errorMsg };
720
888
  }
721
889
  else {
@@ -728,10 +896,10 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
728
896
  }
729
897
  }
730
898
  const endedAt = new Date();
731
- toolsUsed.push(t.name);
899
+ toolsUsed.push(registryName);
732
900
  toolExecutionSummaries.push({
733
901
  toolCallId: t.id,
734
- toolName: t.name,
902
+ toolName: registryName,
735
903
  input,
736
904
  output,
737
905
  ...(errorMsg ? { error: errorMsg } : {}),
@@ -15,22 +15,40 @@
15
15
  */
16
16
  import type { OpenAICompatBuildBodyArgs, OpenAICompatChatMessage, OpenAICompatChatRequest, OpenAICompatChatTool, OpenAICompatMessage, OpenAICompatMessageContent, OpenAICompatResponseFormat, OpenAICompatSSEResult, OpenAICompatStreamChunk, OpenAICompatToolChoiceWire, OpenAICompatV3CallToolChoice, OpenAICompatV3CallTools, DeferredUsage, Tool } from "../types/index.js";
17
17
  export declare const stripTrailingSlash: (s: string) => string;
18
+ /**
19
+ * Build a bijective original ↔ wire tool-name map. Returns undefined when
20
+ * every name is already wire-valid (the common case — the wire then uses
21
+ * original names untouched and callers skip all mapping). Sanitized names
22
+ * that collide get a deterministic numeric suffix so the map stays
23
+ * invertible.
24
+ */
25
+ export declare const buildWireToolNameMaps: (names: readonly string[]) => {
26
+ toWire: Map<string, string>;
27
+ fromWire: Map<string, string>;
28
+ } | undefined;
29
+ /**
30
+ * Estimate the input-token cost of a fully-built wire request (messages +
31
+ * tool definitions). Used by the per-request max_tokens fit against a
32
+ * RUNTIME-DISCOVERED context window — deliberately the same char-based
33
+ * estimator the budget pipeline uses, so both layers agree.
34
+ */
35
+ export declare const estimateWireTokens: (messages: ReadonlyArray<OpenAICompatChatMessage>, tools: OpenAICompatChatTool[] | undefined, provider?: string) => number;
18
36
  export declare const safeStringify: (value: unknown) => string;
19
37
  export declare const stringifyToolInput: (input: unknown) => string;
20
38
  export declare const stringifyToolOutput: (output: unknown) => string;
21
39
  export declare const imageDataToURL: (data: unknown) => string | undefined;
22
40
  export declare const convertContentForOpenAI: (content: unknown) => string | OpenAICompatMessageContent[];
23
- export declare const messageBuilderToOpenAI: (messages: ReadonlyArray<OpenAICompatMessage>) => OpenAICompatChatMessage[];
24
- export declare const buildToolsForOpenAI: (tools: Record<string, Tool>) => OpenAICompatChatTool[] | undefined;
25
- export declare const v3ToolsToOpenAI: (tools: OpenAICompatV3CallTools | undefined) => OpenAICompatChatTool[] | undefined;
26
- export declare const v3ToolChoiceToOpenAI: (choice: OpenAICompatV3CallToolChoice) => OpenAICompatToolChoiceWire | undefined;
41
+ export declare const messageBuilderToOpenAI: (messages: ReadonlyArray<OpenAICompatMessage>, toolNameToWire?: Map<string, string>) => OpenAICompatChatMessage[];
42
+ export declare const buildToolsForOpenAI: (tools: Record<string, Tool>, toolNameToWire?: Map<string, string>) => OpenAICompatChatTool[] | undefined;
43
+ export declare const v3ToolsToOpenAI: (tools: OpenAICompatV3CallTools | undefined, toolNameToWire?: Map<string, string>) => OpenAICompatChatTool[] | undefined;
44
+ export declare const v3ToolChoiceToOpenAI: (choice: OpenAICompatV3CallToolChoice, toolNameToWire?: Map<string, string>) => OpenAICompatToolChoiceWire | undefined;
27
45
  export declare const v3ResponseFormatToOpenAI: (rf: {
28
46
  type: "text" | "json";
29
47
  schema?: Record<string, unknown>;
30
48
  name?: string;
31
49
  description?: string;
32
50
  }) => OpenAICompatResponseFormat | undefined;
33
- export declare const mapNeuroLinkToolChoice: (choice: unknown) => OpenAICompatToolChoiceWire | undefined;
51
+ export declare const mapNeuroLinkToolChoice: (choice: unknown, toolNameToWire?: Map<string, string>) => OpenAICompatToolChoiceWire | undefined;
34
52
  export declare const messagesContainJsonWord: (messages: ReadonlyArray<OpenAICompatChatMessage>) => boolean;
35
53
  export declare const ensureJsonWordInBody: (body: OpenAICompatChatRequest) => OpenAICompatChatRequest;
36
54
  export declare const requiresMaxCompletionTokens: (modelId: string) => boolean;
@@ -14,8 +14,72 @@
14
14
  * module is to be the native replacement for the AI SDK's OpenAI wrapper.
15
15
  */
16
16
  import { createParser } from "eventsource-parser";
17
- import { convertZodToJsonSchema } from "../utils/schemaConversion.js";
17
+ import { sanitizeToolName } from "../mcp/toolConverter.js";
18
+ import { convertZodToJsonSchema, normalizeWireToolSchema, } from "../utils/schemaConversion.js";
19
+ import { estimateTokens, TOKENS_PER_MESSAGE, } from "../utils/tokenEstimation.js";
18
20
  export const stripTrailingSlash = (s) => s.replace(/\/+$/, "");
21
+ // OpenAI-compatible wire tool names: first char letter/underscore (some
22
+ // chat templates treat a leading digit or hyphen as invalid identifiers),
23
+ // then letters/digits/underscore/hyphen, 64 chars total — exactly the
24
+ // pattern below, which mirrors sanitizeToolName's output alphabet. MCP
25
+ // servers can register names outside it (dots, colons, spaces); sent
26
+ // verbatim they make backends reject the tools block or emit tool_calls
27
+ // that no longer match the registry.
28
+ const WIRE_TOOL_NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$/;
29
+ /**
30
+ * Build a bijective original ↔ wire tool-name map. Returns undefined when
31
+ * every name is already wire-valid (the common case — the wire then uses
32
+ * original names untouched and callers skip all mapping). Sanitized names
33
+ * that collide get a deterministic numeric suffix so the map stays
34
+ * invertible.
35
+ */
36
+ export const buildWireToolNameMaps = (names) => {
37
+ if (names.every((name) => WIRE_TOOL_NAME_RE.test(name))) {
38
+ return undefined;
39
+ }
40
+ const toWire = new Map();
41
+ const fromWire = new Map();
42
+ for (const name of names) {
43
+ let wire = WIRE_TOOL_NAME_RE.test(name) ? name : sanitizeToolName(name);
44
+ if (fromWire.has(wire)) {
45
+ let suffix = 2;
46
+ let candidate;
47
+ do {
48
+ const tail = `_${suffix}`;
49
+ candidate = `${wire.slice(0, 64 - tail.length)}${tail}`;
50
+ suffix++;
51
+ } while (fromWire.has(candidate));
52
+ wire = candidate;
53
+ }
54
+ toWire.set(name, wire);
55
+ fromWire.set(wire, name);
56
+ }
57
+ return { toWire, fromWire };
58
+ };
59
+ /**
60
+ * Estimate the input-token cost of a fully-built wire request (messages +
61
+ * tool definitions). Used by the per-request max_tokens fit against a
62
+ * RUNTIME-DISCOVERED context window — deliberately the same char-based
63
+ * estimator the budget pipeline uses, so both layers agree.
64
+ */
65
+ export const estimateWireTokens = (messages, tools, provider) => {
66
+ let total = 0;
67
+ for (const message of messages) {
68
+ const content = typeof message.content === "string"
69
+ ? message.content
70
+ : safeStringify(message.content);
71
+ total += estimateTokens(content, provider) + TOKENS_PER_MESSAGE;
72
+ // tool_calls only exists on the assistant variant of the message union.
73
+ const toolCalls = message.tool_calls;
74
+ if (toolCalls) {
75
+ total += estimateTokens(safeStringify(toolCalls), provider);
76
+ }
77
+ }
78
+ if (tools && tools.length > 0) {
79
+ total += estimateTokens(safeStringify(tools), provider);
80
+ }
81
+ return total;
82
+ };
19
83
  export const safeStringify = (value) => {
20
84
  try {
21
85
  return JSON.stringify(value ?? "");
@@ -132,7 +196,7 @@ export const convertContentForOpenAI = (content) => {
132
196
  }
133
197
  return out;
134
198
  };
135
- export const messageBuilderToOpenAI = (messages) => {
199
+ export const messageBuilderToOpenAI = (messages, toolNameToWire) => {
136
200
  const out = [];
137
201
  for (const msg of messages) {
138
202
  switch (msg.role) {
@@ -165,11 +229,14 @@ export const messageBuilderToOpenAI = (messages) => {
165
229
  }
166
230
  else if (p.type === "tool-call") {
167
231
  const tc = part;
232
+ // History tool-call names must match the wire `tools` block —
233
+ // forward-map when a wire-name map is in effect.
234
+ const historyName = tc.toolName ?? "";
168
235
  toolCalls.push({
169
236
  id: tc.toolCallId ?? "",
170
237
  type: "function",
171
238
  function: {
172
- name: tc.toolName ?? "",
239
+ name: toolNameToWire?.get(historyName) ?? historyName,
173
240
  arguments: stringifyToolInput(tc.input),
174
241
  },
175
242
  });
@@ -223,7 +290,7 @@ export const messageBuilderToOpenAI = (messages) => {
223
290
  }
224
291
  return out;
225
292
  };
226
- export const buildToolsForOpenAI = (tools) => {
293
+ export const buildToolsForOpenAI = (tools, toolNameToWire) => {
227
294
  const entries = Object.entries(tools);
228
295
  if (entries.length === 0) {
229
296
  return undefined;
@@ -235,14 +302,16 @@ export const buildToolsForOpenAI = (tools) => {
235
302
  // tool.inputSchema may be a Zod schema, an AI SDK jsonSchema() wrapper,
236
303
  // or plain JSON Schema — convertZodToJsonSchema normalizes all three.
237
304
  // Sending raw Zod internals (with `_def`) gets rejected by most
238
- // OpenAI-compatible endpoints.
305
+ // OpenAI-compatible endpoints. normalizeWireToolSchema then strips
306
+ // $schema/$defs indirection that proxied backends render into chat
307
+ // templates verbatim (degrading argument generation).
239
308
  const parameters = rawSchema
240
- ? convertZodToJsonSchema(rawSchema)
309
+ ? normalizeWireToolSchema(convertZodToJsonSchema(rawSchema))
241
310
  : { type: "object", properties: {} };
242
311
  out.push({
243
312
  type: "function",
244
313
  function: {
245
- name,
314
+ name: toolNameToWire?.get(name) ?? name,
246
315
  ...(t.description ? { description: t.description } : {}),
247
316
  parameters,
248
317
  },
@@ -254,7 +323,7 @@ export const buildToolsForOpenAI = (tools) => {
254
323
  // path that BaseProvider's `generate()` still drives via the AI SDK's
255
324
  // `generateText`. The streaming path doesn't need these — it consumes
256
325
  // NeuroLink-shaped options directly.
257
- export const v3ToolsToOpenAI = (tools) => {
326
+ export const v3ToolsToOpenAI = (tools, toolNameToWire) => {
258
327
  if (!tools || tools.length === 0) {
259
328
  return undefined;
260
329
  }
@@ -264,9 +333,12 @@ export const v3ToolsToOpenAI = (tools) => {
264
333
  out.push({
265
334
  type: "function",
266
335
  function: {
267
- name: t.name,
336
+ name: toolNameToWire?.get(t.name) ?? t.name,
268
337
  ...(t.description ? { description: t.description } : {}),
269
- parameters: t.inputSchema,
338
+ // The AI SDK serializes inputSchema to JSON Schema before
339
+ // doGenerate; normalize the wire form ($ref/$defs inlining,
340
+ // annotation stripping, nullable collapse) for proxied backends.
341
+ parameters: normalizeWireToolSchema(t.inputSchema),
270
342
  ...(t.strict !== undefined ? { strict: t.strict } : {}),
271
343
  },
272
344
  });
@@ -276,14 +348,19 @@ export const v3ToolsToOpenAI = (tools) => {
276
348
  }
277
349
  return out.length > 0 ? out : undefined;
278
350
  };
279
- export const v3ToolChoiceToOpenAI = (choice) => {
351
+ export const v3ToolChoiceToOpenAI = (choice, toolNameToWire) => {
280
352
  switch (choice.type) {
281
353
  case "auto":
282
354
  case "none":
283
355
  case "required":
284
356
  return choice.type;
285
357
  case "tool":
286
- return { type: "function", function: { name: choice.toolName } };
358
+ return {
359
+ type: "function",
360
+ function: {
361
+ name: toolNameToWire?.get(choice.toolName) ?? choice.toolName,
362
+ },
363
+ };
287
364
  }
288
365
  };
289
366
  export const v3ResponseFormatToOpenAI = (rf) => {
@@ -303,7 +380,7 @@ export const v3ResponseFormatToOpenAI = (rf) => {
303
380
  },
304
381
  };
305
382
  };
306
- export const mapNeuroLinkToolChoice = (choice) => {
383
+ export const mapNeuroLinkToolChoice = (choice, toolNameToWire) => {
307
384
  if (!choice) {
308
385
  return undefined;
309
386
  }
@@ -313,7 +390,10 @@ export const mapNeuroLinkToolChoice = (choice) => {
313
390
  if (typeof choice === "object" && choice !== null) {
314
391
  const c = choice;
315
392
  if (c.type === "tool" && c.toolName) {
316
- return { type: "function", function: { name: c.toolName } };
393
+ return {
394
+ type: "function",
395
+ function: { name: toolNameToWire?.get(c.toolName) ?? c.toolName },
396
+ };
317
397
  }
318
398
  }
319
399
  return undefined;