@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.
- package/CHANGELOG.md +12 -0
- package/dist/browser/neurolink.min.js +430 -417
- package/dist/cli/loop/optionsSchema.js +4 -0
- package/dist/constants/contextWindows.d.ts +24 -0
- package/dist/constants/contextWindows.js +50 -0
- package/dist/context/errorDetection.d.ts +6 -0
- package/dist/context/errorDetection.js +18 -0
- package/dist/context/stepBudgetGuard.d.ts +17 -3
- package/dist/context/stepBudgetGuard.js +53 -14
- package/dist/core/baseProvider.d.ts +12 -0
- package/dist/core/baseProvider.js +18 -0
- package/dist/core/modules/GenerationHandler.js +208 -42
- package/dist/core/modules/ToolsManager.d.ts +17 -0
- package/dist/core/modules/ToolsManager.js +99 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/knowledge/context.d.ts +18 -0
- package/dist/knowledge/context.js +91 -0
- package/dist/knowledge/defaults.d.ts +24 -0
- package/dist/knowledge/defaults.js +29 -0
- package/dist/knowledge/engine.d.ts +35 -0
- package/dist/knowledge/engine.js +179 -0
- package/dist/knowledge/index.d.ts +15 -0
- package/dist/knowledge/index.js +15 -0
- package/dist/knowledge/indexCache.d.ts +19 -0
- package/dist/knowledge/indexCache.js +109 -0
- package/dist/knowledge/knowledgeIndex.d.ts +41 -0
- package/dist/knowledge/knowledgeIndex.js +204 -0
- package/dist/knowledge/normalize.d.ts +32 -0
- package/dist/knowledge/normalize.js +74 -0
- package/dist/knowledge/resolve.d.ts +18 -0
- package/dist/knowledge/resolve.js +156 -0
- package/dist/knowledge/retrieval.d.ts +16 -0
- package/dist/knowledge/retrieval.js +221 -0
- package/dist/lib/constants/contextWindows.d.ts +24 -0
- package/dist/lib/constants/contextWindows.js +50 -0
- package/dist/lib/context/errorDetection.d.ts +6 -0
- package/dist/lib/context/errorDetection.js +18 -0
- package/dist/lib/context/stepBudgetGuard.d.ts +17 -3
- package/dist/lib/context/stepBudgetGuard.js +53 -14
- package/dist/lib/core/baseProvider.d.ts +12 -0
- package/dist/lib/core/baseProvider.js +18 -0
- package/dist/lib/core/modules/GenerationHandler.js +208 -42
- package/dist/lib/core/modules/ToolsManager.d.ts +17 -0
- package/dist/lib/core/modules/ToolsManager.js +99 -1
- package/dist/lib/files/fileTools.d.ts +1 -1
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +3 -0
- package/dist/lib/knowledge/context.d.ts +18 -0
- package/dist/lib/knowledge/context.js +92 -0
- package/dist/lib/knowledge/defaults.d.ts +24 -0
- package/dist/lib/knowledge/defaults.js +30 -0
- package/dist/lib/knowledge/engine.d.ts +35 -0
- package/dist/lib/knowledge/engine.js +180 -0
- package/dist/lib/knowledge/index.d.ts +15 -0
- package/dist/lib/knowledge/index.js +16 -0
- package/dist/lib/knowledge/indexCache.d.ts +19 -0
- package/dist/lib/knowledge/indexCache.js +110 -0
- package/dist/lib/knowledge/knowledgeIndex.d.ts +41 -0
- package/dist/lib/knowledge/knowledgeIndex.js +205 -0
- package/dist/lib/knowledge/normalize.d.ts +32 -0
- package/dist/lib/knowledge/normalize.js +75 -0
- package/dist/lib/knowledge/resolve.d.ts +18 -0
- package/dist/lib/knowledge/resolve.js +157 -0
- package/dist/lib/knowledge/retrieval.d.ts +16 -0
- package/dist/lib/knowledge/retrieval.js +222 -0
- package/dist/lib/mcp/toolDiscoveryService.js +59 -20
- package/dist/lib/neurolink.d.ts +14 -1
- package/dist/lib/neurolink.js +142 -12
- package/dist/lib/providers/litellm.d.ts +27 -19
- package/dist/lib/providers/litellm.js +171 -92
- package/dist/lib/providers/openaiChatCompletionsBase.d.ts +37 -1
- package/dist/lib/providers/openaiChatCompletionsBase.js +201 -33
- package/dist/lib/providers/openaiChatCompletionsClient.d.ts +23 -5
- package/dist/lib/providers/openaiChatCompletionsClient.js +94 -14
- package/dist/lib/proxy/proxyFetch.d.ts +17 -0
- package/dist/lib/proxy/proxyFetch.js +42 -4
- package/dist/lib/types/config.d.ts +12 -0
- package/dist/lib/types/conversation.d.ts +1 -1
- package/dist/lib/types/dynamic.d.ts +12 -0
- package/dist/lib/types/generate.d.ts +32 -2
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/lib/types/index.js +1 -0
- package/dist/lib/types/knowledge.d.ts +342 -0
- package/dist/lib/types/knowledge.js +30 -0
- package/dist/lib/types/openaiCompatible.d.ts +2 -0
- package/dist/lib/types/providers.d.ts +9 -0
- package/dist/lib/types/stream.d.ts +14 -0
- package/dist/lib/utils/errorHandling.js +8 -2
- package/dist/lib/utils/schemaConversion.d.ts +16 -0
- package/dist/lib/utils/schemaConversion.js +165 -8
- package/dist/lib/utils/timeout.d.ts +22 -0
- package/dist/lib/utils/timeout.js +72 -12
- package/dist/lib/utils/tokenLimits.js +22 -0
- package/dist/lib/utils/toolCallRepair.d.ts +8 -0
- package/dist/lib/utils/toolCallRepair.js +4 -1
- package/dist/mcp/toolDiscoveryService.js +59 -20
- package/dist/neurolink.d.ts +14 -1
- package/dist/neurolink.js +142 -12
- package/dist/providers/litellm.d.ts +27 -19
- package/dist/providers/litellm.js +171 -92
- package/dist/providers/openaiChatCompletionsBase.d.ts +37 -1
- package/dist/providers/openaiChatCompletionsBase.js +201 -33
- package/dist/providers/openaiChatCompletionsClient.d.ts +23 -5
- package/dist/providers/openaiChatCompletionsClient.js +94 -14
- package/dist/proxy/proxyFetch.d.ts +17 -0
- package/dist/proxy/proxyFetch.js +42 -4
- package/dist/types/config.d.ts +12 -0
- package/dist/types/conversation.d.ts +1 -1
- package/dist/types/dynamic.d.ts +12 -0
- package/dist/types/generate.d.ts +32 -2
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/knowledge.d.ts +342 -0
- package/dist/types/knowledge.js +29 -0
- package/dist/types/openaiCompatible.d.ts +2 -0
- package/dist/types/providers.d.ts +9 -0
- package/dist/types/stream.d.ts +14 -0
- package/dist/utils/errorHandling.js +8 -2
- package/dist/utils/schemaConversion.d.ts +16 -0
- package/dist/utils/schemaConversion.js +165 -8
- package/dist/utils/timeout.d.ts +22 -0
- package/dist/utils/timeout.js +72 -12
- package/dist/utils/tokenLimits.js +22 -0
- package/dist/utils/toolCallRepair.d.ts +8 -0
- package/dist/utils/toolCallRepair.js +4 -1
- 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 {
|
|
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
|
-
|
|
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:
|
|
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:
|
|
392
|
+
tools: wireTools,
|
|
272
393
|
...(options.toolChoice
|
|
273
|
-
? {
|
|
394
|
+
? {
|
|
395
|
+
toolChoice: v3ToolChoiceToOpenAI(options.toolChoice, wireNameMaps?.toWire),
|
|
396
|
+
}
|
|
274
397
|
: {}),
|
|
275
398
|
streaming: false,
|
|
276
399
|
...(responseFormat ? { responseFormat } : {}),
|
|
277
400
|
}), modelId));
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
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
|
-
?
|
|
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
|
-
|
|
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,
|
|
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 —
|
|
654
|
-
//
|
|
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.
|
|
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
|
-
|
|
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:
|
|
881
|
+
toolName: registryName,
|
|
714
882
|
toolCallId: t.id,
|
|
715
883
|
input,
|
|
716
884
|
});
|
|
717
885
|
if (!toolDef || typeof toolDef.execute !== "function") {
|
|
718
|
-
errorMsg = `Tool '${
|
|
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(
|
|
899
|
+
toolsUsed.push(registryName);
|
|
732
900
|
toolExecutionSummaries.push({
|
|
733
901
|
toolCallId: t.id,
|
|
734
|
-
toolName:
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
393
|
+
return {
|
|
394
|
+
type: "function",
|
|
395
|
+
function: { name: toolNameToWire?.get(c.toolName) ?? c.toolName },
|
|
396
|
+
};
|
|
317
397
|
}
|
|
318
398
|
}
|
|
319
399
|
return undefined;
|