@juspay/neurolink 9.69.3 → 9.70.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/browser/neurolink.min.js +355 -347
  3. package/dist/core/modules/GenerationHandler.js +75 -23
  4. package/dist/core/modules/structuredOutputPolicy.d.ts +28 -0
  5. package/dist/core/modules/structuredOutputPolicy.js +50 -0
  6. package/dist/lib/core/modules/GenerationHandler.js +75 -23
  7. package/dist/lib/core/modules/structuredOutputPolicy.d.ts +28 -0
  8. package/dist/lib/core/modules/structuredOutputPolicy.js +51 -0
  9. package/dist/lib/neurolink.js +58 -0
  10. package/dist/lib/providers/anthropic.js +34 -7
  11. package/dist/lib/providers/googleVertex.js +17 -2
  12. package/dist/lib/types/generate.d.ts +47 -19
  13. package/dist/lib/types/index.d.ts +1 -0
  14. package/dist/lib/types/index.js +1 -0
  15. package/dist/lib/types/livekit.d.ts +369 -0
  16. package/dist/lib/types/livekit.js +13 -0
  17. package/dist/lib/types/utilities.d.ts +16 -0
  18. package/dist/lib/utils/json/coerce.d.ts +10 -0
  19. package/dist/lib/utils/json/coerce.js +141 -0
  20. package/dist/lib/utils/json/extract.d.ts +10 -0
  21. package/dist/lib/utils/json/extract.js +61 -11
  22. package/dist/lib/utils/tokenLimits.d.ts +20 -0
  23. package/dist/lib/utils/tokenLimits.js +55 -0
  24. package/dist/lib/voice/livekit/brain.d.ts +21 -0
  25. package/dist/lib/voice/livekit/brain.js +75 -0
  26. package/dist/lib/voice/livekit/config.d.ts +41 -0
  27. package/dist/lib/voice/livekit/config.js +80 -0
  28. package/dist/lib/voice/livekit/eventBridge.d.ts +27 -0
  29. package/dist/lib/voice/livekit/eventBridge.js +360 -0
  30. package/dist/lib/voice/livekit/index.d.ts +15 -0
  31. package/dist/lib/voice/livekit/index.js +16 -0
  32. package/dist/lib/voice/livekit/tokens.d.ts +19 -0
  33. package/dist/lib/voice/livekit/tokens.js +51 -0
  34. package/dist/lib/voice/livekit/voiceAgent.d.ts +32 -0
  35. package/dist/lib/voice/livekit/voiceAgent.js +415 -0
  36. package/dist/lib/voice/livekit/voiceAgentWorker.d.ts +27 -0
  37. package/dist/lib/voice/livekit/voiceAgentWorker.js +58 -0
  38. package/dist/neurolink.js +58 -0
  39. package/dist/providers/anthropic.js +34 -7
  40. package/dist/providers/googleVertex.js +17 -2
  41. package/dist/types/generate.d.ts +47 -19
  42. package/dist/types/index.d.ts +1 -0
  43. package/dist/types/index.js +1 -0
  44. package/dist/types/livekit.d.ts +369 -0
  45. package/dist/types/livekit.js +12 -0
  46. package/dist/types/utilities.d.ts +16 -0
  47. package/dist/utils/json/coerce.d.ts +10 -0
  48. package/dist/utils/json/coerce.js +140 -0
  49. package/dist/utils/json/extract.d.ts +10 -0
  50. package/dist/utils/json/extract.js +61 -11
  51. package/dist/utils/tokenLimits.d.ts +20 -0
  52. package/dist/utils/tokenLimits.js +55 -0
  53. package/dist/voice/livekit/brain.d.ts +21 -0
  54. package/dist/voice/livekit/brain.js +74 -0
  55. package/dist/voice/livekit/config.d.ts +41 -0
  56. package/dist/voice/livekit/config.js +79 -0
  57. package/dist/voice/livekit/eventBridge.d.ts +27 -0
  58. package/dist/voice/livekit/eventBridge.js +359 -0
  59. package/dist/voice/livekit/index.d.ts +15 -0
  60. package/dist/voice/livekit/index.js +15 -0
  61. package/dist/voice/livekit/tokens.d.ts +19 -0
  62. package/dist/voice/livekit/tokens.js +50 -0
  63. package/dist/voice/livekit/voiceAgent.d.ts +32 -0
  64. package/dist/voice/livekit/voiceAgent.js +414 -0
  65. package/dist/voice/livekit/voiceAgentWorker.d.ts +27 -0
  66. package/dist/voice/livekit/voiceAgentWorker.js +57 -0
  67. package/package.json +23 -6
