@juspay/neurolink 9.15.0 → 9.16.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 (193) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/adapters/video/videoAnalyzer.d.ts +1 -1
  3. package/dist/adapters/video/videoAnalyzer.js +10 -8
  4. package/dist/cli/commands/setup-anthropic.js +1 -14
  5. package/dist/cli/commands/setup-azure.js +1 -12
  6. package/dist/cli/commands/setup-bedrock.js +1 -9
  7. package/dist/cli/commands/setup-google-ai.js +1 -12
  8. package/dist/cli/commands/setup-openai.js +1 -14
  9. package/dist/cli/commands/workflow.d.ts +27 -0
  10. package/dist/cli/commands/workflow.js +216 -0
  11. package/dist/cli/factories/commandFactory.js +79 -20
  12. package/dist/cli/index.js +0 -1
  13. package/dist/cli/parser.js +4 -1
  14. package/dist/cli/utils/maskCredential.d.ts +11 -0
  15. package/dist/cli/utils/maskCredential.js +23 -0
  16. package/dist/constants/contextWindows.js +107 -16
  17. package/dist/constants/enums.d.ts +99 -15
  18. package/dist/constants/enums.js +152 -22
  19. package/dist/context/budgetChecker.js +1 -1
  20. package/dist/context/contextCompactor.js +31 -4
  21. package/dist/context/emergencyTruncation.d.ts +21 -0
  22. package/dist/context/emergencyTruncation.js +88 -0
  23. package/dist/context/errorDetection.d.ts +16 -0
  24. package/dist/context/errorDetection.js +48 -1
  25. package/dist/context/errors.d.ts +19 -0
  26. package/dist/context/errors.js +21 -0
  27. package/dist/context/stages/slidingWindowTruncator.d.ts +6 -0
  28. package/dist/context/stages/slidingWindowTruncator.js +159 -24
  29. package/dist/core/baseProvider.js +306 -200
  30. package/dist/core/conversationMemoryManager.js +104 -61
  31. package/dist/core/evaluationProviders.js +16 -33
  32. package/dist/core/factory.js +237 -164
  33. package/dist/core/modules/GenerationHandler.js +175 -116
  34. package/dist/core/modules/MessageBuilder.js +222 -170
  35. package/dist/core/modules/StreamHandler.d.ts +1 -0
  36. package/dist/core/modules/StreamHandler.js +95 -27
  37. package/dist/core/modules/TelemetryHandler.d.ts +10 -1
  38. package/dist/core/modules/TelemetryHandler.js +25 -7
  39. package/dist/core/modules/ToolsManager.js +115 -191
  40. package/dist/core/redisConversationMemoryManager.js +418 -282
  41. package/dist/factories/providerRegistry.d.ts +5 -0
  42. package/dist/factories/providerRegistry.js +20 -2
  43. package/dist/index.d.ts +2 -2
  44. package/dist/index.js +4 -2
  45. package/dist/lib/adapters/video/videoAnalyzer.d.ts +1 -1
  46. package/dist/lib/adapters/video/videoAnalyzer.js +10 -8
  47. package/dist/lib/constants/contextWindows.js +107 -16
  48. package/dist/lib/constants/enums.d.ts +99 -15
  49. package/dist/lib/constants/enums.js +152 -22
  50. package/dist/lib/context/budgetChecker.js +1 -1
  51. package/dist/lib/context/contextCompactor.js +31 -4
  52. package/dist/lib/context/emergencyTruncation.d.ts +21 -0
  53. package/dist/lib/context/emergencyTruncation.js +89 -0
  54. package/dist/lib/context/errorDetection.d.ts +16 -0
  55. package/dist/lib/context/errorDetection.js +48 -1
  56. package/dist/lib/context/errors.d.ts +19 -0
  57. package/dist/lib/context/errors.js +22 -0
  58. package/dist/lib/context/stages/slidingWindowTruncator.d.ts +6 -0
  59. package/dist/lib/context/stages/slidingWindowTruncator.js +159 -24
  60. package/dist/lib/core/baseProvider.js +306 -200
  61. package/dist/lib/core/conversationMemoryManager.js +104 -61
  62. package/dist/lib/core/evaluationProviders.js +16 -33
  63. package/dist/lib/core/factory.js +237 -164
  64. package/dist/lib/core/modules/GenerationHandler.js +175 -116
  65. package/dist/lib/core/modules/MessageBuilder.js +222 -170
  66. package/dist/lib/core/modules/StreamHandler.d.ts +1 -0
  67. package/dist/lib/core/modules/StreamHandler.js +95 -27
  68. package/dist/lib/core/modules/TelemetryHandler.d.ts +10 -1
  69. package/dist/lib/core/modules/TelemetryHandler.js +25 -7
  70. package/dist/lib/core/modules/ToolsManager.js +115 -191
  71. package/dist/lib/core/redisConversationMemoryManager.js +418 -282
  72. package/dist/lib/factories/providerRegistry.d.ts +5 -0
  73. package/dist/lib/factories/providerRegistry.js +20 -2
  74. package/dist/lib/index.d.ts +2 -2
  75. package/dist/lib/index.js +4 -2
  76. package/dist/lib/mcp/externalServerManager.js +66 -0
  77. package/dist/lib/mcp/mcpCircuitBreaker.js +24 -0
  78. package/dist/lib/mcp/mcpClientFactory.js +16 -0
  79. package/dist/lib/mcp/toolDiscoveryService.js +32 -6
  80. package/dist/lib/mcp/toolRegistry.js +193 -123
  81. package/dist/lib/neurolink.d.ts +6 -0
  82. package/dist/lib/neurolink.js +1162 -646
  83. package/dist/lib/providers/amazonBedrock.d.ts +1 -1
  84. package/dist/lib/providers/amazonBedrock.js +521 -319
  85. package/dist/lib/providers/anthropic.js +73 -17
  86. package/dist/lib/providers/anthropicBaseProvider.js +77 -17
  87. package/dist/lib/providers/googleAiStudio.d.ts +1 -1
  88. package/dist/lib/providers/googleAiStudio.js +292 -227
  89. package/dist/lib/providers/googleVertex.d.ts +36 -1
  90. package/dist/lib/providers/googleVertex.js +553 -260
  91. package/dist/lib/providers/ollama.js +329 -278
  92. package/dist/lib/providers/openAI.js +77 -19
  93. package/dist/lib/providers/sagemaker/parsers.js +3 -3
  94. package/dist/lib/providers/sagemaker/streaming.js +3 -3
  95. package/dist/lib/proxy/proxyFetch.js +81 -48
  96. package/dist/lib/rag/ChunkerFactory.js +1 -1
  97. package/dist/lib/rag/chunkers/MarkdownChunker.d.ts +22 -0
  98. package/dist/lib/rag/chunkers/MarkdownChunker.js +213 -9
  99. package/dist/lib/rag/chunking/markdownChunker.d.ts +16 -0
  100. package/dist/lib/rag/chunking/markdownChunker.js +174 -2
  101. package/dist/lib/rag/pipeline/contextAssembly.js +2 -1
  102. package/dist/lib/rag/ragIntegration.d.ts +18 -1
  103. package/dist/lib/rag/ragIntegration.js +94 -14
  104. package/dist/lib/rag/retrieval/vectorQueryTool.js +21 -4
  105. package/dist/lib/server/abstract/baseServerAdapter.js +4 -1
  106. package/dist/lib/server/adapters/fastifyAdapter.js +35 -30
  107. package/dist/lib/services/server/ai/observability/instrumentation.d.ts +32 -0
  108. package/dist/lib/services/server/ai/observability/instrumentation.js +39 -0
  109. package/dist/lib/telemetry/attributes.d.ts +52 -0
  110. package/dist/lib/telemetry/attributes.js +61 -0
  111. package/dist/lib/telemetry/index.d.ts +3 -0
  112. package/dist/lib/telemetry/index.js +3 -0
  113. package/dist/lib/telemetry/telemetryService.d.ts +6 -0
  114. package/dist/lib/telemetry/telemetryService.js +6 -0
  115. package/dist/lib/telemetry/tracers.d.ts +15 -0
  116. package/dist/lib/telemetry/tracers.js +17 -0
  117. package/dist/lib/telemetry/withSpan.d.ts +9 -0
  118. package/dist/lib/telemetry/withSpan.js +35 -0
  119. package/dist/lib/types/contextTypes.d.ts +10 -0
  120. package/dist/lib/types/streamTypes.d.ts +14 -0
  121. package/dist/lib/utils/conversationMemory.js +121 -82
  122. package/dist/lib/utils/logger.d.ts +5 -0
  123. package/dist/lib/utils/logger.js +50 -2
  124. package/dist/lib/utils/messageBuilder.js +22 -42
  125. package/dist/lib/utils/modelDetection.js +3 -3
  126. package/dist/lib/utils/providerRetry.d.ts +41 -0
  127. package/dist/lib/utils/providerRetry.js +114 -0
  128. package/dist/lib/utils/retryability.d.ts +14 -0
  129. package/dist/lib/utils/retryability.js +23 -0
  130. package/dist/lib/utils/sanitizers/svg.js +4 -5
  131. package/dist/lib/utils/tokenEstimation.d.ts +11 -1
  132. package/dist/lib/utils/tokenEstimation.js +19 -4
  133. package/dist/lib/utils/videoAnalysisProcessor.js +7 -3
  134. package/dist/mcp/externalServerManager.js +66 -0
  135. package/dist/mcp/mcpCircuitBreaker.js +24 -0
  136. package/dist/mcp/mcpClientFactory.js +16 -0
  137. package/dist/mcp/toolDiscoveryService.js +32 -6
  138. package/dist/mcp/toolRegistry.js +193 -123
  139. package/dist/neurolink.d.ts +6 -0
  140. package/dist/neurolink.js +1162 -646
  141. package/dist/providers/amazonBedrock.d.ts +1 -1
  142. package/dist/providers/amazonBedrock.js +521 -319
  143. package/dist/providers/anthropic.js +73 -17
  144. package/dist/providers/anthropicBaseProvider.js +77 -17
  145. package/dist/providers/googleAiStudio.d.ts +1 -1
  146. package/dist/providers/googleAiStudio.js +292 -227
  147. package/dist/providers/googleVertex.d.ts +36 -1
  148. package/dist/providers/googleVertex.js +553 -260
  149. package/dist/providers/ollama.js +329 -278
  150. package/dist/providers/openAI.js +77 -19
  151. package/dist/providers/sagemaker/parsers.js +3 -3
  152. package/dist/providers/sagemaker/streaming.js +3 -3
  153. package/dist/proxy/proxyFetch.js +81 -48
  154. package/dist/rag/ChunkerFactory.js +1 -1
  155. package/dist/rag/chunkers/MarkdownChunker.d.ts +22 -0
  156. package/dist/rag/chunkers/MarkdownChunker.js +213 -9
  157. package/dist/rag/chunking/markdownChunker.d.ts +16 -0
  158. package/dist/rag/chunking/markdownChunker.js +174 -2
  159. package/dist/rag/pipeline/contextAssembly.js +2 -1
  160. package/dist/rag/ragIntegration.d.ts +18 -1
  161. package/dist/rag/ragIntegration.js +94 -14
  162. package/dist/rag/retrieval/vectorQueryTool.js +21 -4
  163. package/dist/server/abstract/baseServerAdapter.js +4 -1
  164. package/dist/server/adapters/fastifyAdapter.js +35 -30
  165. package/dist/services/server/ai/observability/instrumentation.d.ts +32 -0
  166. package/dist/services/server/ai/observability/instrumentation.js +39 -0
  167. package/dist/telemetry/attributes.d.ts +52 -0
  168. package/dist/telemetry/attributes.js +60 -0
  169. package/dist/telemetry/index.d.ts +3 -0
  170. package/dist/telemetry/index.js +3 -0
  171. package/dist/telemetry/telemetryService.d.ts +6 -0
  172. package/dist/telemetry/telemetryService.js +6 -0
  173. package/dist/telemetry/tracers.d.ts +15 -0
  174. package/dist/telemetry/tracers.js +16 -0
  175. package/dist/telemetry/withSpan.d.ts +9 -0
  176. package/dist/telemetry/withSpan.js +34 -0
  177. package/dist/types/contextTypes.d.ts +10 -0
  178. package/dist/types/streamTypes.d.ts +14 -0
  179. package/dist/utils/conversationMemory.js +121 -82
  180. package/dist/utils/logger.d.ts +5 -0
  181. package/dist/utils/logger.js +50 -2
  182. package/dist/utils/messageBuilder.js +22 -42
  183. package/dist/utils/modelDetection.js +3 -3
  184. package/dist/utils/providerRetry.d.ts +41 -0
  185. package/dist/utils/providerRetry.js +113 -0
  186. package/dist/utils/retryability.d.ts +14 -0
  187. package/dist/utils/retryability.js +22 -0
  188. package/dist/utils/sanitizers/svg.js +4 -5
  189. package/dist/utils/tokenEstimation.d.ts +11 -1
  190. package/dist/utils/tokenEstimation.js +19 -4
  191. package/dist/utils/videoAnalysisProcessor.js +7 -3
  192. package/dist/workflow/config.d.ts +26 -26
  193. package/package.json +1 -1
