@juspay/neurolink 12.11.3 → 12.12.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 +3 -4
- package/dist/browser/neurolink.min.js +533 -581
- package/dist/constants/enums.d.ts +13 -0
- package/dist/constants/enums.js +14 -0
- package/dist/core/baseProvider.d.ts +71 -3
- package/dist/core/baseProvider.js +152 -44
- package/dist/core/modules/GenerationHandler.d.ts +22 -24
- package/dist/core/modules/GenerationHandler.js +28 -463
- package/dist/core/nativeGenerateLoop.d.ts +35 -0
- package/dist/core/nativeGenerateLoop.js +261 -0
- package/dist/files/fileTools.d.ts +5 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/middleware/builtin/guardrails.d.ts +0 -5
- package/dist/middleware/builtin/guardrails.js +33 -5
- package/dist/middleware/factory.js +1 -1
- package/dist/middleware/wrapLanguageModel.d.ts +18 -0
- package/dist/middleware/wrapLanguageModel.js +53 -0
- package/dist/processors/media/AudioProcessor.js +46 -11
- package/dist/providers/amazonSagemaker.d.ts +17 -1
- package/dist/providers/amazonSagemaker.js +110 -0
- package/dist/providers/anthropic/client.d.ts +11 -0
- package/dist/providers/anthropic/client.js +148 -1
- package/dist/providers/catalog/index.generated.d.ts +1 -1
- package/dist/providers/catalog/index.generated.js +3 -0
- package/dist/providers/catalog/loader.js +1 -0
- package/dist/providers/catalog/mancer.json +192 -0
- package/dist/providers/configuredOpenAICompat.d.ts +11 -0
- package/dist/providers/configuredOpenAICompat.js +16 -0
- package/dist/providers/googleVertex/client.d.ts +0 -9
- package/dist/providers/googleVertex/client.js +0 -33
- package/dist/providers/openaiChatCompletionsBase.d.ts +21 -1
- package/dist/providers/openaiChatCompletionsBase.js +178 -0
- package/dist/providers/providerTypeUtils.d.ts +1 -2
- package/dist/providers/providerTypeUtils.js +5 -1
- package/dist/types/aiCompat.d.ts +485 -0
- package/dist/types/aiCompat.js +17 -0
- package/dist/types/conversation.d.ts +1 -1
- package/dist/types/generate.d.ts +52 -0
- package/dist/types/middleware.d.ts +3 -6
- package/dist/types/providerCatalog.generated.d.ts +2 -2
- package/dist/types/providers.d.ts +14 -1
- package/dist/types/tools.d.ts +2 -2
- package/dist/utils/generationErrors.d.ts +78 -6
- package/dist/utils/generationErrors.js +114 -6
- package/dist/utils/nativeSingleShot.d.ts +3 -0
- package/dist/utils/nativeSingleShot.js +83 -0
- package/dist/utils/tool.d.ts +30 -5
- package/dist/utils/tool.js +43 -5
- package/package.json +3 -6
- package/dist/utils/generation.d.ts +0 -8
- package/dist/utils/generation.js +0 -8
|
@@ -14,23 +14,16 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { SpanKind, SpanStatusCode } from "@opentelemetry/api";
|
|
16
16
|
import { getModelId } from "../../providers/providerTypeUtils.js";
|
|
17
|
-
import { resolveSamplingParams } from "../../models/modelRegistry.js";
|
|
18
17
|
import { tracers } from "../../telemetry/tracers.js";
|
|
19
18
|
import { logger } from "../../utils/logger.js";
|
|
20
|
-
import { emitToolEndFromStepFinish } from "../../utils/toolEndEmitter.js";
|
|
21
19
|
import { calculateCost } from "../../utils/pricing.js";
|
|
22
20
|
import { withProviderRetry } from "../../utils/providerRetry.js";
|
|
23
21
|
import { parseTimeout } from "../../utils/timeout.js";
|
|
24
22
|
import { calculateCacheSavingsPercent, extractCacheCreationTokens, extractCacheReadTokens, extractTokenUsage, } from "../../utils/tokenUtils.js";
|
|
25
23
|
import { DEFAULT_MAX_STEPS, DEFAULT_WRAPUP_TIME_LEAD_MS, } from "../constants.js";
|
|
26
|
-
import {
|
|
27
|
-
import { isTemperatureDeprecatedError, isSchemaComplexityError, isToolsSchemaConflictError, isToolsSchemaExclusionInForce, } from "./structuredOutputPolicy.js";
|
|
24
|
+
import { isTemperatureDeprecatedError, isSchemaComplexityError, isToolsSchemaConflictError, } from "./structuredOutputPolicy.js";
|
|
28
25
|
import { coerceJsonToSchema, recoverScalarRoot, schemaAccepts, } from "../../utils/json/coerce.js";
|
|
29
|
-
import { convertZodToJsonSchema } from "../../utils/schemaConversion.js";
|
|
30
26
|
import { NoObjectGeneratedError } from "../../utils/generationErrors.js";
|
|
31
|
-
import { Output, stepCountIs } from "../../utils/tool.js";
|
|
32
|
-
import { generateText } from "../../utils/generation.js";
|
|
33
|
-
import { extractSystemMessages } from "../../utils/systemMessages.js";
|
|
34
27
|
const genTracer = tracers.generation;
|
|
35
28
|
/**
|
|
36
29
|
* Safely preview-serialize a value for debug logging.
|
|
@@ -98,112 +91,9 @@ export function resolveTurnBudget(options, turnStartMs) {
|
|
|
98
91
|
const turnDeadline = turnBudgetMs ? turnStartMs + turnBudgetMs : undefined;
|
|
99
92
|
return { callerTimeoutMs, turnBudgetMs, wrapupLeadMs, turnDeadline };
|
|
100
93
|
}
|
|
101
|
-
/**
|
|
102
|
-
* Merge the per-call providerOptions namespaces for generateText. Both the
|
|
103
|
-
* timeout forwarding (`neurolink.timeoutMs`, read by NeuroLink's delegating
|
|
104
|
-
* chat-completions models) and Gemini thinking (`google.thinkingConfig`) may
|
|
105
|
-
* apply on the same call — built here as ONE object because two conditional
|
|
106
|
-
* `providerOptions:` spreads in the args literal would silently clobber each
|
|
107
|
-
* other (object spread does not deep-merge).
|
|
108
|
-
*/
|
|
109
|
-
function buildProviderOptions(options, isGoogleProvider, callerTimeoutMs, finalResultSchema) {
|
|
110
|
-
const providerOptions = {};
|
|
111
|
-
if (callerTimeoutMs !== undefined) {
|
|
112
|
-
providerOptions.neurolink = { timeoutMs: callerTimeoutMs };
|
|
113
|
-
}
|
|
114
|
-
if (finalResultSchema) {
|
|
115
|
-
providerOptions.anthropic = { finalResultSchema };
|
|
116
|
-
}
|
|
117
|
-
if (options.thinkingConfig?.enabled && isGoogleProvider) {
|
|
118
|
-
// Gemini 3 uses thinkingLevel; Gemini 2.5 uses thinkingBudget.
|
|
119
|
-
providerOptions.google = {
|
|
120
|
-
thinkingConfig: {
|
|
121
|
-
...(options.thinkingConfig.thinkingLevel && {
|
|
122
|
-
thinkingLevel: options.thinkingConfig.thinkingLevel,
|
|
123
|
-
}),
|
|
124
|
-
...(options.thinkingConfig.budgetTokens &&
|
|
125
|
-
!options.thinkingConfig.thinkingLevel && {
|
|
126
|
-
thinkingBudget: options.thinkingConfig.budgetTokens,
|
|
127
|
-
}),
|
|
128
|
-
includeThoughts: true,
|
|
129
|
-
},
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
return Object.keys(providerOptions).length > 0
|
|
133
|
-
? providerOptions
|
|
134
|
-
: undefined;
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Build the prepareStep result for a forced wrap-up step: tools withdrawn
|
|
138
|
-
* (toolChoice: "none") plus an honest time message (native-loop parity) —
|
|
139
|
-
* without the message, weaker models keep trying to emit tool calls and leak
|
|
140
|
-
* raw tool-call tokens into the text answer.
|
|
141
|
-
*/
|
|
142
|
-
function buildWrapupStepResult(prepared, stepMessages) {
|
|
143
|
-
const baseMessages = prepared?.messages ??
|
|
144
|
-
stepMessages;
|
|
145
|
-
return {
|
|
146
|
-
...(prepared ?? {}),
|
|
147
|
-
messages: [
|
|
148
|
-
...baseMessages,
|
|
149
|
-
{
|
|
150
|
-
role: "user",
|
|
151
|
-
content: "The time budget for this task is nearly exhausted. Do not call any more tools. Give your best final answer NOW from the information already gathered, and note anything you could not verify in the remaining time.",
|
|
152
|
-
},
|
|
153
|
-
],
|
|
154
|
-
toolChoice: "none",
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
94
|
/**
|
|
158
95
|
* GenerationHandler class - Handles text generation operations for AI providers
|
|
159
96
|
*/
|
|
160
|
-
/**
|
|
161
|
-
* Append a schema-derived JSON instruction to the hoisted system prompt for
|
|
162
|
-
* the structured-output fallback retry. Phrased for tool loops too: the model
|
|
163
|
-
* may still call tools first; only the final answer must be the object.
|
|
164
|
-
*/
|
|
165
|
-
function appendJsonInstruction(system, schema) {
|
|
166
|
-
const jsonSchema = JSON.stringify(convertZodToJsonSchema(schema));
|
|
167
|
-
const instruction = "When you give your final answer, respond with only a single JSON object that conforms to the following JSON Schema. " +
|
|
168
|
-
"No prose before or after it, and no markdown code fence. " +
|
|
169
|
-
`JSON Schema: ${jsonSchema}`;
|
|
170
|
-
const existing = system ?? [];
|
|
171
|
-
const last = existing[existing.length - 1];
|
|
172
|
-
// Merge into the caller's own system message when there is one, the way
|
|
173
|
-
// messageBuilder folds STRUCTURED_OUTPUT_INSTRUCTIONS into a single system
|
|
174
|
-
// turn — several self-hosted OpenAI-compatible stacks only honour one
|
|
175
|
-
// leading system message.
|
|
176
|
-
if (last !== undefined && typeof last.content === "string") {
|
|
177
|
-
return [
|
|
178
|
-
...existing.slice(0, -1),
|
|
179
|
-
{ ...last, content: `${last.content}\n\n${instruction}` },
|
|
180
|
-
];
|
|
181
|
-
}
|
|
182
|
-
return [...existing, { role: "system", content: instruction }];
|
|
183
|
-
}
|
|
184
|
-
/** Sum two AI-SDK usage records leaf by leaf (the v6 shape nests token
|
|
185
|
-
* details); undefined counts are treated as absent, not zero. */
|
|
186
|
-
function addUsage(a, b) {
|
|
187
|
-
const sum = (x, y) => {
|
|
188
|
-
if (typeof x === "number" || typeof y === "number") {
|
|
189
|
-
return (typeof x === "number" ? x : 0) + (typeof y === "number" ? y : 0);
|
|
190
|
-
}
|
|
191
|
-
if (x && y && typeof x === "object" && typeof y === "object") {
|
|
192
|
-
const left = x;
|
|
193
|
-
const right = y;
|
|
194
|
-
const out = {};
|
|
195
|
-
for (const key of new Set([
|
|
196
|
-
...Object.keys(left),
|
|
197
|
-
...Object.keys(right),
|
|
198
|
-
])) {
|
|
199
|
-
out[key] = sum(left[key], right[key]);
|
|
200
|
-
}
|
|
201
|
-
return out;
|
|
202
|
-
}
|
|
203
|
-
return x ?? y;
|
|
204
|
-
};
|
|
205
|
-
return sum(a, b);
|
|
206
|
-
}
|
|
207
97
|
export class GenerationHandler {
|
|
208
98
|
providerName;
|
|
209
99
|
modelName;
|
|
@@ -238,247 +128,23 @@ export class GenerationHandler {
|
|
|
238
128
|
* Helper method to call generateText with optional structured output
|
|
239
129
|
* @private
|
|
240
130
|
*/
|
|
241
|
-
async callGenerateText(model, messages, tools, options, callConfig) {
|
|
242
|
-
const { shouldUseTools, includeStructuredOutput, turnStartMs, promptJsonInstruction, } = callConfig;
|
|
243
|
-
// Check if this is a Google provider (for provider-specific options)
|
|
244
|
-
const isGoogleProvider = this.providerName === "google-ai" || this.providerName === "vertex";
|
|
245
|
-
// Check if this is an Anthropic provider (includes Vertex+Claude)
|
|
246
|
-
const isAnthropicProvider = this.providerName === "anthropic" ||
|
|
247
|
-
this.providerName === "bedrock" ||
|
|
248
|
-
(this.providerName === "vertex" && this.modelName?.startsWith("claude-"));
|
|
249
|
-
// Gemini 2.5 and earlier cannot use tools + structured JSON output simultaneously.
|
|
250
|
-
// When both are requested on a Google provider, disable structured output (tools take priority).
|
|
251
|
-
const wantsStructuredOutput = includeStructuredOutput &&
|
|
252
|
-
(!!options.schema ||
|
|
253
|
-
options.output?.format === "json" ||
|
|
254
|
-
options.output?.format === "structured");
|
|
255
|
-
// The tools↔schema conflict is a Gemini-only API limitation. Vertex+Claude
|
|
256
|
-
// supports both simultaneously, so only exclude for actual Gemini models.
|
|
257
|
-
const useStructuredOutput = wantsStructuredOutput &&
|
|
258
|
-
!isToolsSchemaExclusionInForce(this.providerName, this.modelName, shouldUseTools, Object.keys(tools).length);
|
|
259
|
-
// Annotate the last tool with cache_control so the full tool-definition
|
|
260
|
-
// block becomes a cache breakpoint for Anthropic-family providers.
|
|
261
|
-
// Non-Anthropic providers harmlessly ignore unknown providerOptions.
|
|
262
|
-
// Note: The AI SDK Tool type doesn't yet include providerOptions, so we
|
|
263
|
-
// use a type assertion. The Anthropic adapter reads this at runtime.
|
|
264
|
-
//
|
|
265
|
-
// Deliberately NOT a clone: the record is call-scoped (built fresh in
|
|
266
|
-
// BaseProvider.prepareGenerationContext) and the AI SDK re-reads it on
|
|
267
|
-
// every agent-loop step, so `search_tools` hydration (tools.discovery)
|
|
268
|
-
// can add discovered tools mid-loop and have them callable on the next
|
|
269
|
-
// step. A clone would freeze the tool set for the whole call.
|
|
270
|
-
const toolsWithCache = tools;
|
|
271
|
-
if (isAnthropicProvider &&
|
|
272
|
-
shouldUseTools &&
|
|
273
|
-
Object.keys(toolsWithCache).length > 0) {
|
|
274
|
-
const toolNames = Object.keys(toolsWithCache);
|
|
275
|
-
const lastToolName = toolNames[toolNames.length - 1];
|
|
276
|
-
if (lastToolName && toolsWithCache[lastToolName]) {
|
|
277
|
-
const lastTool = toolsWithCache[lastToolName];
|
|
278
|
-
toolsWithCache[lastToolName] = {
|
|
279
|
-
...lastTool,
|
|
280
|
-
providerOptions: {
|
|
281
|
-
...(lastTool.providerOptions ?? {}),
|
|
282
|
-
anthropic: { cacheControl: { type: "ephemeral" } },
|
|
283
|
-
},
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
const prepareStep = options.prepareStep;
|
|
288
|
-
const { callerTimeoutMs, turnBudgetMs, wrapupLeadMs, turnDeadline } = resolveTurnBudget(options, turnStartMs);
|
|
289
|
-
let wrapupForced = false;
|
|
290
|
-
// The native Anthropic Messages surface cannot combine AI-SDK structured
|
|
291
|
-
// output with tools (see structuredOutputPolicy — experimental_output
|
|
292
|
-
// replaces the tools array), so `useStructuredOutput` is false above and
|
|
293
|
-
// the schema would simply be dropped for every agent/MCP turn. Hand the
|
|
294
|
-
// JSON Schema to the provider instead: it appends an additive
|
|
295
|
-
// `final_result` tool and returns the answer as that tool's arguments,
|
|
296
|
-
// keeping the real tools callable. Bedrock is deliberately excluded — it
|
|
297
|
-
// talks to the raw AWS SDK directly, not an ai-sdk provider package, and
|
|
298
|
-
// has no such handling.
|
|
299
|
-
const finalResultSchema = this.providerName === "anthropic" &&
|
|
300
|
-
!!options.schema &&
|
|
301
|
-
shouldUseTools &&
|
|
302
|
-
Object.keys(tools).length > 0
|
|
303
|
-
? convertZodToJsonSchema(options.schema)
|
|
304
|
-
: undefined;
|
|
305
|
-
const providerOptions = buildProviderOptions(options, isGoogleProvider, callerTimeoutMs, finalResultSchema);
|
|
306
|
-
// Hoist system-role messages into generateText's top-level `system` option
|
|
307
|
-
// rather than passing them inside `messages` (deprecated by the AI SDK,
|
|
308
|
-
// rejected in v7). See extractSystemMessages for the rationale. (#1024)
|
|
309
|
-
const { system: hoistedSystem, messages: nonSystemMessages } = extractSystemMessages(messages);
|
|
310
|
-
// Structured-output fallback: spell the schema out in the prompt. The
|
|
311
|
-
// native `response_format` attempt already failed to yield an object, and
|
|
312
|
-
// a vendor that ignores `response_format` outright (GMI Cloud's MiniMax
|
|
313
|
-
// endpoint answers a strict json_schema request in markdown) would answer
|
|
314
|
-
// a silent retry the same way. With the schema in the system prompt the
|
|
315
|
-
// same text-coercion path in formatEnhancedResult recovers a schema-valid
|
|
316
|
-
// object — verified live on that endpoint, 3/3.
|
|
317
|
-
const system = promptJsonInstruction && options.schema
|
|
318
|
-
? appendJsonInstruction(hoistedSystem, options.schema)
|
|
319
|
-
: hoistedSystem;
|
|
320
|
-
// Per-step context budget guard: the tool loop appends assistant turns and
|
|
321
|
-
// tool results on every step — growth the pre-call budget check never
|
|
322
|
-
// sees. Estimate each step's projected request and deterministically
|
|
323
|
-
// reclaim budget (truncate old tool outputs, then drop oldest exchanges)
|
|
324
|
-
// so long agentic runs cannot overflow the model's window mid-loop.
|
|
325
|
-
// Parity with the googleVertex native loops' createContextGuard, upgraded
|
|
326
|
-
// from stop-only to compact-and-continue. The caller's prepareStep result
|
|
327
|
-
// wins on conflicts; the guard only contributes `messages`.
|
|
328
|
-
//
|
|
329
|
-
// Overhead is resolved PER STEP because `toolsWithCache` is deliberately
|
|
330
|
-
// mutable (search_tools hydration adds discovered tools mid-loop) — a
|
|
331
|
-
// once-captured estimate would undercount later steps. Tools are only
|
|
332
|
-
// ever added, so memoizing on tool count keeps the common step O(1).
|
|
333
|
-
let cachedOverhead = { toolCount: -1, tokens: 0 };
|
|
334
|
-
const stepBudgetGuard = createStepBudgetGuard({
|
|
335
|
-
provider: this.providerName ?? "unknown",
|
|
336
|
-
model: this.modelName,
|
|
337
|
-
maxTokens: options.maxTokens,
|
|
338
|
-
getFixedOverheadTokens: () => {
|
|
339
|
-
const toolCount = shouldUseTools
|
|
340
|
-
? Object.keys(toolsWithCache).length
|
|
341
|
-
: 0;
|
|
342
|
-
if (toolCount !== cachedOverhead.toolCount) {
|
|
343
|
-
cachedOverhead = {
|
|
344
|
-
toolCount,
|
|
345
|
-
tokens: estimateFixedOverheadTokens(system, shouldUseTools ? toolsWithCache : undefined, this.providerName),
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
return cachedOverhead.tokens;
|
|
349
|
-
},
|
|
350
|
-
});
|
|
351
|
-
// Registry-driven strip: models that reject sampling params (Sonnet 5 /
|
|
352
|
-
// Opus 4.7+ / Fable 5 families — e.g. Claude on Bedrock or behind any
|
|
353
|
-
// AI-SDK provider) must not receive temperature. Applies uniformly to
|
|
354
|
-
// every provider on this loop path; the reactive
|
|
355
|
-
// isTemperatureDeprecatedError retry below remains the safety net.
|
|
356
|
-
const samplingParams = resolveSamplingParams(this.providerName, getModelId(model, this.modelName || ""), options.temperature !== undefined
|
|
357
|
-
? { temperature: options.temperature }
|
|
358
|
-
: {}, "aiSdk.generateText");
|
|
359
|
-
const result = await (this.deps.generateTextFn ?? generateText)({
|
|
360
|
-
model,
|
|
361
|
-
...(system && { system }),
|
|
362
|
-
messages: nonSystemMessages,
|
|
363
|
-
...(shouldUseTools &&
|
|
364
|
-
Object.keys(toolsWithCache).length > 0 && { tools: toolsWithCache }),
|
|
365
|
-
stopWhen: stepCountIs(options.maxSteps ?? DEFAULT_MAX_STEPS),
|
|
366
|
-
...(shouldUseTools &&
|
|
367
|
-
options.toolChoice && { toolChoice: options.toolChoice }),
|
|
368
|
-
experimental_prepareStep: (async (stepOptions) => {
|
|
369
|
-
// Public contract preserved: a caller-supplied prepareStep receives
|
|
370
|
-
// the ORIGINAL AI-SDK step options, exactly as before the guard
|
|
371
|
-
// existed — callers that inspect message history see the real thing.
|
|
372
|
-
const callerResult = prepareStep
|
|
373
|
-
? await prepareStep({
|
|
374
|
-
...stepOptions,
|
|
375
|
-
maxSteps: options.maxSteps ?? DEFAULT_MAX_STEPS,
|
|
376
|
-
})
|
|
377
|
-
: undefined;
|
|
378
|
-
// The guard runs LAST, on the messages that will actually be sent:
|
|
379
|
-
// the caller's override when one was returned (out-of-contract for
|
|
380
|
-
// NeuroLink's public prepareStep type, but possible at runtime), else
|
|
381
|
-
// the step's own messages. It never replaces a caller's content
|
|
382
|
-
// choices — it only reclaims budget from whatever was chosen.
|
|
383
|
-
const callerMessages = callerResult?.messages;
|
|
384
|
-
// Usage feedback: the provider's REAL prompt-token count for the
|
|
385
|
-
// previous step calibrates the guard's char-based estimator (see
|
|
386
|
-
// createStepBudgetGuard) — free precision, no tokenizer.
|
|
387
|
-
const previousStep = stepOptions.steps[stepOptions.steps.length - 1];
|
|
388
|
-
const compacted = stepBudgetGuard(callerMessages ?? stepOptions.messages, previousStep?.usage?.inputTokens);
|
|
389
|
-
const prepared = compacted
|
|
390
|
-
? { ...(callerResult ?? {}), messages: compacted }
|
|
391
|
-
: callerResult;
|
|
392
|
-
// Wrap-up: inside the lead window before the turn deadline, stop
|
|
393
|
-
// offering tools so this step produces the final answer. Overrides
|
|
394
|
-
// any caller toolChoice — an honest partial beats a discarded turn.
|
|
395
|
-
if (turnDeadline !== undefined &&
|
|
396
|
-
shouldUseTools &&
|
|
397
|
-
Date.now() >= turnDeadline - wrapupLeadMs) {
|
|
398
|
-
if (!wrapupForced) {
|
|
399
|
-
wrapupForced = true;
|
|
400
|
-
logger.warn("[GenerationHandler] Turn budget nearly exhausted — forcing wrap-up (toolChoice: none)", {
|
|
401
|
-
provider: this.providerName,
|
|
402
|
-
turnBudgetMs,
|
|
403
|
-
wrapupLeadMs,
|
|
404
|
-
stepNumber: stepOptions.stepNumber,
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
return buildWrapupStepResult(prepared, stepOptions.messages);
|
|
408
|
-
}
|
|
409
|
-
return prepared;
|
|
410
|
-
}),
|
|
411
|
-
temperature: samplingParams.temperature,
|
|
412
|
-
maxOutputTokens: options.maxTokens,
|
|
413
|
-
maxRetries: 0, // NL11: Disable AI SDK's invisible internal retries; we handle retries with OTel instrumentation
|
|
414
|
-
abortSignal: options.abortSignal,
|
|
415
|
-
// Schema-driven tool-call repair (BZ-665): fixes near-miss tool names
|
|
416
|
-
// (case/substring/Levenshtein) and — for tools whose schema carries a
|
|
417
|
-
// validator — coerces mis-typed arguments ("123" → 123) and remaps
|
|
418
|
-
// near-miss parameter names before the call is marked invalid. Wired
|
|
419
|
-
// for every AI-SDK-loop provider; native loops have their own paths.
|
|
420
|
-
...(shouldUseTools &&
|
|
421
|
-
!options.disableToolCallRepair && {
|
|
422
|
-
experimental_repairToolCall: (async (...repairArgs) => {
|
|
423
|
-
// Lazy import to avoid a circular dependency at module load time
|
|
424
|
-
const { createToolCallRepair } = await import("../../utils/toolCallRepair.js");
|
|
425
|
-
return createToolCallRepair()(...repairArgs);
|
|
426
|
-
}),
|
|
427
|
-
}),
|
|
428
|
-
// Forward the caller's resolved timeout to the model layer: the AI-SDK
|
|
429
|
-
// V3 call options carry no `timeout`, so delegating chat-completions
|
|
430
|
-
// models (litellm & friends) could otherwise only ever apply their
|
|
431
|
-
// provider default per step — an explicit `timeout: "15m"` bounded the
|
|
432
|
-
// outer loop while each step stayed capped at the default.
|
|
433
|
-
// Merged namespaces (neurolink timeout forwarding + Gemini thinking) —
|
|
434
|
-
// built as ONE object; see buildProviderOptions.
|
|
435
|
-
...(providerOptions && { providerOptions }),
|
|
436
|
-
...(useStructuredOutput &&
|
|
437
|
-
options.schema && {
|
|
438
|
-
experimental_output: Output.object({ schema: options.schema }),
|
|
439
|
-
}),
|
|
440
|
-
// Anthropic thinking: experimental_thinking with budgetTokens.
|
|
441
|
-
// (Gemini thinking rides providerOptions.google above.)
|
|
442
|
-
...(options.thinkingConfig?.enabled &&
|
|
443
|
-
isAnthropicProvider &&
|
|
444
|
-
options.thinkingConfig.budgetTokens &&
|
|
445
|
-
!options.thinkingConfig.thinkingLevel && {
|
|
446
|
-
experimental_thinking: {
|
|
447
|
-
type: "enabled",
|
|
448
|
-
budgetTokens: options.thinkingConfig.budgetTokens,
|
|
449
|
-
},
|
|
450
|
-
}),
|
|
451
|
-
experimental_telemetry: this.getTelemetryConfigFn(options, "generate"),
|
|
452
|
-
onStepFinish: ({ toolCalls, toolResults }) => {
|
|
453
|
-
logger.info("Tool execution completed", { toolResults, toolCalls });
|
|
454
|
-
// Emit tool:end events for Pipeline B (metrics aggregator).
|
|
455
|
-
// This surfaces AI-SDK-driven tool completions as telemetry events
|
|
456
|
-
// so that tool spans are created even when the SDK runs tools
|
|
457
|
-
// internally (gaps G5 / S2).
|
|
458
|
-
emitToolEndFromStepFinish(this.deps.getEmitterFn?.(), toolResults);
|
|
459
|
-
// Handle tool execution storage
|
|
460
|
-
this.handleToolStorageFn(toolCalls, toolResults, options, new Date()).catch((error) => {
|
|
461
|
-
logger.warn("[GenerationHandler] Failed to store tool executions", {
|
|
462
|
-
provider: this.providerName,
|
|
463
|
-
error: error instanceof Error ? error.message : String(error),
|
|
464
|
-
});
|
|
465
|
-
});
|
|
466
|
-
},
|
|
467
|
-
});
|
|
468
|
-
if (wrapupForced) {
|
|
469
|
-
// Non-enumerable marker read by formatEnhancedResult to report
|
|
470
|
-
// stopReason "time-limit" — the result object itself is the only
|
|
471
|
-
// artifact that travels from this call to result formatting.
|
|
472
|
-
Object.defineProperty(result, "__nlTurnWrapup", {
|
|
473
|
-
value: true,
|
|
474
|
-
enumerable: false,
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
return result;
|
|
478
|
-
}
|
|
479
131
|
/**
|
|
480
|
-
*
|
|
132
|
+
* The ai-package generate loop.
|
|
133
|
+
*
|
|
134
|
+
* Unreachable: every text provider now implements a native generate() and
|
|
135
|
+
* none of them return here. That was established by trapping the seam —
|
|
136
|
+
* replacing the ai package's generateText with a throwing stub left the full
|
|
137
|
+
* provider matrix passing and zero cells reaching it — and non-text request
|
|
138
|
+
* kinds return from runGenerateInActiveContext before this handler is
|
|
139
|
+
* consulted.
|
|
140
|
+
*
|
|
141
|
+
* Kept as an explicit failure rather than deleted outright so a provider
|
|
142
|
+
* added without a native generate() fails loudly here instead of silently
|
|
143
|
+
* reintroducing a dependency on the removed package.
|
|
481
144
|
*/
|
|
145
|
+
async callGenerateText(_model, _messages, _tools, _options, _callConfig) {
|
|
146
|
+
throw new Error("GenerationHandler.callGenerateText is no longer implemented: every provider must supply a native generate(). See docs/plans/2026-09-03-completing-the-ai-sdk-removal.md");
|
|
147
|
+
}
|
|
482
148
|
async executeGeneration(model, messages, tools, options) {
|
|
483
149
|
return genTracer.startActiveSpan("neurolink.executeGeneration", { kind: SpanKind.INTERNAL }, async (span) => {
|
|
484
150
|
const shouldUseTools = !options.disableTools && this.supportsToolsFn();
|
|
@@ -566,19 +232,12 @@ export class GenerationHandler {
|
|
|
566
232
|
});
|
|
567
233
|
}
|
|
568
234
|
// Set token usage and completion attributes on span
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
shouldUseTools,
|
|
573
|
-
includeStructuredOutput: true,
|
|
574
|
-
turnStartMs: genStartTime,
|
|
575
|
-
}, { result, span });
|
|
576
|
-
this.setUsageSpanAttributes(span, recovered);
|
|
577
|
-
if (recovered.finishReason) {
|
|
578
|
-
span.setAttribute("gen_ai.response.finish_reason", recovered.finishReason);
|
|
235
|
+
this.setUsageSpanAttributes(span, result);
|
|
236
|
+
if (result.finishReason) {
|
|
237
|
+
span.setAttribute("gen_ai.response.finish_reason", result.finishReason);
|
|
579
238
|
}
|
|
580
239
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
581
|
-
return
|
|
240
|
+
return result;
|
|
582
241
|
}
|
|
583
242
|
catch (error) {
|
|
584
243
|
// Fall back to text-mode (no experimental_output) when structured
|
|
@@ -631,13 +290,11 @@ export class GenerationHandler {
|
|
|
631
290
|
// includeStructuredOutput intentionally omitted
|
|
632
291
|
includeStructuredOutput: false,
|
|
633
292
|
turnStartMs: genStartTime,
|
|
634
|
-
promptJsonInstruction: true,
|
|
635
293
|
}), span, "generateText(fallback)");
|
|
636
294
|
// NLK-GAP-007: Record recovery event after successful fallback
|
|
637
295
|
span.addEvent("retry.recovered", {
|
|
638
296
|
"retry.attempts": 2,
|
|
639
297
|
"retry.strategy": "structured_output_disabled",
|
|
640
|
-
"retry.prompt_json_instruction": true,
|
|
641
298
|
});
|
|
642
299
|
span.setAttribute("retry.count", 1);
|
|
643
300
|
logger.info("[GenerationHandler] generateText returned (fallback)", {
|
|
@@ -648,20 +305,12 @@ export class GenerationHandler {
|
|
|
648
305
|
toolCallsTotal: result.toolCalls?.length || 0,
|
|
649
306
|
responseChars: result.text?.length || 0,
|
|
650
307
|
});
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
shouldUseTools,
|
|
655
|
-
includeStructuredOutput: false,
|
|
656
|
-
turnStartMs: genStartTime,
|
|
657
|
-
promptJsonInstruction: true,
|
|
658
|
-
}, { result, span });
|
|
659
|
-
this.setUsageSpanAttributes(span, recovered);
|
|
660
|
-
if (recovered.finishReason) {
|
|
661
|
-
span.setAttribute("gen_ai.response.finish_reason", recovered.finishReason);
|
|
308
|
+
this.setUsageSpanAttributes(span, result);
|
|
309
|
+
if (result.finishReason) {
|
|
310
|
+
span.setAttribute("gen_ai.response.finish_reason", result.finishReason);
|
|
662
311
|
}
|
|
663
312
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
664
|
-
return
|
|
313
|
+
return result;
|
|
665
314
|
}
|
|
666
315
|
// Retry once without `temperature` when the model deprecated it. The
|
|
667
316
|
// newest Anthropic models (e.g. claude-opus-4-8 with tools + advanced
|
|
@@ -693,19 +342,12 @@ export class GenerationHandler {
|
|
|
693
342
|
"retry.strategy": "temperature_omitted",
|
|
694
343
|
});
|
|
695
344
|
span.setAttribute("retry.count", 1);
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
shouldUseTools,
|
|
700
|
-
includeStructuredOutput: true,
|
|
701
|
-
turnStartMs: genStartTime,
|
|
702
|
-
}, { result, span });
|
|
703
|
-
this.setUsageSpanAttributes(span, recovered);
|
|
704
|
-
if (recovered.finishReason) {
|
|
705
|
-
span.setAttribute("gen_ai.response.finish_reason", recovered.finishReason);
|
|
345
|
+
this.setUsageSpanAttributes(span, result);
|
|
346
|
+
if (result.finishReason) {
|
|
347
|
+
span.setAttribute("gen_ai.response.finish_reason", result.finishReason);
|
|
706
348
|
}
|
|
707
349
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
708
|
-
return
|
|
350
|
+
return result;
|
|
709
351
|
}
|
|
710
352
|
span.setStatus({
|
|
711
353
|
code: SpanStatusCode.ERROR,
|
|
@@ -855,83 +497,6 @@ export class GenerationHandler {
|
|
|
855
497
|
/**
|
|
856
498
|
* Format the enhanced result
|
|
857
499
|
*/
|
|
858
|
-
/**
|
|
859
|
-
* One re-ask with `toolChoice: "none"` when a tool loop ends on a
|
|
860
|
-
* `tool-calls` finish that carries neither a tool call nor any text.
|
|
861
|
-
*
|
|
862
|
-
* io.net's Llama endpoint does exactly this on the step after a tool result
|
|
863
|
-
* when the caller asked for JSON: the model's JSON-shaped answer trips the
|
|
864
|
-
* vendor's tool-call parser, which drops it and reports
|
|
865
|
-
* `finish_reason: tool_calls` with `content: null` and no `tool_calls`.
|
|
866
|
-
* The AI-SDK loop has nothing to execute and stops, so the caller gets an
|
|
867
|
-
* empty turn although the tool ran. Replaying that request with
|
|
868
|
-
* `tool_choice: "none"` (or no tool list) returns the answer — verified on
|
|
869
|
-
* the wire, 4/4 — so the recovery is one bounded extra step that keeps the
|
|
870
|
-
* executed tool steps and usage in the returned result.
|
|
871
|
-
*/
|
|
872
|
-
async recoverEmptyToolCallsFinish(model, messages, tools, options, callConfig, attempt) {
|
|
873
|
-
const { result, span } = attempt;
|
|
874
|
-
// The re-ask is one more loop step, so it must fit the caller's step
|
|
875
|
-
// budget: when the loop already spent every step, the honest answer is
|
|
876
|
-
// the step-cap stop the caller configured, not an extra request.
|
|
877
|
-
const maxSteps = options.maxSteps ?? DEFAULT_MAX_STEPS;
|
|
878
|
-
const emptyToolCallsFinish = callConfig.shouldUseTools &&
|
|
879
|
-
!callConfig.isToolReask &&
|
|
880
|
-
Object.keys(tools).length > 0 &&
|
|
881
|
-
result.finishReason === "tool-calls" &&
|
|
882
|
-
result.toolCalls.length === 0 &&
|
|
883
|
-
(result.text ?? "").trim().length === 0 &&
|
|
884
|
-
result.steps.length < maxSteps;
|
|
885
|
-
if (!emptyToolCallsFinish) {
|
|
886
|
-
return result;
|
|
887
|
-
}
|
|
888
|
-
logger.warn("[GenerationHandler] tool loop ended on a tool-calls finish with no tool call and no text — re-asking once with toolChoice: none", {
|
|
889
|
-
provider: this.providerName,
|
|
890
|
-
model: this.modelName,
|
|
891
|
-
stepsSoFar: result.steps.length,
|
|
892
|
-
});
|
|
893
|
-
span.setAttribute("neurolink.has_fallback", true);
|
|
894
|
-
span.addEvent("retry.initial_failure", {
|
|
895
|
-
"retry.attempt": 1,
|
|
896
|
-
"retry.reason": "empty_tool_calls_finish",
|
|
897
|
-
});
|
|
898
|
-
let reask;
|
|
899
|
-
try {
|
|
900
|
-
reask = await withProviderRetry(() => this.callGenerateText(model, [...messages, ...result.response.messages], tools, { ...options, toolChoice: "none", maxSteps: 1 }, { ...callConfig, isToolReask: true }), span, "generateText(tool-choice-none)");
|
|
901
|
-
}
|
|
902
|
-
catch (reaskError) {
|
|
903
|
-
// The re-ask is a bonus request on top of a call that already
|
|
904
|
-
// succeeded. If it fails for any reason, hand back the original result
|
|
905
|
-
// — the executed tool steps are still in it — rather than turning a
|
|
906
|
-
// degraded turn into a thrown one.
|
|
907
|
-
logger.warn("[GenerationHandler] toolChoice: none re-ask failed; returning the original result", {
|
|
908
|
-
provider: this.providerName,
|
|
909
|
-
model: this.modelName,
|
|
910
|
-
error: reaskError instanceof Error
|
|
911
|
-
? reaskError.message
|
|
912
|
-
: String(reaskError),
|
|
913
|
-
});
|
|
914
|
-
span.addEvent("retry.failed", {
|
|
915
|
-
"retry.attempts": 2,
|
|
916
|
-
"retry.strategy": "tool_choice_none_reask",
|
|
917
|
-
});
|
|
918
|
-
return result;
|
|
919
|
-
}
|
|
920
|
-
span.addEvent("retry.recovered", {
|
|
921
|
-
"retry.attempts": 2,
|
|
922
|
-
"retry.strategy": "tool_choice_none_reask",
|
|
923
|
-
});
|
|
924
|
-
span.setAttribute("retry.count", 1);
|
|
925
|
-
// Keep the executed tool steps in front of the re-ask so
|
|
926
|
-
// extractToolInformation still reports them and usage stays a true
|
|
927
|
-
// cross-step total. `steps` and `totalUsage` are constructor-assigned
|
|
928
|
-
// fields on the AI-SDK result; every other accessor derives from the
|
|
929
|
-
// final step, which is now the re-ask's answer.
|
|
930
|
-
const merged = reask;
|
|
931
|
-
merged.steps = [...result.steps, ...reask.steps];
|
|
932
|
-
merged.totalUsage = addUsage(result.totalUsage, reask.totalUsage);
|
|
933
|
-
return reask;
|
|
934
|
-
}
|
|
935
500
|
formatEnhancedResult(generateResult, tools, toolsUsed, toolExecutions, options) {
|
|
936
501
|
// Structured output check — schema alone is sufficient to activate
|
|
937
502
|
const useStructuredOutput = !!options.schema ||
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The multi-step tool loop that the ai package's `generateText` used to supply.
|
|
3
|
+
*
|
|
4
|
+
* It deliberately loops over a provider's own `doGenerate` rather than over any
|
|
5
|
+
* streaming machinery. That is the whole lesson of the reverted first attempt:
|
|
6
|
+
* `doGenerate` is where the JSON-versus-SSE wire choice lives, along with the
|
|
7
|
+
* 400 retry, the context-overflow refit and the provider's own structured-output
|
|
8
|
+
* handling. Looping around the streaming path instead silently changed
|
|
9
|
+
* generate() to send `stream: true` and broke ten providers against a
|
|
10
|
+
* non-streaming body.
|
|
11
|
+
*
|
|
12
|
+
* Every provider whose delegating model exposes a v3-shaped `doGenerate` can
|
|
13
|
+
* share this: the v3 result shape (`content` parts, `finishReason`, `usage`) is
|
|
14
|
+
* the same across Anthropic, the OpenAI-compatible family and SageMaker.
|
|
15
|
+
*/
|
|
16
|
+
import type { NativeGenerateLoopArgs, NativeGenerateLoopResult, ToolExecutionSummaryInternal } from "../types/index.js";
|
|
17
|
+
/**
|
|
18
|
+
* Narrow a model handle to the delegating shape this loop drives.
|
|
19
|
+
* `LanguageModel` is a union that includes a bare string id, and a double
|
|
20
|
+
* assertion through unknown is banned by Critical Rule 14.
|
|
21
|
+
*/
|
|
22
|
+
export declare const hasNativeDoGenerate: (value: unknown) => value is {
|
|
23
|
+
doGenerate: (options: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Spell a JSON Schema into the conversation's system turn.
|
|
27
|
+
*
|
|
28
|
+
* The structured-output fallback for vendors that reject or ignore
|
|
29
|
+
* `response_format`. Merged into an existing trailing system message rather
|
|
30
|
+
* than appended as a second one: several self-hosted OpenAI-compatible stacks
|
|
31
|
+
* honour only the first system message, so a second would be dropped and the
|
|
32
|
+
* fallback would silently do nothing.
|
|
33
|
+
*/
|
|
34
|
+
export declare const appendJsonSchemaInstruction: (conversation: Array<Record<string, unknown>>, schema: unknown) => Array<Record<string, unknown>>;
|
|
35
|
+
export declare function runNativeGenerateLoop(args: NativeGenerateLoopArgs, toolExecutionSummaries: ToolExecutionSummaryInternal[]): Promise<NativeGenerateLoopResult>;
|