@@ -21,6 +21,8 @@ import { calculateCost } from "../../utils/pricing.js";
21
21
  import { withProviderRetry } from "../../utils/providerRetry.js";
22
22
  import { calculateCacheSavingsPercent, extractCacheCreationTokens, extractCacheReadTokens, extractTokenUsage, } from "../../utils/tokenUtils.js";
23
23
  import { DEFAULT_MAX_STEPS } from "../constants.js";
24
+ import { isToolsSchemaConflictError, isToolsSchemaExclusionInForce, } from "./structuredOutputPolicy.js";
25
+ import { coerceJsonToSchema } from "../../utils/json/coerce.js";
24
26
  import { NoObjectGeneratedError } from "../../utils/generationErrors.js";
25
27
  import { Output, stepCountIs } from "../../utils/tool.js";
26
28
  import { generateText } from "../../utils/generation.js";
@@ -76,8 +78,10 @@ export class GenerationHandler {
76
78
  (!!options.schema ||
77
79
  options.output?.format === "json" ||
78
80
  options.output?.format === "structured");
81
+ // The tools↔schema conflict is a Gemini-only API limitation. Vertex+Claude
82
+ // supports both simultaneously, so only exclude for actual Gemini models.
79
83
  const useStructuredOutput = wantsStructuredOutput &&
80
- !(isGoogleProvider && shouldUseTools && Object.keys(tools).length > 0);
84
+ !isToolsSchemaExclusionInForce(this.providerName, this.modelName, shouldUseTools, Object.keys(tools).length);
81
85
  // Annotate the last tool with cache_control so the full tool-definition
82
86
  // block becomes a cache breakpoint for Anthropic-family providers.
83
87
  // Non-Anthropic providers harmlessly ignore unknown providerOptions.
@@ -279,20 +283,30 @@ export class GenerationHandler {
279
283
  return result;
280
284
  }
281
285
  catch (error) {
282
- // If NoObjectGeneratedError is thrown when using schema + tools together,
283
- // fall back to generating without experimental_output and extract JSON manually
284
- if (error instanceof NoObjectGeneratedError && useStructuredOutput) {
286
+ // Fall back to text-mode (no experimental_output) when structured
287
+ // output + tools failed, in two cases:
288
+ // 1. NoObjectGeneratedError the SDK couldn't coerce the object.
289
+ // 2. The provider rejected json-mode-with-tools outright (e.g. Groq:
290
+ // "json mode cannot be combined with tool/function calling").
291
+ // In both cases we retry without structured output and let
292
+ // formatEnhancedResult coerce the text response into valid JSON.
293
+ const isStructuredOutputConflict = useStructuredOutput &&
294
+ (error instanceof NoObjectGeneratedError ||
295
+ isToolsSchemaConflictError(error));
296
+ if (isStructuredOutputConflict) {
285
297
  span.setAttribute("neurolink.has_fallback", true);
286
298
  // NLK-GAP-007: Record initial failure event before fallback retry
287
299
  span.addEvent("retry.initial_failure", {
288
- "error.message": error.message,
300
+ "error.message": error instanceof Error ? error.message : String(error),
289
301
  "retry.attempt": 1,
290
- "retry.reason": "NoObjectGeneratedError_structured_output_fallback",
302
+ "retry.reason": error instanceof NoObjectGeneratedError
303
+ ? "NoObjectGeneratedError_structured_output_fallback"
304
+ : "tools_schema_conflict_structured_output_fallback",
291
305
  });
292
- logger.debug("[GenerationHandler] NoObjectGeneratedError caught - falling back to manual JSON extraction", {
306
+ logger.debug("[GenerationHandler] structured-output conflict caught - falling back to manual JSON extraction", {
293
307
  provider: this.providerName,
294
308
  model: this.modelName,
295
- error: error.message,
309
+ error: error instanceof Error ? error.message : String(error),
296
310
  });
297
311
  // Retry without experimental_output - the formatEnhancedResult method
298
312
  // will extract JSON from the text response
@@ -460,41 +474,76 @@ export class GenerationHandler {
460
474
  options.output?.format === "json" ||
461
475
  options.output?.format === "structured";
462
476
  let content;
477
+ let structuredData;
478
+ let jsonRepaired = false;
479
+ let jsonTruncated = false;
480
+ // Strip an outer ```json fence and coerce raw model text into canonical
481
+ // JSON. Object/array roots are recovered via balanced-scan + jsonrepair;
482
+ // scalar JSON roots (string/number/bool) via plain JSON.parse. When
483
+ // nothing JSON-shaped is recoverable, the raw text is returned unchanged,
484
+ // structuredData stays unset, and a WARN makes the broken case observable.
485
+ const coerceTextMode = (rawText) => {
486
+ const strippedText = rawText
487
+ .replace(/^```(?:json)?\s*\n?/i, "")
488
+ .replace(/\n?```\s*$/i, "")
489
+ .trim();
490
+ const coerced = coerceJsonToSchema(strippedText, options.schema);
491
+ if (coerced) {
492
+ structuredData = coerced.structuredData;
493
+ if (coerced.repaired) {
494
+ jsonRepaired = true;
495
+ }
496
+ if (coerced.truncated) {
497
+ jsonTruncated = true;
498
+ }
499
+ return coerced.content;
500
+ }
501
+ try {
502
+ const scalar = JSON.parse(strippedText);
503
+ if (scalar !== null && scalar !== undefined) {
504
+ structuredData = scalar;
505
+ return strippedText;
506
+ }
507
+ }
508
+ catch {
509
+ // not JSON at all — fall through to raw text + WARN
510
+ }
511
+ logger.warn("[GenerationHandler] schema requested but no JSON could be recovered from model text; returning raw text", { provider: this.providerName, model: this.modelName });
512
+ return strippedText;
513
+ };
463
514
  if (useStructuredOutput) {
464
515
  try {
465
516
  const experimentalOutput = generateResult.experimental_output;
466
517
  if (experimentalOutput !== undefined) {
518
+ // AI-SDK already parsed + schema-validated the object. Expose it
519
+ // directly and serialise canonically — no hand-parsing needed.
520
+ structuredData = experimentalOutput;
467
521
  content = JSON.stringify(experimentalOutput);
468
522
  }
469
523
  else {
470
- // Fall back to text parsing
471
- const rawText = generateResult.text || "";
472
- const strippedText = rawText
473
- .replace(/^```(?:json)?\s*\n?/i, "")
474
- .replace(/\n?```\s*$/i, "")
475
- .trim();
476
- content = strippedText;
524
+ content = coerceTextMode(generateResult.text || "");
477
525
  }
478
526
  }
479
527
  catch (outputError) {
480
- // experimental_output is a getter that can throw NoObjectGeneratedError
481
- // Fall back to text parsing when structured output fails
528
+ // experimental_output is a getter that can throw NoObjectGeneratedError.
482
529
  logger.debug("[GenerationHandler] experimental_output threw, falling back to text parsing", {
483
530
  error: outputError instanceof Error
484
531
  ? outputError.message
485
532
  : String(outputError),
486
533
  });
487
- const rawText = generateResult.text || "";
488
- const strippedText = rawText
489
- .replace(/^```(?:json)?\s*\n?/i, "")
490
- .replace(/\n?```\s*$/i, "")
491
- .trim();
492
- content = strippedText;
534
+ content = coerceTextMode(generateResult.text || "");
493
535
  }
494
536
  }
495
537
  else {
496
538
  content = generateResult.text;
497
539
  }
540
+ // Tie the coercion repair to the provider's truncation signal: if the
541
+ // response stopped on the token cap, treat the structured output as
542
+ // truncated regardless of which coerce candidate won, and warn.
543
+ if (useStructuredOutput && generateResult.finishReason === "length") {
544
+ jsonTruncated = true;
545
+ logger.warn("[GenerationHandler] Structured output truncated by token cap (finishReason=length); increase maxTokens", { provider: this.providerName, model: this.modelName });
546
+ }
498
547
  // Extract usage with support for different formats and reasoning tokens
499
548
  // Note: The AI SDK bundles thinking tokens into promptTokens for Google models.
500
549
  // Separate reasoningTokens tracking will work when/if the AI SDK adds support.
@@ -537,8 +586,11 @@ export class GenerationHandler {
537
586
  const reasoningTokens = usage.reasoning ?? undefined;
538
587
  return {
539
588
  content,
589
+ structuredData,
540
590
  usage,
541
591
  finishReason: generateResult.finishReason,
592
+ jsonRepaired: jsonRepaired || undefined,
593
+ jsonTruncated: jsonTruncated || undefined,
542
594
  provider: this.providerName,
543
595
  model: this.modelName,
544
596
  reasoning,
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Policy for when AI-SDK structured output (experimental_output) must be
3
+ * disabled because the provider cannot combine tool calls with JSON-schema
4
+ * enforcement.
5
+ *
6
+ * This is a GEMINI-ONLY API limitation. Anthropic Claude — including when
7
+ * hosted on Vertex (modelName starts with "claude-") — supports tools and
8
+ * structured output simultaneously, so it must NOT be excluded. A gate keyed on
9
+ * "any Vertex model" wrongly disables structured output for Vertex+Claude (the
10
+ * primary production config) and forces fragile hand-parsed JSON.
11
+ */
12
+ /** True when the provider+model is a Gemini model (the only family with the tools↔schema conflict). */
13
+ export declare function isGeminiProvider(providerName: string, modelName: string | undefined): boolean;
14
+ /**
15
+ * True when structured output must be disabled for this call because tools are
16
+ * active on a Gemini provider. Mirrors the AI-SDK constraint exactly.
17
+ */
18
+ export declare function isToolsSchemaExclusionInForce(providerName: string, modelName: string | undefined, shouldUseTools: boolean, toolCount: number): boolean;
19
+ /**
20
+ * True when a provider error indicates the request was rejected because JSON /
21
+ * structured output and tool-calling cannot be combined for that provider
22
+ * (e.g. Groq: "json mode cannot be combined with tool/function calling").
23
+ *
24
+ * This is the runtime, provider-agnostic complement to the static Gemini gate:
25
+ * any provider with the same limitation can be detected from its error message
26
+ * and retried without structured output instead of failing the call.
27
+ */
28
+ export declare function isToolsSchemaConflictError(error: unknown): boolean;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Policy for when AI-SDK structured output (experimental_output) must be
3
+ * disabled because the provider cannot combine tool calls with JSON-schema
4
+ * enforcement.
5
+ *
6
+ * This is a GEMINI-ONLY API limitation. Anthropic Claude — including when
7
+ * hosted on Vertex (modelName starts with "claude-") — supports tools and
8
+ * structured output simultaneously, so it must NOT be excluded. A gate keyed on
9
+ * "any Vertex model" wrongly disables structured output for Vertex+Claude (the
10
+ * primary production config) and forces fragile hand-parsed JSON.
11
+ */
12
+ /** True when the provider+model is a Gemini model (the only family with the tools↔schema conflict). */
13
+ export function isGeminiProvider(providerName, modelName) {
14
+ if (providerName === "google-ai") {
15
+ return true;
16
+ }
17
+ if (providerName === "vertex") {
18
+ // Vertex hosts both Gemini and Claude. Only non-Claude (Gemini) models
19
+ // have the tools↔schema conflict.
20
+ return !(modelName?.startsWith("claude-") ?? false);
21
+ }
22
+ return false;
23
+ }
24
+ /**
25
+ * True when structured output must be disabled for this call because tools are
26
+ * active on a Gemini provider. Mirrors the AI-SDK constraint exactly.
27
+ */
28
+ export function isToolsSchemaExclusionInForce(providerName, modelName, shouldUseTools, toolCount) {
29
+ return (isGeminiProvider(providerName, modelName) && shouldUseTools && toolCount > 0);
30
+ }
31
+ /**
32
+ * True when a provider error indicates the request was rejected because JSON /
33
+ * structured output and tool-calling cannot be combined for that provider
34
+ * (e.g. Groq: "json mode cannot be combined with tool/function calling").
35
+ *
36
+ * This is the runtime, provider-agnostic complement to the static Gemini gate:
37
+ * any provider with the same limitation can be detected from its error message
38
+ * and retried without structured output instead of failing the call.
39
+ */
40
+ export function isToolsSchemaConflictError(error) {
41
+ const message = error instanceof Error
42
+ ? error.message
43
+ : typeof error === "string"
44
+ ? error
45
+ : "";
46
+ return (/cannot be combined with (a )?(tool|function)/i.test(message) ||
47
+ /json[\s_-]?(mode|schema|object|output)[^.]{0,60}(tool|function)/i.test(message) ||
48
+ /(tool|function)[\s-]?call[^.]{0,60}json[\s_-]?(mode|schema)/i.test(message) ||
49
+ /response_format[^.]{0,60}(tool|function)/i.test(message));
50
+ }
@@ -21,6 +21,8 @@ import { calculateCost } from "../../utils/pricing.js";
21
21
  import { withProviderRetry } from "../../utils/providerRetry.js";
22
22
  import { calculateCacheSavingsPercent, extractCacheCreationTokens, extractCacheReadTokens, extractTokenUsage, } from "../../utils/tokenUtils.js";
23
23
  import { DEFAULT_MAX_STEPS } from "../constants.js";
24
+ import { isToolsSchemaConflictError, isToolsSchemaExclusionInForce, } from "./structuredOutputPolicy.js";
25
+ import { coerceJsonToSchema } from "../../utils/json/coerce.js";
24
26
  import { NoObjectGeneratedError } from "../../utils/generationErrors.js";
25
27
  import { Output, stepCountIs } from "../../utils/tool.js";
26
28
  import { generateText } from "../../utils/generation.js";
@@ -76,8 +78,10 @@ export class GenerationHandler {
76
78
  (!!options.schema ||
77
79
  options.output?.format === "json" ||
78
80
  options.output?.format === "structured");
81
+ // The tools↔schema conflict is a Gemini-only API limitation. Vertex+Claude
82
+ // supports both simultaneously, so only exclude for actual Gemini models.
79
83
  const useStructuredOutput = wantsStructuredOutput &&
80
- !(isGoogleProvider && shouldUseTools && Object.keys(tools).length > 0);
84
+ !isToolsSchemaExclusionInForce(this.providerName, this.modelName, shouldUseTools, Object.keys(tools).length);
81
85
  // Annotate the last tool with cache_control so the full tool-definition
82
86
  // block becomes a cache breakpoint for Anthropic-family providers.
83
87
  // Non-Anthropic providers harmlessly ignore unknown providerOptions.
@@ -279,20 +283,30 @@ export class GenerationHandler {
279
283
  return result;
280
284
  }
281
285
  catch (error) {
282
- // If NoObjectGeneratedError is thrown when using schema + tools together,
283
- // fall back to generating without experimental_output and extract JSON manually
284
- if (error instanceof NoObjectGeneratedError && useStructuredOutput) {
286
+ // Fall back to text-mode (no experimental_output) when structured
287
+ // output + tools failed, in two cases:
288
+ // 1. NoObjectGeneratedError the SDK couldn't coerce the object.
289
+ // 2. The provider rejected json-mode-with-tools outright (e.g. Groq:
290
+ // "json mode cannot be combined with tool/function calling").
291
+ // In both cases we retry without structured output and let
292
+ // formatEnhancedResult coerce the text response into valid JSON.
293
+ const isStructuredOutputConflict = useStructuredOutput &&
294
+ (error instanceof NoObjectGeneratedError ||
295
+ isToolsSchemaConflictError(error));
296
+ if (isStructuredOutputConflict) {
285
297
  span.setAttribute("neurolink.has_fallback", true);
286
298
  // NLK-GAP-007: Record initial failure event before fallback retry
287
299
  span.addEvent("retry.initial_failure", {
288
- "error.message": error.message,
300
+ "error.message": error instanceof Error ? error.message : String(error),
289
301
  "retry.attempt": 1,
290
- "retry.reason": "NoObjectGeneratedError_structured_output_fallback",
302
+ "retry.reason": error instanceof NoObjectGeneratedError
303
+ ? "NoObjectGeneratedError_structured_output_fallback"
304
+ : "tools_schema_conflict_structured_output_fallback",
291
305
  });
292
- logger.debug("[GenerationHandler] NoObjectGeneratedError caught - falling back to manual JSON extraction", {
306
+ logger.debug("[GenerationHandler] structured-output conflict caught - falling back to manual JSON extraction", {
293
307
  provider: this.providerName,
294
308
  model: this.modelName,
295
- error: error.message,
309
+ error: error instanceof Error ? error.message : String(error),
296
310
  });
297
311
  // Retry without experimental_output - the formatEnhancedResult method
298
312
  // will extract JSON from the text response
@@ -460,41 +474,76 @@ export class GenerationHandler {
460
474
  options.output?.format === "json" ||
461
475
  options.output?.format === "structured";
462
476
  let content;
477
+ let structuredData;
478
+ let jsonRepaired = false;
479
+ let jsonTruncated = false;
480
+ // Strip an outer ```json fence and coerce raw model text into canonical
481
+ // JSON. Object/array roots are recovered via balanced-scan + jsonrepair;
482
+ // scalar JSON roots (string/number/bool) via plain JSON.parse. When
483
+ // nothing JSON-shaped is recoverable, the raw text is returned unchanged,
484
+ // structuredData stays unset, and a WARN makes the broken case observable.
485
+ const coerceTextMode = (rawText) => {
486
+ const strippedText = rawText
487
+ .replace(/^```(?:json)?\s*\n?/i, "")
488
+ .replace(/\n?```\s*$/i, "")
489
+ .trim();
490
+ const coerced = coerceJsonToSchema(strippedText, options.schema);
491
+ if (coerced) {
492
+ structuredData = coerced.structuredData;
493
+ if (coerced.repaired) {
494
+ jsonRepaired = true;
495
+ }
496
+ if (coerced.truncated) {
497
+ jsonTruncated = true;
498
+ }
499
+ return coerced.content;
500
+ }
501
+ try {
502
+ const scalar = JSON.parse(strippedText);
503
+ if (scalar !== null && scalar !== undefined) {
504
+ structuredData = scalar;
505
+ return strippedText;
506
+ }
507
+ }
508
+ catch {
509
+ // not JSON at all — fall through to raw text + WARN
510
+ }
511
+ logger.warn("[GenerationHandler] schema requested but no JSON could be recovered from model text; returning raw text", { provider: this.providerName, model: this.modelName });
512
+ return strippedText;
513
+ };
463
514
  if (useStructuredOutput) {
464
515
  try {
465
516
  const experimentalOutput = generateResult.experimental_output;
466
517
  if (experimentalOutput !== undefined) {
518
+ // AI-SDK already parsed + schema-validated the object. Expose it
519
+ // directly and serialise canonically — no hand-parsing needed.
520
+ structuredData = experimentalOutput;
467
521
  content = JSON.stringify(experimentalOutput);
468
522
  }
469
523
  else {
470
- // Fall back to text parsing
471
- const rawText = generateResult.text || "";
472
- const strippedText = rawText
473
- .replace(/^```(?:json)?\s*\n?/i, "")
474
- .replace(/\n?```\s*$/i, "")
475
- .trim();
476
- content = strippedText;
524
+ content = coerceTextMode(generateResult.text || "");
477
525
  }
478
526
  }
479
527
  catch (outputError) {
480
- // experimental_output is a getter that can throw NoObjectGeneratedError
481
- // Fall back to text parsing when structured output fails
528
+ // experimental_output is a getter that can throw NoObjectGeneratedError.
482
529
  logger.debug("[GenerationHandler] experimental_output threw, falling back to text parsing", {
483
530
  error: outputError instanceof Error
484
531
  ? outputError.message
485
532
  : String(outputError),
486
533
  });
487
- const rawText = generateResult.text || "";
488
- const strippedText = rawText
489
- .replace(/^```(?:json)?\s*\n?/i, "")
490
- .replace(/\n?```\s*$/i, "")
491
- .trim();
492
- content = strippedText;
534
+ content = coerceTextMode(generateResult.text || "");
493
535
  }
494
536
  }
495
537
  else {
496
538
  content = generateResult.text;
497
539
  }
540
+ // Tie the coercion repair to the provider's truncation signal: if the
541
+ // response stopped on the token cap, treat the structured output as
542
+ // truncated regardless of which coerce candidate won, and warn.
543
+ if (useStructuredOutput && generateResult.finishReason === "length") {
544
+ jsonTruncated = true;
545
+ logger.warn("[GenerationHandler] Structured output truncated by token cap (finishReason=length); increase maxTokens", { provider: this.providerName, model: this.modelName });
546
+ }
498
547
  // Extract usage with support for different formats and reasoning tokens
499
548
  // Note: The AI SDK bundles thinking tokens into promptTokens for Google models.
500
549
  // Separate reasoningTokens tracking will work when/if the AI SDK adds support.
@@ -537,8 +586,11 @@ export class GenerationHandler {
537
586
  const reasoningTokens = usage.reasoning ?? undefined;
538
587
  return {
539
588
  content,
589
+ structuredData,
540
590
  usage,
541
591
  finishReason: generateResult.finishReason,
592
+ jsonRepaired: jsonRepaired || undefined,
593
+ jsonTruncated: jsonTruncated || undefined,
542
594
  provider: this.providerName,
543
595
  model: this.modelName,
544
596
  reasoning,
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Policy for when AI-SDK structured output (experimental_output) must be
3
+ * disabled because the provider cannot combine tool calls with JSON-schema
4
+ * enforcement.
5
+ *
6
+ * This is a GEMINI-ONLY API limitation. Anthropic Claude — including when
7
+ * hosted on Vertex (modelName starts with "claude-") — supports tools and
8
+ * structured output simultaneously, so it must NOT be excluded. A gate keyed on
9
+ * "any Vertex model" wrongly disables structured output for Vertex+Claude (the
10
+ * primary production config) and forces fragile hand-parsed JSON.
11
+ */
12
+ /** True when the provider+model is a Gemini model (the only family with the tools↔schema conflict). */
13
+ export declare function isGeminiProvider(providerName: string, modelName: string | undefined): boolean;
14
+ /**
15
+ * True when structured output must be disabled for this call because tools are
16
+ * active on a Gemini provider. Mirrors the AI-SDK constraint exactly.
17
+ */
18
+ export declare function isToolsSchemaExclusionInForce(providerName: string, modelName: string | undefined, shouldUseTools: boolean, toolCount: number): boolean;
19
+ /**
20
+ * True when a provider error indicates the request was rejected because JSON /
21
+ * structured output and tool-calling cannot be combined for that provider
22
+ * (e.g. Groq: "json mode cannot be combined with tool/function calling").
23
+ *
24
+ * This is the runtime, provider-agnostic complement to the static Gemini gate:
25
+ * any provider with the same limitation can be detected from its error message
26
+ * and retried without structured output instead of failing the call.
27
+ */
28
+ export declare function isToolsSchemaConflictError(error: unknown): boolean;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Policy for when AI-SDK structured output (experimental_output) must be
3
+ * disabled because the provider cannot combine tool calls with JSON-schema
4
+ * enforcement.
5
+ *
6
+ * This is a GEMINI-ONLY API limitation. Anthropic Claude — including when
7
+ * hosted on Vertex (modelName starts with "claude-") — supports tools and
8
+ * structured output simultaneously, so it must NOT be excluded. A gate keyed on
9
+ * "any Vertex model" wrongly disables structured output for Vertex+Claude (the
10
+ * primary production config) and forces fragile hand-parsed JSON.
11
+ */
12
+ /** True when the provider+model is a Gemini model (the only family with the tools↔schema conflict). */
13
+ export function isGeminiProvider(providerName, modelName) {
14
+ if (providerName === "google-ai") {
15
+ return true;
16
+ }
17
+ if (providerName === "vertex") {
18
+ // Vertex hosts both Gemini and Claude. Only non-Claude (Gemini) models
19
+ // have the tools↔schema conflict.
20
+ return !(modelName?.startsWith("claude-") ?? false);
21
+ }
22
+ return false;
23
+ }
24
+ /**
25
+ * True when structured output must be disabled for this call because tools are
26
+ * active on a Gemini provider. Mirrors the AI-SDK constraint exactly.
27
+ */
28
+ export function isToolsSchemaExclusionInForce(providerName, modelName, shouldUseTools, toolCount) {
29
+ return (isGeminiProvider(providerName, modelName) && shouldUseTools && toolCount > 0);
30
+ }
31
+ /**
32
+ * True when a provider error indicates the request was rejected because JSON /
33
+ * structured output and tool-calling cannot be combined for that provider
34
+ * (e.g. Groq: "json mode cannot be combined with tool/function calling").
35
+ *
36
+ * This is the runtime, provider-agnostic complement to the static Gemini gate:
37
+ * any provider with the same limitation can be detected from its error message
38
+ * and retried without structured output instead of failing the call.
39
+ */
40
+ export function isToolsSchemaConflictError(error) {
41
+ const message = error instanceof Error
42
+ ? error.message
43
+ : typeof error === "string"
44
+ ? error
45
+ : "";
46
+ return (/cannot be combined with (a )?(tool|function)/i.test(message) ||
47
+ /json[\s_-]?(mode|schema|object|output)[^.]{0,60}(tool|function)/i.test(message) ||
48
+ /(tool|function)[\s-]?call[^.]{0,60}json[\s_-]?(mode|schema)/i.test(message) ||
49
+ /response_format[^.]{0,60}(tool|function)/i.test(message));
50
+ }
51
+ //# sourceMappingURL=structuredOutputPolicy.js.map
@@ -66,6 +66,7 @@ import { CircuitBreaker, ERROR_CODES, ErrorFactory, isAbortError, isRetriableErr
66
66
  import { hasLifecycleErrorFired, markLifecycleErrorFired, } from "./utils/lifecycleCallbacks.js";
67
67
  import { resolveLifecycleTimeoutMs } from "./utils/lifecycleTimeout.js";
68
68
  import { cloneOptionsForCallIsolation } from "./utils/cloneOptions.js";
69
+ import { coerceJsonToSchema } from "./utils/json/coerce.js";
69
70
  // Factory processing imports
70
71
  import { createCleanStreamOptions, enhanceTextGenerationOptions, processFactoryOptions, processStreamingFactoryOptions, validateFactoryConfig, } from "./utils/factoryProcessing.js";
71
72
  import { logger, mcpLogger } from "./utils/logger.js";
@@ -3345,6 +3346,60 @@ Current user's request: ${currentInput}`;
3345
3346
  }
3346
3347
  finalizeGenerateRequestResult(params) {
3347
3348
  const { generateSpan, options, textOptions, textResult, factoryResult, originalPrompt, startTime, } = params;
3349
+ // Provider-agnostic JSON coercion for schema requests. Structured-output
3350
+ // enforcement makes valid JSON the overwhelming case; for every other
3351
+ // provider path — including generate() overrides (Vertex, Anthropic,
3352
+ // Bedrock, Google AI Studio) — object/array roots are recovered here via
3353
+ // balanced-scan + jsonrepair and scalar JSON roots via plain JSON.parse,
3354
+ // with the parsed value exposed as `structuredData`. If nothing
3355
+ // JSON-shaped is recoverable (pure prose), the raw text is returned,
3356
+ // `structuredData` stays undefined, and a WARN makes the case observable.
3357
+ // Runs BEFORE the end-of-generation emits below so event consumers see
3358
+ // the same coerced content/structuredData the caller receives.
3359
+ if (textOptions.schema &&
3360
+ textResult.structuredData === undefined &&
3361
+ typeof textResult.content === "string") {
3362
+ const coerced = coerceJsonToSchema(textResult.content, textOptions.schema);
3363
+ if (coerced) {
3364
+ textResult.content = coerced.content;
3365
+ textResult.structuredData = coerced.structuredData;
3366
+ if (coerced.repaired) {
3367
+ textResult.jsonRepaired = true;
3368
+ }
3369
+ if (coerced.truncated) {
3370
+ textResult.jsonTruncated = true;
3371
+ }
3372
+ }
3373
+ else {
3374
+ try {
3375
+ const scalar = JSON.parse(textResult.content);
3376
+ if (scalar !== null && scalar !== undefined) {
3377
+ textResult.structuredData = scalar;
3378
+ }
3379
+ }
3380
+ catch {
3381
+ logger.warn("[NeuroLink] schema requested but no JSON could be recovered from model output; returning raw text", { provider: textResult.provider, model: textResult.model });
3382
+ }
3383
+ }
3384
+ }
3385
+ // Surface truncation when a schema was requested: either the provider
3386
+ // reported finishReason="length" or the recovered JSON came from an
3387
+ // unclosed span. Either way `structuredData` may be incomplete — warn at
3388
+ // info level so it is observable in production (not just debug logs).
3389
+ if (textOptions.schema) {
3390
+ if (textResult.finishReason === "length") {
3391
+ textResult.jsonTruncated = true;
3392
+ }
3393
+ if (textResult.jsonTruncated) {
3394
+ logger.warn("[NeuroLink] Structured output may be truncated (finishReason=length or unclosed JSON); " +
3395
+ "increase maxTokens to fit the full response.", {
3396
+ provider: textResult.provider,
3397
+ model: textResult.model,
3398
+ finishReason: textResult.finishReason,
3399
+ outputTokens: textResult.usage?.output,
3400
+ });
3401
+ }
3402
+ }
3348
3403
  // Skip the top-level `generation:end` emission when the provider already
3349
3404
  // emitted it from its native generate path (Vertex / Google AI Studio).
3350
3405
  // Without this guard, native-path providers would surface TWO events
@@ -3378,7 +3433,10 @@ Current user's request: ${currentInput}`;
3378
3433
  this.emitter.emit("message", `Generation completed in ${Date.now() - startTime}ms`);
3379
3434
  const generateResult = {
3380
3435
  content: textResult.content,
3436
+ structuredData: textResult.structuredData,
3381
3437
  finishReason: textResult.finishReason,
3438
+ jsonRepaired: textResult.jsonRepaired,
3439
+ jsonTruncated: textResult.jsonTruncated,
3382
3440
  provider: textResult.provider,
3383
3441
  model: textResult.model,
3384
3442
  usage: textResult.usage