@@ -13,9 +13,14 @@
13
13
  * @module core/modules/GenerationHandler
14
14
  */
15
15
  import { generateText, Output, NoObjectGeneratedError } from "ai";
16
+ import { SpanKind, SpanStatusCode } from "@opentelemetry/api";
17
+ import { tracers } from "../../telemetry/tracers.js";
16
18
  import { logger } from "../../utils/logger.js";
17
19
  import { extractTokenUsage, extractCacheCreationTokens, extractCacheReadTokens, calculateCacheSavingsPercent, } from "../../utils/tokenUtils.js";
20
+ import { withProviderRetry } from "../../utils/providerRetry.js";
21
+ import { calculateCost } from "../../utils/pricing.js";
18
22
  import { DEFAULT_MAX_STEPS } from "../constants.js";
23
+ const genTracer = tracers.generation;
19
24
  /**
20
25
  * Safely preview-serialize a value for debug logging.
21
26
  * Handles undefined, circular references, and non-serializable values.
@@ -59,10 +64,14 @@ export class GenerationHandler {
59
64
  const isAnthropicProvider = this.providerName === "anthropic" ||
60
65
  this.providerName === "bedrock" ||
61
66
  (this.providerName === "vertex" && this.modelName?.startsWith("claude-"));
62
- const useStructuredOutput = includeStructuredOutput &&
67
+ // Gemini 2.5 and earlier cannot use tools + structured JSON output simultaneously.
68
+ // When both are requested on a Google provider, disable structured output (tools take priority).
69
+ const wantsStructuredOutput = includeStructuredOutput &&
63
70
  !!options.schema &&
64
71
  (options.output?.format === "json" ||
65
72
  options.output?.format === "structured");
73
+ const useStructuredOutput = wantsStructuredOutput &&
74
+ !(isGoogleProvider && shouldUseTools && Object.keys(tools).length > 0);
66
75
  // Annotate the last tool with cache_control so the full tool-definition
67
76
  // block becomes a cache breakpoint for Anthropic-family providers.
68
77
  // Non-Anthropic providers harmlessly ignore unknown providerOptions.
@@ -98,6 +107,7 @@ export class GenerationHandler {
98
107
  }),
99
108
  temperature: options.temperature,
100
109
  maxTokens: options.maxTokens,
110
+ maxRetries: 0, // NL11: Disable AI SDK's invisible internal retries; we handle retries with OTel instrumentation
101
111
  abortSignal: options.abortSignal,
102
112
  ...(useStructuredOutput &&
103
113
  options.schema && {
@@ -153,108 +163,59 @@ export class GenerationHandler {
153
163
  * Execute the generation with AI SDK
154
164
  */
