@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.
Files changed (52) hide show
  1. package/CHANGELOG.md +3 -4
  2. package/dist/browser/neurolink.min.js +533 -581
  3. package/dist/constants/enums.d.ts +13 -0
  4. package/dist/constants/enums.js +14 -0
  5. package/dist/core/baseProvider.d.ts +71 -3
  6. package/dist/core/baseProvider.js +152 -44
  7. package/dist/core/modules/GenerationHandler.d.ts +22 -24
  8. package/dist/core/modules/GenerationHandler.js +28 -463
  9. package/dist/core/nativeGenerateLoop.d.ts +35 -0
  10. package/dist/core/nativeGenerateLoop.js +261 -0
  11. package/dist/files/fileTools.d.ts +5 -5
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.js +4 -0
  14. package/dist/middleware/builtin/guardrails.d.ts +0 -5
  15. package/dist/middleware/builtin/guardrails.js +33 -5
  16. package/dist/middleware/factory.js +1 -1
  17. package/dist/middleware/wrapLanguageModel.d.ts +18 -0
  18. package/dist/middleware/wrapLanguageModel.js +53 -0
  19. package/dist/processors/media/AudioProcessor.js +46 -11
  20. package/dist/providers/amazonSagemaker.d.ts +17 -1
  21. package/dist/providers/amazonSagemaker.js +110 -0
  22. package/dist/providers/anthropic/client.d.ts +11 -0
  23. package/dist/providers/anthropic/client.js +148 -1
  24. package/dist/providers/catalog/index.generated.d.ts +1 -1
  25. package/dist/providers/catalog/index.generated.js +3 -0
  26. package/dist/providers/catalog/loader.js +1 -0
  27. package/dist/providers/catalog/mancer.json +192 -0
  28. package/dist/providers/configuredOpenAICompat.d.ts +11 -0
  29. package/dist/providers/configuredOpenAICompat.js +16 -0
  30. package/dist/providers/googleVertex/client.d.ts +0 -9
  31. package/dist/providers/googleVertex/client.js +0 -33
  32. package/dist/providers/openaiChatCompletionsBase.d.ts +21 -1
  33. package/dist/providers/openaiChatCompletionsBase.js +178 -0
  34. package/dist/providers/providerTypeUtils.d.ts +1 -2
  35. package/dist/providers/providerTypeUtils.js +5 -1
  36. package/dist/types/aiCompat.d.ts +485 -0
  37. package/dist/types/aiCompat.js +17 -0
  38. package/dist/types/conversation.d.ts +1 -1
  39. package/dist/types/generate.d.ts +52 -0
  40. package/dist/types/middleware.d.ts +3 -6
  41. package/dist/types/providerCatalog.generated.d.ts +2 -2
  42. package/dist/types/providers.d.ts +14 -1
  43. package/dist/types/tools.d.ts +2 -2
  44. package/dist/utils/generationErrors.d.ts +78 -6
  45. package/dist/utils/generationErrors.js +114 -6
  46. package/dist/utils/nativeSingleShot.d.ts +3 -0
  47. package/dist/utils/nativeSingleShot.js +83 -0
  48. package/dist/utils/tool.d.ts +30 -5
  49. package/dist/utils/tool.js +43 -5
  50. package/package.json +3 -6
  51. package/dist/utils/generation.d.ts +0 -8
  52. 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 { createStepBudgetGuard, estimateFixedOverheadTokens, } from "../../context/stepBudgetGuard.js";
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
- * Execute the generation with AI SDK
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
- // Span attributes come from the RECOVERED result: a successful
570
- // toolChoice:"none" re-ask changes the finish reason and adds usage.
571
- const recovered = await this.recoverEmptyToolCallsFinish(model, messages, tools, options, {
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 recovered;
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
- // Span attributes come from the RECOVERED result: a successful
652
- // toolChoice:"none" re-ask changes the finish reason and adds usage.
653
- const recovered = await this.recoverEmptyToolCallsFinish(model, messages, tools, options, {
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 recovered;
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
- // Span attributes come from the RECOVERED result: a successful
697
- // toolChoice:"none" re-ask changes the finish reason and adds usage.
698
- const recovered = await this.recoverEmptyToolCallsFinish(model, messages, tools, { ...options, temperature: undefined }, {
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 recovered;
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>;