155
165
  async executeGeneration(model, messages, tools, options) {
156
- const shouldUseTools = !options.disableTools && this.supportsToolsFn();
157
- const useStructuredOutput = !!options.schema &&
158
- (options.output?.format === "json" ||
159
- options.output?.format === "structured");
160
- const requestId = options.requestId ||
161
- options.context?.requestId ||
162
- "unknown";
163
- logger.info("[GenerationHandler] Calling generateText", {
164
- requestId,
165
- model: model.modelId || "unknown",
166
- messageCount: messages.length,
167
- toolCount: Object.keys(tools || {}).length,
168
- maxSteps: options.maxSteps,
169
- temperature: options.temperature,
170
- });
171
- if (logger.shouldLog("debug")) {
172
- try {
173
- logger.debug("[Observability] Full generateText parameters", {
174
- requestId,
175
- model: model.modelId || "unknown",
176
- messageCount: messages.length,
177
- messages: messages.map((msg, i) => ({
178
- index: i,
179
- role: msg.role,
180
- contentLength: typeof msg.content === "string"
181
- ? msg.content.length
182
- : safePreview(msg.content).length,
183
- contentPreview: typeof msg.content === "string"
184
- ? msg.content.substring(0, 200)
185
- : "[multimodal]",
186
- })),
187
- toolNames: Object.keys(tools || {}),
188
- toolCount: Object.keys(tools || {}).length,
189
- maxSteps: options.maxSteps,
190
- temperature: options.temperature,
191
- maxTokens: options.maxTokens,
192
- });
193
- }
194
- catch {
195
- // Ignore serialization errors in debug logging
196
- }
197
- }
198
- const genStartTime = Date.now();
199
- try {
200
- const result = await this.callGenerateText(model, messages, tools, options, shouldUseTools, true);
201
- logger.info("[GenerationHandler] generateText returned", {
166
+ return genTracer.startActiveSpan("neurolink.executeGeneration", { kind: SpanKind.INTERNAL }, async (span) => {
167
+ const shouldUseTools = !options.disableTools && this.supportsToolsFn();
168
+ const toolCount = Object.keys(tools || {}).length;
169
+ const useStructuredOutput = !!options.schema &&
170
+ (options.output?.format === "json" ||
171
+ options.output?.format === "structured");
172
+ span.setAttribute("gen_ai.system", this.providerName || "unknown");
173
+ span.setAttribute("neurolink.structured_output", useStructuredOutput);
174
+ span.setAttribute("neurolink.tool_count", toolCount);
175
+ span.setAttribute("neurolink.message_count", messages.length);
176
+ span.setAttribute("gen_ai.request.model", model.modelId || this.modelName || "unknown");
177
+ const requestId = options.requestId ||
178
+ options.context?.requestId ||
179
+ "unknown";
180
+ logger.info("[GenerationHandler] Calling generateText", {
202
181
  requestId,
203
- durationMs: Date.now() - genStartTime,
204
- finishReason: result.finishReason,
205
- steps: result.steps?.length || 1,
206
- toolCallsTotal: result.toolCalls?.length || 0,
207
- responseChars: result.text?.length || 0,
182
+ model: model.modelId || "unknown",
183
+ messageCount: messages.length,
184
+ toolCount,
185
+ maxSteps: options.maxSteps,
186
+ temperature: options.temperature,
208
187
  });
209
188
  if (logger.shouldLog("debug")) {
210
- logger.debug("[Observability] Full LLM response", {
211
- requestId,
212
- finishReason: result.finishReason,
213
- responseTextPreview: result.text?.substring(0, 200) || "",
214
- responseTextLength: result.text?.length || 0,
215
- toolCalls: result.toolCalls?.map((tc) => ({
216
- toolName: tc.toolName,
217
- argsPreview: safePreview(tc.args),
218
- })),
219
- toolResults: result.toolResults?.map((tr) => ({
220
- toolName: tr.toolName,
221
- resultPreview: safePreview(tr.result),
222
- })),
223
- steps: result.steps?.map((step, i) => ({
224
- stepIndex: i,
225
- stepType: step.stepType,
226
- textPreview: step.text?.substring(0, 200),
227
- textLength: step.text?.length || 0,
228
- toolCalls: step.toolCalls?.map((tc) => ({
229
- toolName: tc.toolName,
230
- argsPreview: safePreview(tc.args),
189
+ try {
190
+ logger.debug("[Observability] Full generateText parameters", {
191
+ requestId,
192
+ model: model.modelId || "unknown",
193
+ messageCount: messages.length,
194
+ messages: messages.map((msg, i) => ({
195
+ index: i,
196
+ role: msg.role,
197
+ contentLength: typeof msg.content === "string"
198
+ ? msg.content.length
199
+ : safePreview(msg.content).length,
200
+ contentPreview: typeof msg.content === "string"
201
+ ? msg.content.substring(0, 200)
202
+ : "[multimodal]",
231
203
  })),
232
- toolResults: step.toolResults?.map((tr) => ({
233
- toolName: tr.toolName,
234
- resultPreview: safePreview(tr.result),
235
- })),
236
- finishReason: step.finishReason,
237
- })),
238
- usage: result.usage,
239
- providerMetadata: result.experimental_providerMetadata ||
240
- result.providerMetadata,
241
- });
204
+ toolNames: Object.keys(tools || {}),
205
+ toolCount,
206
+ maxSteps: options.maxSteps,
207
+ temperature: options.temperature,
208
+ maxTokens: options.maxTokens,
209
+ });
210
+ }
211
+ catch {
212
+ // Ignore serialization errors in debug logging
213
+ }
242
214
  }
243
- return result;
244
- }
245
- catch (error) {
246
- // If NoObjectGeneratedError is thrown when using schema + tools together,
247
- // fall back to generating without experimental_output and extract JSON manually
248
- if (error instanceof NoObjectGeneratedError && useStructuredOutput) {
249
- logger.debug("[GenerationHandler] NoObjectGeneratedError caught - falling back to manual JSON extraction", {
250
- provider: this.providerName,
251
- model: this.modelName,
252
- error: error.message,
253
- });
254
- // Retry without experimental_output - the formatEnhancedResult method
255
- // will extract JSON from the text response
256
- const result = await this.callGenerateText(model, messages, tools, options, shouldUseTools, false);
257
- logger.info("[GenerationHandler] generateText returned (fallback)", {
215
+ const genStartTime = Date.now();
216
+ try {
217
+ const result = await withProviderRetry(() => this.callGenerateText(model, messages, tools, options, shouldUseTools, true), span, "generateText");
218
+ logger.info("[GenerationHandler] generateText returned", {
258
219
  requestId,
259
220
  durationMs: Date.now() - genStartTime,
260
221
  finishReason: result.finishReason,
@@ -262,11 +223,107 @@ export class GenerationHandler {
262
223
  toolCallsTotal: result.toolCalls?.length || 0,
263
224
  responseChars: result.text?.length || 0,
264
225
  });
226
+ if (logger.shouldLog("debug")) {
227
+ logger.debug("[Observability] LLM response metadata", {
228
+ requestId,
229
+ responseLength: result.text?.length || 0,
230
+ hasToolCalls: !!(result.toolCalls && result.toolCalls.length > 0),
231
+ toolCallCount: result.toolCalls?.length || 0,
232
+ toolNames: result.toolCalls?.map((tc) => tc.toolName),
233
+ finishReason: result.finishReason,
234
+ stepCount: result.steps?.length || 0,
235
+ steps: result.steps?.map((step, i) => ({
236
+ stepIndex: i,
237
+ stepType: step.stepType,
238
+ textLength: step.text?.length || 0,
239
+ toolCallCount: step.toolCalls?.length || 0,
240
+ toolNames: step.toolCalls?.map((tc) => tc.toolName),
241
+ toolResultCount: step.toolResults?.length || 0,
242
+ finishReason: step.finishReason,
243
+ })),
244
+ usage: result.usage,
245
+ });
246
+ }
247
+ // Set token usage and completion attributes on span
248
+ if (result.usage) {
249
+ span.setAttribute("gen_ai.usage.input_tokens", result.usage.promptTokens || 0);
250
+ span.setAttribute("gen_ai.usage.output_tokens", result.usage.completionTokens || 0);
251
+ // Cost on span so users can query "what did this trace cost?"
252
+ const cost = calculateCost(this.providerName, this.modelName, {
253
+ input: result.usage.promptTokens || 0,
254
+ output: result.usage.completionTokens || 0,
255
+ total: (result.usage.promptTokens || 0) +
256
+ (result.usage.completionTokens || 0),
257
+ });
258
+ span.setAttribute("neurolink.cost", cost ?? 0);
259
+ }
260
+ if (result.finishReason) {
261
+ span.setAttribute("gen_ai.response.finish_reason", result.finishReason);
262
+ }
263
+ span.setStatus({ code: SpanStatusCode.OK });
265
264
  return result;
266
265
  }
267
- // Re-throw other errors
268
- throw error;
269
- }
266
+ catch (error) {
267
+ // If NoObjectGeneratedError is thrown when using schema + tools together,
268
+ // fall back to generating without experimental_output and extract JSON manually
269
+ if (error instanceof NoObjectGeneratedError && useStructuredOutput) {
270
+ span.setAttribute("neurolink.has_fallback", true);
271
+ // NLK-GAP-007: Record initial failure event before fallback retry
272
+ span.addEvent("retry.initial_failure", {
273
+ "error.message": error.message,
274
+ "retry.attempt": 1,
275
+ "retry.reason": "NoObjectGeneratedError_structured_output_fallback",
276
+ });
277
+ logger.debug("[GenerationHandler] NoObjectGeneratedError caught - falling back to manual JSON extraction", {
278
+ provider: this.providerName,
279
+ model: this.modelName,
280
+ error: error.message,
281
+ });
282
+ // Retry without experimental_output - the formatEnhancedResult method
283
+ // will extract JSON from the text response
284
+ const result = await withProviderRetry(() => this.callGenerateText(model, messages, tools, options, shouldUseTools, false), span, "generateText(fallback)");
285
+ // NLK-GAP-007: Record recovery event after successful fallback
286
+ span.addEvent("retry.recovered", {
287
+ "retry.attempts": 2,
288
+ "retry.strategy": "structured_output_disabled",
289
+ });
290
+ span.setAttribute("retry.count", 1);
291
+ logger.info("[GenerationHandler] generateText returned (fallback)", {
292
+ requestId,
293
+ durationMs: Date.now() - genStartTime,
294
+ finishReason: result.finishReason,
295
+ steps: result.steps?.length || 1,
296
+ toolCallsTotal: result.toolCalls?.length || 0,
297
+ responseChars: result.text?.length || 0,
298
+ });
299
+ if (result.usage) {
300
+ span.setAttribute("gen_ai.usage.input_tokens", result.usage.promptTokens || 0);
301
+ span.setAttribute("gen_ai.usage.output_tokens", result.usage.completionTokens || 0);
302
+ const fallbackCost = calculateCost(this.providerName, this.modelName, {
303
+ input: result.usage.promptTokens || 0,
304
+ output: result.usage.completionTokens || 0,
305
+ total: (result.usage.promptTokens || 0) +
306
+ (result.usage.completionTokens || 0),
307
+ });
308
+ span.setAttribute("neurolink.cost", fallbackCost ?? 0);
309
+ }
310
+ if (result.finishReason) {
311
+ span.setAttribute("gen_ai.response.finish_reason", result.finishReason);
312
+ }
313
+ span.setStatus({ code: SpanStatusCode.OK });
314
+ return result;
315
+ }
316
+ span.setStatus({
317
+ code: SpanStatusCode.ERROR,
318
+ message: error instanceof Error ? error.message : String(error),
319
+ });
320
+ // Re-throw other errors
321
+ throw error;
322
+ }
323
+ finally {
324
+ span.end();
325
+ }
326
+ });
270
327
  }
271
328
  /**
272
329
  * Extract cache metrics from provider metadata (e.g. Anthropic's providerMetadata.anthropic)
@@ -297,21 +354,23 @@ export class GenerationHandler {
297
354
  */
298
355
  logGenerationComplete(generateResult) {
299
356
  const cacheMetrics = this.extractCacheMetricsFromProviderMetadata(generateResult);
300
- logger.debug(`generateText completed`, {
301
- provider: this.providerName,
302
- model: this.modelName,
303
- responseLength: generateResult.text?.length || 0,
304
- toolResultsCount: generateResult.toolResults?.length || 0,
305
- finishReason: generateResult.finishReason,
306
- usage: generateResult.usage,
307
- ...(cacheMetrics.cacheCreationTokens !== undefined && {
308
- cacheCreationTokens: cacheMetrics.cacheCreationTokens,
309
- }),
310
- ...(cacheMetrics.cacheReadTokens !== undefined && {
311
- cacheReadTokens: cacheMetrics.cacheReadTokens,
312
- }),
313
- timestamp: Date.now(),
314
- });
357
+ if (logger.shouldLog("debug")) {
358
+ logger.debug(`generateText completed`, {
359
+ provider: this.providerName,
360
+ model: this.modelName,
361
+ responseLength: generateResult.text?.length || 0,
362
+ toolResultsCount: generateResult.toolResults?.length || 0,
363
+ finishReason: generateResult.finishReason,
364
+ usage: generateResult.usage,
365
+ ...(cacheMetrics.cacheCreationTokens !== undefined && {
366
+ cacheCreationTokens: cacheMetrics.cacheCreationTokens,
367
+ }),
368
+ ...(cacheMetrics.cacheReadTokens !== undefined && {
369
+ cacheReadTokens: cacheMetrics.cacheReadTokens,
370
+ }),
371
+ timestamp: Date.now(),
372
+ });
373
+ }
315
374
  }
316
375
  /**
317
376
  * Extract tool information from generation result