@juspay/neurolink 12.59.0 → 13.0.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 (146) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/dist/auth/anthropicOAuth.js +3 -3
  3. package/dist/auth/middleware/AuthMiddleware.d.ts +1 -1
  4. package/dist/auth/sessionManager.js +4 -4
  5. package/dist/auth/tokenStore.js +4 -4
  6. package/dist/browser/neurolink.min.js +474 -473
  7. package/dist/cli/commands/observability.d.ts +1 -6
  8. package/dist/cli/commands/observability.js +0 -106
  9. package/dist/cli/commands/proxy.js +9 -18
  10. package/dist/cli/commands/telemetry.d.ts +3 -13
  11. package/dist/cli/commands/telemetry.js +7 -354
  12. package/dist/cli/loop/session.js +1 -1
  13. package/dist/client/httpClient.js +1 -1
  14. package/dist/client/sseClient.js +1 -1
  15. package/dist/client/streamingClient.js +2 -2
  16. package/dist/client/wsClient.js +1 -1
  17. package/dist/constants/langfuse.d.ts +25 -0
  18. package/dist/constants/langfuse.js +38 -0
  19. package/dist/constants/telemetry.d.ts +20 -0
  20. package/dist/constants/telemetry.js +34 -0
  21. package/dist/core/baseProvider.d.ts +2 -1
  22. package/dist/core/baseProvider.js +4 -11
  23. package/dist/core/conversationMemoryManager.js +5 -8
  24. package/dist/core/factory.js +1 -1
  25. package/dist/core/geminiLoopAdapter.d.ts +8 -1
  26. package/dist/core/geminiLoopAdapter.js +25 -1
  27. package/dist/core/loopEngine.js +69 -40
  28. package/dist/core/modules/MessageBuilder.js +2 -2
  29. package/dist/core/modules/StreamHandler.js +2 -2
  30. package/dist/core/modules/TelemetryHandler.d.ts +2 -18
  31. package/dist/core/modules/TelemetryHandler.js +1 -66
  32. package/dist/core/modules/ToolsManager.js +40 -31
  33. package/dist/core/nativeGenerateLoop.js +78 -8
  34. package/dist/core/redisConversationMemoryManager.js +8 -15
  35. package/dist/core/toolExecutionGuards.js +1 -13
  36. package/dist/index.d.ts +5 -12
  37. package/dist/index.js +5 -37
  38. package/dist/mcp/batching/requestBatcher.js +1 -1
  39. package/dist/mcp/externalServerManager.js +3 -21
  40. package/dist/mcp/toolDiscoveryService.js +4 -16
  41. package/dist/mcp/toolRegistry.js +2 -5
  42. package/dist/memory/memoryRetrievalTools.js +1 -1
  43. package/dist/neurolink.d.ts +16 -27
  44. package/dist/neurolink.js +788 -792
  45. package/dist/observability/exporterSpanProcessor.d.ts +50 -0
  46. package/dist/observability/exporterSpanProcessor.js +181 -0
  47. package/dist/observability/exporters/langfuseExporter.d.ts +13 -34
  48. package/dist/observability/exporters/langfuseExporter.js +101 -154
  49. package/dist/observability/index.d.ts +1 -0
  50. package/dist/observability/index.js +1 -0
  51. package/dist/observability/langfuseMasking.d.ts +8 -0
  52. package/dist/observability/langfuseMasking.js +47 -0
  53. package/dist/observability/otlpEncoding.d.ts +20 -0
  54. package/dist/observability/otlpEncoding.js +85 -0
  55. package/dist/providers/amazonBedrock/client.js +142 -176
  56. package/dist/providers/amazonBedrock/loopAdapter.js +14 -1
  57. package/dist/providers/amazonSagemaker.js +118 -80
  58. package/dist/providers/anthropic/client.js +3 -52
  59. package/dist/providers/anthropic/loopAdapter.js +24 -1
  60. package/dist/providers/anthropic/rateLimitCapture.js +9 -12
  61. package/dist/providers/googleAiStudio/client.d.ts +4 -5
  62. package/dist/providers/googleAiStudio/client.js +38 -40
  63. package/dist/providers/googleVertex/client.d.ts +2 -7
  64. package/dist/providers/googleVertex/client.js +282 -378
  65. package/dist/providers/googleVertex/constants.js +2 -2
  66. package/dist/providers/litellm/client.d.ts +2 -9
  67. package/dist/providers/litellm/client.js +1 -63
  68. package/dist/providers/openAI/client.d.ts +1 -8
  69. package/dist/providers/openAI/client.js +0 -65
  70. package/dist/providers/openaiChatCompletionsBase.d.ts +1 -0
  71. package/dist/providers/openaiChatCompletionsBase.js +61 -4
  72. package/dist/providers/replicate.js +22 -4
  73. package/dist/proxy/codexUsage.js +7 -0
  74. package/dist/proxy/proxyFetch.js +19 -25
  75. package/dist/proxy/proxyTracer.d.ts +14 -7
  76. package/dist/proxy/proxyTracer.js +129 -83
  77. package/dist/proxy/proxyTranslationEngine.js +0 -1
  78. package/dist/proxy/requestLogger.d.ts +1 -1
  79. package/dist/proxy/requestLogger.js +39 -45
  80. package/dist/server/middleware/auth.d.ts +5 -1
  81. package/dist/server/middleware/auth.js +10 -3
  82. package/dist/server/middleware/common.js +1 -1
  83. package/dist/server/routes/agentRoutes.js +4 -4
  84. package/dist/server/routes/claudeProxyRoutes.js +99 -196
  85. package/dist/server/routes/codexProxyRoutes.js +621 -591
  86. package/dist/server/routes/healthRoutes.js +6 -6
  87. package/dist/server/routes/mcpRoutes.js +7 -7
  88. package/dist/server/routes/memoryRoutes.js +7 -7
  89. package/dist/server/routes/openApiRoutes.js +3 -3
  90. package/dist/server/routes/toolRoutes.js +5 -5
  91. package/dist/server/routes/transcribeRoutes.js +1 -1
  92. package/dist/server/utils/validation.d.ts +3 -3
  93. package/dist/services/server/ai/observability/instrumentation.d.ts +34 -201
  94. package/dist/services/server/ai/observability/instrumentation.js +220 -1267
  95. package/dist/services/server/ai/observability/langfuseSetup.d.ts +14 -0
  96. package/dist/services/server/ai/observability/langfuseSetup.js +55 -0
  97. package/dist/services/server/ai/observability/otlpSetup.d.ts +16 -0
  98. package/dist/services/server/ai/observability/otlpSetup.js +58 -0
  99. package/dist/session/globalSessionState.js +0 -11
  100. package/dist/telemetry/attributes.d.ts +23 -46
  101. package/dist/telemetry/attributes.js +76 -162
  102. package/dist/telemetry/chatMessages.d.ts +13 -0
  103. package/dist/telemetry/chatMessages.js +59 -0
  104. package/dist/telemetry/index.d.ts +5 -19
  105. package/dist/telemetry/index.js +5 -26
  106. package/dist/telemetry/observation.d.ts +37 -0
  107. package/dist/telemetry/observation.js +112 -0
  108. package/dist/telemetry/observationNames.d.ts +18 -0
  109. package/dist/telemetry/observationNames.js +24 -0
  110. package/dist/telemetry/spanSanitizer.d.ts +39 -0
  111. package/dist/telemetry/spanSanitizer.js +92 -0
  112. package/dist/telemetry/spanSerialization.d.ts +13 -0
  113. package/dist/telemetry/spanSerialization.js +46 -0
  114. package/dist/telemetry/tracers.d.ts +37 -18
  115. package/dist/telemetry/tracers.js +97 -19
  116. package/dist/telemetry/withSpan.d.ts +47 -25
  117. package/dist/telemetry/withSpan.js +158 -71
  118. package/dist/types/cli.d.ts +0 -11
  119. package/dist/types/decision.d.ts +3 -0
  120. package/dist/types/exporter.d.ts +54 -0
  121. package/dist/types/exporter.js +0 -4
  122. package/dist/types/generate.d.ts +4 -1
  123. package/dist/types/isolatedAgent.d.ts +5 -4
  124. package/dist/types/loopEngine.d.ts +18 -28
  125. package/dist/types/observability.d.ts +127 -160
  126. package/dist/types/providers.d.ts +15 -0
  127. package/dist/types/proxy.d.ts +41 -2
  128. package/dist/types/server.d.ts +9 -5
  129. package/dist/utils/anthropicTraceSanitizer.d.ts +2 -2
  130. package/dist/utils/anthropicTraceSanitizer.js +3 -1
  131. package/dist/utils/conversationMemory.js +4 -4
  132. package/dist/utils/fileDetector.js +1 -1
  133. package/dist/utils/messageBuilder.js +1 -1
  134. package/dist/utils/noOutputSentinel.d.ts +4 -16
  135. package/dist/utils/noOutputSentinel.js +12 -28
  136. package/dist/utils/providerRetry.js +4 -4
  137. package/dist/voice/livekit/brain.js +1 -1
  138. package/dist/voice/livekit/realtimeMemory.d.ts +1 -1
  139. package/dist/voice/livekit/realtimeMemory.js +66 -1
  140. package/dist/voice/livekit/realtimeVoiceAgent.js +1 -1
  141. package/docs-site/static/search-index.json +42 -64
  142. package/package.json +5 -4
  143. package/dist/telemetry/telemetryService.d.ts +0 -79
  144. package/dist/telemetry/telemetryService.js +0 -572
  145. package/dist/utils/observabilityHelpers.d.ts +0 -27
  146. package/dist/utils/observabilityHelpers.js +0 -47
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable max-lines-per-function */
2
2
  // Native SDK imports - no more @ai-sdk/google-vertex dependency
3
3
  import fs from "fs";
4
+ import { LangfuseOtelSpanAttributes } from "@langfuse/tracing";
4
5
  import { guardToolExecutor } from "../../core/toolExecutionGuards.js";
5
6
  import path from "path";
6
7
  import { ErrorCategory, ErrorSeverity, } from "../../constants/enums.js";
@@ -35,17 +36,17 @@ import { createNativeThinkingConfig } from "../../utils/thinkingConfig.js";
35
36
  import { TimeoutError, withTimeout } from "../../utils/async/index.js";
36
37
  import { parseTimeout } from "../../utils/timeout.js";
37
38
  import { appendStepText, buildContextCapMessage, buildDedupedEngineTools, buildLoopExitMessage, buildToolLoopCapMessage, buildWrapupNudgeText, createContextGuard, createTurnClock, extractThoughtSignature, isAbortError, mapGeminiFinishReason, prependConversationMessages, resolveTurnStopReason, DedupExecuteMap, } from "../googleNativeGemini3/index.js";
38
- import { createGeminiLoopAdapter } from "../../core/geminiLoopAdapter.js";
39
+ import { createGeminiLoopAdapter, describeGeminiRequest, } from "../../core/geminiLoopAdapter.js";
39
40
  import { runAgenticLoop } from "../../core/loopEngine.js";
40
41
  import { createAnthropicLoopAdapter } from "../anthropic/loopAdapter.js";
41
- import { extractMcpToolErrorMessage } from "../../utils/mcpErrorText.js";
42
42
  import { createStreamChannel } from "../../core/streamChannel.js";
43
43
  import { toNativeToolDeclarations } from "../../core/nativeToolFormat.js";
44
44
  import { getAvailableInputTokens, getContextWindowSize, } from "../../constants/contextWindows.js";
45
45
  import { resolveLiveTool } from "../../tools/toolDiscovery.js";
46
- import { ATTR, LANGFUSE_ATTR, spanContentAttribute, spanJsonAttribute, spanModelRequestAttribute, tracers, withClientSpan, withClientStreamSpan, withSpan, } from "../../telemetry/index.js";
47
- import { SpanKind, SpanStatusCode, context as otelContext, trace as otelTrace, } from "@opentelemetry/api";
48
- import { calculateCost } from "../../utils/pricing.js";
46
+ import { ATTR, spanContentAttribute, spanModelRequestAttribute, tracers, withClientSpan, withClientStreamSpan, withSpan, } from "../../telemetry/index.js";
47
+ import { observe, observeGeneration, observeTrace, } from "../../telemetry/observation.js";
48
+ import { withGeneration, withStepSpan } from "../../telemetry/withSpan.js";
49
+ import { context as otelContext, SpanKind, SpanStatusCode, trace as otelTrace, } from "@opentelemetry/api";
49
50
  import { transformToolExecutions } from "../../utils/transformationUtils.js";
50
51
  import { resolveToolExecutionRecords } from "../../core/toolExecutionRecorder.js";
51
52
  import { modelSupportsForcedToolChoice, resolveSamplingParams, } from "../../models/modelRegistry.js";
@@ -1000,15 +1001,14 @@ export class GoogleVertexProvider extends BaseProvider {
1000
1001
  // it sees for AI Studio. BaseProvider.stream does NOT emit this span
1001
1002
  // for any provider — each native provider has to add it itself.
1002
1003
  return withClientStreamSpan({
1003
- name: "neurolink.provider.stream",
1004
+ name: "provider.stream",
1004
1005
  tracer: tracers.provider,
1006
+ detached: true,
1005
1007
  attributes: {
1006
- [ATTR.GEN_AI_SYSTEM]: this.providerName,
1007
- [ATTR.GEN_AI_MODEL]: modelName,
1008
- [ATTR.GEN_AI_OPERATION]: "stream",
1008
+ [ATTR.NL_MODEL]: modelName,
1009
1009
  [ATTR.NL_PROVIDER]: this.providerName,
1010
1010
  },
1011
- }, async (streamSpan) => {
1011
+ }, async () => {
1012
1012
  const streamStartTime = Date.now();
1013
1013
  // Tool filter (a0269210): trust options.tools — caller (BaseProvider.stream)
1014
1014
  // already merged MCP/built-in tools and applied any enabledToolNames filter.
@@ -1021,7 +1021,7 @@ export class GoogleVertexProvider extends BaseProvider {
1021
1021
  // Pipeline A. Without this, test:tracing's "Message Build Span"
1022
1022
  // assertion has to skip on every native-Vertex stream.
1023
1023
  const processedOptions = await withSpan({
1024
- name: "neurolink.message.build",
1024
+ name: "message.build",
1025
1025
  tracer: tracers.provider,
1026
1026
  attributes: {
1027
1027
  [ATTR.NL_PROVIDER]: this.providerName,
@@ -1059,10 +1059,6 @@ export class GoogleVertexProvider extends BaseProvider {
1059
1059
  });
1060
1060
  result = await this.runNativeStreamWithMiddleware(mergedOptions, modelName, (effectiveOptions) => this.executeNativeGemini3Stream(effectiveOptions));
1061
1061
  }
1062
- // Cost / token usage on the stream span. Native streams resolve
1063
- // usage synchronously (the stream loop has already drained), so
1064
- // `result.usage` is populated by the time we reach this point.
1065
- this.attachUsageAndCostAttributes(streamSpan, modelName, result?.usage);
1066
1062
  // Wrap the result's async iterable to fire onChunk / onFinish
1067
1063
  // lifecycle callbacks. Pipeline A gets these via the AI SDK
1068
1064
  // wrapStream middleware; the native path has to fire them here.
@@ -1695,6 +1691,7 @@ export class GoogleVertexProvider extends BaseProvider {
1695
1691
  // nudge, and the terminal block below.
1696
1692
  const engineAdapter = createGeminiLoopAdapter({
1697
1693
  providerLabel: "GoogleVertex",
1694
+ provider: this.providerName,
1698
1695
  maxSteps,
1699
1696
  // Ported verbatim: the same DEFAULT_TOOL_MAX_RETRIES threshold, plus
1700
1697
  // the two rules this loop has always had and the engine did not.
@@ -2473,7 +2470,7 @@ export class GoogleVertexProvider extends BaseProvider {
2473
2470
  // Cross-step text accumulation + last SDK finish reason, so the
2474
2471
  // maxSteps-exhaustion exit can surface real gathered text (Bug 1) and a
2475
2472
  // meaningful finishReason (Bug 2) instead of a placeholder / "unknown".
2476
- let accumulatedText = "";
2473
+ const accumulatedText = "";
2477
2474
  let lastFinishReason;
2478
2475
  const allToolCalls = [];
2479
2476
  const toolExecutions = [];
@@ -2540,6 +2537,7 @@ export class GoogleVertexProvider extends BaseProvider {
2540
2537
  // nudge, and the terminal block below.
2541
2538
  const engineAdapter = createGeminiLoopAdapter({
2542
2539
  providerLabel: "GoogleVertex",
2540
+ provider: this.providerName,
2543
2541
  maxSteps,
2544
2542
  // Ported verbatim: the same DEFAULT_TOOL_MAX_RETRIES threshold, plus
2545
2543
  // the two rules this loop has always had and the engine did not.
@@ -2992,57 +2990,76 @@ export class GoogleVertexProvider extends BaseProvider {
2992
2990
  // Vertex endpoint would hang the maxSteps recovery path indefinitely.
2993
2991
  // On timeout withTimeout rejects (TimeoutError) and the catch below
2994
2992
  // falls back to the placeholder — no new failure path is introduced.
2995
- return await withTimeout((async () => {
2996
- const stream = await client.models.generateContentStream({
2997
- model: modelName,
2998
- contents,
2999
- config: synthConfig,
3000
- });
3001
- const parts = [];
3002
- let finishReason;
3003
- let inputTokens = 0;
3004
- let outputTokens = 0;
3005
- let reasoningTokens = 0;
3006
- for await (const chunk of stream) {
3007
- const chunkRecord = chunk;
3008
- const candidates = chunkRecord.candidates;
3009
- const firstCandidate = candidates?.[0];
3010
- const chunkFinishReason = firstCandidate?.finishReason;
3011
- if (typeof chunkFinishReason === "string" && chunkFinishReason) {
3012
- finishReason = chunkFinishReason;
3013
- }
3014
- const chunkContent = firstCandidate?.content;
3015
- if (chunkContent && Array.isArray(chunkContent.parts)) {
3016
- parts.push(...chunkContent.parts);
3017
- }
3018
- const usageMetadata = chunkRecord.usageMetadata;
3019
- if (usageMetadata) {
3020
- if (usageMetadata.promptTokenCount !== undefined &&
3021
- usageMetadata.promptTokenCount > 0) {
3022
- inputTokens = usageMetadata.promptTokenCount;
2993
+ return await withGeneration(describeGeminiRequest(this.providerName, {
2994
+ model: modelName,
2995
+ contents,
2996
+ config,
2997
+ }), async (span) => {
2998
+ const synth = await withTimeout((async () => {
2999
+ const stream = await client.models.generateContentStream({
3000
+ model: modelName,
3001
+ contents,
3002
+ config: synthConfig,
3003
+ });
3004
+ const parts = [];
3005
+ let finishReason;
3006
+ let inputTokens = 0;
3007
+ let outputTokens = 0;
3008
+ let reasoningTokens = 0;
3009
+ for await (const chunk of stream) {
3010
+ const chunkRecord = chunk;
3011
+ const candidates = chunkRecord.candidates;
3012
+ const firstCandidate = candidates?.[0];
3013
+ const chunkFinishReason = firstCandidate?.finishReason;
3014
+ if (typeof chunkFinishReason === "string" &&
3015
+ chunkFinishReason) {
3016
+ finishReason = chunkFinishReason;
3023
3017
  }
3024
- if (usageMetadata.candidatesTokenCount !== undefined &&
3025
- usageMetadata.candidatesTokenCount > 0) {
3026
- outputTokens = usageMetadata.candidatesTokenCount;
3018
+ const chunkContent = firstCandidate?.content;
3019
+ if (chunkContent && Array.isArray(chunkContent.parts)) {
3020
+ parts.push(...chunkContent.parts);
3027
3021
  }
3028
- if (usageMetadata.thoughtsTokenCount !== undefined &&
3029
- usageMetadata.thoughtsTokenCount > 0) {
3030
- reasoningTokens = usageMetadata.thoughtsTokenCount;
3022
+ const usageMetadata = chunkRecord.usageMetadata;
3023
+ if (usageMetadata) {
3024
+ if (usageMetadata.promptTokenCount !== undefined &&
3025
+ usageMetadata.promptTokenCount > 0) {
3026
+ inputTokens = usageMetadata.promptTokenCount;
3027
+ }
3028
+ if (usageMetadata.candidatesTokenCount !== undefined &&
3029
+ usageMetadata.candidatesTokenCount > 0) {
3030
+ outputTokens = usageMetadata.candidatesTokenCount;
3031
+ }
3032
+ if (usageMetadata.thoughtsTokenCount !== undefined &&
3033
+ usageMetadata.thoughtsTokenCount > 0) {
3034
+ reasoningTokens = usageMetadata.thoughtsTokenCount;
3035
+ }
3031
3036
  }
3032
3037
  }
3033
- }
3034
- const text = parts
3035
- .filter((part) => typeof part.text === "string")
3036
- .map((part) => part.text)
3037
- .join("");
3038
- return {
3039
- text,
3040
- finishReason,
3041
- inputTokens,
3042
- outputTokens,
3043
- reasoningTokens,
3044
- };
3045
- })(), timeoutMs, "Gemini synthesis call timed out");
3038
+ const text = parts
3039
+ .filter((part) => typeof part.text === "string")
3040
+ .map((part) => part.text)
3041
+ .join("");
3042
+ return {
3043
+ text,
3044
+ finishReason,
3045
+ inputTokens,
3046
+ outputTokens,
3047
+ reasoningTokens,
3048
+ };
3049
+ })(), timeoutMs, "Gemini synthesis call timed out");
3050
+ observeGeneration(span, {
3051
+ provider: this.providerName,
3052
+ model: modelName,
3053
+ output: synth.text,
3054
+ usage: {
3055
+ inputTokens: synth.inputTokens,
3056
+ outputTokens: synth.outputTokens,
3057
+ reasoningTokens: synth.reasoningTokens,
3058
+ },
3059
+ finishReason: synth.finishReason,
3060
+ });
3061
+ return synth;
3062
+ });
3046
3063
  }
3047
3064
  catch (error) {
3048
3065
  logger.warn("[GoogleVertex] Tools-disabled synthesis call failed; falling back to placeholder", { error: error instanceof Error ? error.message : String(error) });
@@ -3466,24 +3483,18 @@ export class GoogleVertexProvider extends BaseProvider {
3466
3483
  const turnSpan = tracers.provider.startSpan("anthropic.vertex.stream", {
3467
3484
  kind: SpanKind.CLIENT,
3468
3485
  attributes: {
3469
- // Mark as span, not generation — without it Langfuse infers "generation"
3470
- // from the gen_ai.* attributes; the model calls live in child spans.
3471
- [LANGFUSE_ATTR.OBSERVATION_TYPE]: "span",
3472
- [ATTR.GEN_AI_SYSTEM]: "anthropic",
3473
- [ATTR.GEN_AI_MODEL]: modelName,
3474
- [ATTR.GEN_AI_OPERATION]: "stream",
3486
+ [ATTR.NL_MODEL]: modelName,
3475
3487
  [ATTR.NL_PROVIDER]: this.providerName,
3476
3488
  [ATTR.NL_TOOL_COUNT]: offeredToolNames.length,
3477
- [LANGFUSE_ATTR.OBSERVATION_INPUT]: turnInputAttribute,
3478
- // Also lift IO to the trace — Langfuse reads trace input/output from
3479
- // langfuse.trace.* and the trace list is unreadable without it.
3480
- [LANGFUSE_ATTR.TRACE_INPUT]: turnInputAttribute,
3481
- [LANGFUSE_ATTR.OBSERVATION_METADATA]: spanJsonAttribute({
3482
- toolsOffered: offeredToolNames,
3483
- toolCount: offeredToolNames.length,
3484
- maxSteps,
3485
- structuredOutput: useFinalResultTool,
3486
- }),
3489
+ },
3490
+ });
3491
+ observe(turnSpan, "span", {
3492
+ input: turnInputAttribute,
3493
+ metadata: {
3494
+ toolsOffered: offeredToolNames,
3495
+ toolCount: offeredToolNames.length,
3496
+ maxSteps,
3497
+ structuredOutput: useFinalResultTool,
3487
3498
  },
3488
3499
  });
3489
3500
  const turnContext = otelTrace.setSpan(otelContext.active(), turnSpan);
@@ -3543,7 +3554,6 @@ export class GoogleVertexProvider extends BaseProvider {
3543
3554
  const loopPromise = (async () => {
3544
3555
  let step = 0;
3545
3556
  const currentMessages = [...messages];
3546
- let stepConversation = currentMessages;
3547
3557
  // Step-cap / abort bookkeeping (mirrors the generate twin): read by the
3548
3558
  // terminal recovery block after the loop and by the finishReason
3549
3559
  // mapping written into finishReasonRef.
@@ -3558,71 +3568,6 @@ export class GoogleVertexProvider extends BaseProvider {
3558
3568
  const contextGuard = createContextGuard(getContextWindowSize("vertex", modelName));
3559
3569
  const failedTools = new Map();
3560
3570
  try {
3561
- // Restores the per-tool observation the hand-rolled dispatch had.
3562
- // The execution runs INSIDE the span's context so spans the tool opens
3563
- // itself nest under this call rather than dangling beside the turn, and
3564
- // the settled RESULT is inspected — an MCP tool reports failure in its
3565
- // payload, so an observation that only watches for throws records a
3566
- // failed call as successful.
3567
- //
3568
- // Stream path only: the generate twin never had per-tool spans, and
3569
- // giving it them here would be behaviour GAINED under cover of a
3570
- // migration. Shared with resolveToolOnMiss below: the old dispatch
3571
- // opened the span before the executor lookup, so a tool hydrated
3572
- // mid-turn was observed exactly like one declared up front.
3573
- const withToolSpan = (toolCallName, run, call) => {
3574
- // Arguments are inputs: honour the same opt-out the AI SDK path uses.
3575
- const argsAttribute = call?.args !== undefined &&
3576
- process.env.NEUROLINK_RECORD_INPUTS?.trim().toLowerCase() !==
3577
- "false"
3578
- ? spanContentAttribute(call.args)
3579
- : undefined;
3580
- const toolSpan = tracers.mcp.startSpan("ai.toolCall", {
3581
- kind: SpanKind.INTERNAL,
3582
- attributes: {
3583
- [LANGFUSE_ATTR.OBSERVATION_TYPE]: "tool",
3584
- [ATTR.GEN_AI_TOOL_NAME]: toolCallName,
3585
- "ai.toolCall.name": toolCallName,
3586
- ...(call?.toolCallId
3587
- ? { "ai.toolCall.id": call.toolCallId }
3588
- : {}),
3589
- ...(argsAttribute !== undefined
3590
- ? {
3591
- "ai.toolCall.args": argsAttribute,
3592
- [LANGFUSE_ATTR.OBSERVATION_INPUT]: argsAttribute,
3593
- }
3594
- : {}),
3595
- },
3596
- }, turnContext);
3597
- const finish = (output, errorMessage) => {
3598
- const outputAttribute = spanContentAttribute(output);
3599
- toolSpan.setAttribute("ai.toolCall.result", outputAttribute);
3600
- toolSpan.setAttribute(LANGFUSE_ATTR.OBSERVATION_OUTPUT, outputAttribute);
3601
- if (errorMessage) {
3602
- toolSpan.setAttribute(LANGFUSE_ATTR.OBSERVATION_LEVEL, "ERROR");
3603
- toolSpan.setAttribute(LANGFUSE_ATTR.OBSERVATION_STATUS_MESSAGE, errorMessage);
3604
- toolSpan.setStatus({
3605
- code: SpanStatusCode.ERROR,
3606
- message: errorMessage,
3607
- });
3608
- }
3609
- else {
3610
- toolSpan.setStatus({ code: SpanStatusCode.OK });
3611
- }
3612
- toolSpan.end();
3613
- };
3614
- return otelContext.with(otelTrace.setSpan(turnContext, toolSpan), async () => {
3615
- try {
3616
- const result = await run();
3617
- finish(result, extractMcpToolErrorMessage(result));
3618
- return result;
3619
- }
3620
- catch (error) {
3621
- finish({ error: true }, error instanceof Error ? error.message : String(error));
3622
- throw error;
3623
- }
3624
- });
3625
- };
3626
3571
  // Executors handed to the engine, taken through the turn's
3627
3572
  // DedupExecuteMap so an identical repeated call is answered from the
3628
3573
  // per-turn cache rather than run again (BZ-3327). `.get()` returns the
@@ -3644,17 +3589,9 @@ export class GoogleVertexProvider extends BaseProvider {
3644
3589
  toolTimeoutMs: toolExecTimeoutMs,
3645
3590
  abortSignal: internalAbort.signal,
3646
3591
  onProgress: () => turnClock.noteProgress(),
3647
- withToolSpan,
3648
3592
  }),
3649
3593
  };
3650
3594
  }
3651
- // The turn runs on the shared engine. The step cap, tool dispatch, the
3652
- // failure breaker, per-step usage accumulation and the pre-first-chunk
3653
- // provider retry all live there now. What stays here is everything the
3654
- // engine has no opinion about: the turn clock, the context guard, the
3655
- // per-step Langfuse generation span, conversation-memory storage, the
3656
- // wrap-up nudge, and the reserved finalization in the terminal block.
3657
- //
3658
3595
  // maxSteps is agenticStepBudget, NOT maxSteps: when structured output is
3659
3596
  // active the last slot is reserved for the forced final_result call, and
3660
3597
  // the engine must never spend it.
@@ -3662,6 +3599,7 @@ export class GoogleVertexProvider extends BaseProvider {
3662
3599
  // MessageParam and every hook here is typed against the wrong shape.
3663
3600
  const baseAdapter = createAnthropicLoopAdapter({
3664
3601
  client,
3602
+ provider: this.providerName,
3665
3603
  maxSteps: agenticStepBudget,
3666
3604
  toolsRecord: options.tools ?? {},
3667
3605
  // Set here and NOT for native Anthropic: these loops have always had
@@ -3751,15 +3689,10 @@ export class GoogleVertexProvider extends BaseProvider {
3751
3689
  }
3752
3690
  : {}),
3753
3691
  });
3754
- // Wrapped rather than configured: both of these fire once PER STEP, and
3755
- // these are the only hooks that see a single step's request and results.
3756
- // Reading them off the turn's final result would batch every step into
3757
- // one late write and lose the per-step generation span entirely.
3758
3692
  const adapter = {
3759
3693
  ...baseAdapter,
3760
3694
  buildStepRequest: (conversation, engineStep) => {
3761
3695
  step = engineStep + 1;
3762
- stepConversation = conversation;
3763
3696
  turnClock.noteProgress();
3764
3697
  return baseAdapter.buildStepRequest(conversation, engineStep);
3765
3698
  },
@@ -3779,79 +3712,21 @@ export class GoogleVertexProvider extends BaseProvider {
3779
3712
  toolTimeoutMs: toolExecTimeoutMs,
3780
3713
  abortSignal: internalAbort.signal,
3781
3714
  onProgress: () => turnClock.noteProgress(),
3782
- // Same observation as an up-front executor: the hand-rolled
3783
- // dispatch spanned at the call, after the executor lookup, so
3784
- // a hydrated tool was never the one unobserved call in a turn.
3785
- withToolSpan,
3786
3715
  }),
3787
3716
  };
3788
3717
  },
3789
3718
  executeStep: async (request, stepChannel, signal) => {
3790
- // One generation observation per API call: request in, content and
3791
- // usage out. Started here rather than inside the adapter because the
3792
- // attributes are this provider's, not the engine's.
3793
- const generationSpan = tracers.generation.startSpan("anthropic.messages.stream", {
3794
- kind: SpanKind.CLIENT,
3795
- attributes: {
3796
- [LANGFUSE_ATTR.OBSERVATION_TYPE]: "generation",
3797
- [LANGFUSE_ATTR.OBSERVATION_MODEL_NAME]: modelName,
3798
- [LANGFUSE_ATTR.OBSERVATION_MODEL_PARAMETERS]: spanJsonAttribute({
3799
- max_tokens: requestParams.max_tokens,
3800
- temperature: requestParams.temperature,
3801
- top_p: requestParams.top_p,
3802
- }),
3803
- [LANGFUSE_ATTR.OBSERVATION_INPUT]: spanModelRequestAttribute({
3804
- system: systemPromptWithSchema,
3805
- messages: sanitizeAnthropicMessagesForTrace(stepConversation),
3806
- }),
3807
- [LANGFUSE_ATTR.OBSERVATION_METADATA]: spanJsonAttribute({
3808
- step,
3809
- toolsOffered: offeredToolNames.length,
3810
- }),
3811
- [ATTR.GEN_AI_SYSTEM]: "anthropic",
3812
- [ATTR.GEN_AI_MODEL]: modelName,
3813
- [ATTR.GEN_AI_OPERATION]: "chat",
3719
+ const result = await baseAdapter.executeStep(request, {
3720
+ push: (chunk) => {
3721
+ turnClock.noteProgress();
3722
+ stepChannel.push(chunk);
3814
3723
  },
3815
- }, turnContext);
3816
- let firstDeltaSeen = false;
3817
- try {
3818
- const result = await baseAdapter.executeStep(request, {
3819
- push: (chunk) => {
3820
- turnClock.noteProgress();
3821
- if (chunk.content && !firstDeltaSeen) {
3822
- firstDeltaSeen = true;
3823
- // Time-to-first-token for this generation.
3824
- generationSpan.setAttribute(LANGFUSE_ATTR.OBSERVATION_COMPLETION_START_TIME, new Date().toISOString());
3825
- }
3826
- stepChannel.push(chunk);
3827
- },
3828
- }, signal);
3829
- turnCacheUsage.read += result.usage.cacheReadTokens ?? 0;
3830
- turnCacheUsage.creation += result.usage.cacheWriteTokens ?? 0;
3831
- turnCacheUsage.creation5m += result.usage.cacheWrite5mTokens ?? 0;
3832
- turnCacheUsage.creation1h += result.usage.cacheWrite1hTokens ?? 0;
3833
- generationSpan.setAttribute(LANGFUSE_ATTR.OBSERVATION_OUTPUT, spanContentAttribute({
3834
- text: result.text,
3835
- ...(result.toolCalls.length > 0
3836
- ? { toolCalls: result.toolCalls }
3837
- : {}),
3838
- }));
3839
- generationSpan.setAttribute(`${LANGFUSE_ATTR.OBSERVATION_METADATA}.finishReason`, result.rawStopReason ?? "unknown");
3840
- return result;
3841
- }
3842
- catch (error) {
3843
- generationSpan.setStatus({
3844
- code: SpanStatusCode.ERROR,
3845
- message: error instanceof Error ? error.message : String(error),
3846
- });
3847
- if (error instanceof Error) {
3848
- generationSpan.recordException(error);
3849
- }
3850
- throw error;
3851
- }
3852
- finally {
3853
- generationSpan.end();
3854
- }
3724
+ }, signal);
3725
+ turnCacheUsage.read += result.usage.cacheReadTokens ?? 0;
3726
+ turnCacheUsage.creation += result.usage.cacheWriteTokens ?? 0;
3727
+ turnCacheUsage.creation5m += result.usage.cacheWrite5mTokens ?? 0;
3728
+ turnCacheUsage.creation1h += result.usage.cacheWrite1hTokens ?? 0;
3729
+ return result;
3855
3730
  },
3856
3731
  buildToolResultMessages: (conversation, stepResult, toolResults, engineStep) => {
3857
3732
  for (const result of toolResults) {
@@ -3905,15 +3780,14 @@ export class GoogleVertexProvider extends BaseProvider {
3905
3780
  return next;
3906
3781
  },
3907
3782
  };
3908
- const activeSpan = otelTrace.getSpan(turnContext);
3909
- const { stream: engineStream, resultPromise } = runAgenticLoop(adapter, currentMessages.slice(), {
3783
+ const { stream: engineStream, resultPromise } = otelContext.with(turnContext, () => runAgenticLoop(adapter, currentMessages.slice(), {
3910
3784
  tools: engineTools,
3911
3785
  abortSignal: internalAbort.signal,
3912
- ...(activeSpan ? { span: activeSpan } : {}),
3786
+ span: turnSpan,
3913
3787
  // Same value `guardToolExecutor` already received above, so the
3914
3788
  // engine's own per-tool bound cannot undercut it.
3915
3789
  toolTimeoutMs: toolExecTimeoutMs,
3916
- });
3790
+ }));
3917
3791
  const pump = (async () => {
3918
3792
  for await (const chunk of engineStream) {
3919
3793
  if (chunk.reasoning) {
@@ -4050,33 +3924,44 @@ export class GoogleVertexProvider extends BaseProvider {
4050
3924
  tools,
4051
3925
  messages: currentMessages,
4052
3926
  });
4053
- const finalizationStream = await client.messages.stream({
4054
- ...requestParams,
4055
- max_tokens: terminalMaxTokens,
4056
- // Resized, not inherited: terminalMaxTokens can sit below the
4057
- // budget requestParams' thinking was sized for.
4058
- thinking: this.buildClaudeThinkingParam(options, terminalMaxTokens, forceFinalResult),
4059
- ...(forceFinalResult && {
4060
- tool_choice: {
4061
- type: "tool",
4062
- name: "final_result",
4063
- },
4064
- }),
4065
- ...(cachedFinal.system !== undefined && {
4066
- system: cachedFinal.system,
4067
- }),
4068
- ...(cachedFinal.tools &&
4069
- cachedFinal.tools.length > 0 && {
4070
- tools: cachedFinal.tools,
4071
- }),
3927
+ const response = await this.withClaudeGeneration({
3928
+ stepNumber: maxSteps + 1,
3929
+ model: requestParams.model,
3930
+ maxTokens: terminalMaxTokens,
3931
+ temperature: requestParams.temperature,
3932
+ topP: requestParams.top_p,
3933
+ system: cachedFinal.system,
4072
3934
  messages: cachedFinal.messages,
3935
+ }, async () => {
3936
+ const finalizationStream = await client.messages.stream({
3937
+ ...requestParams,
3938
+ max_tokens: terminalMaxTokens,
3939
+ // Resized, not inherited: terminalMaxTokens can sit below the
3940
+ // budget requestParams' thinking was sized for.
3941
+ thinking: this.buildClaudeThinkingParam(options, terminalMaxTokens, forceFinalResult),
3942
+ ...(forceFinalResult && {
3943
+ tool_choice: {
3944
+ type: "tool",
3945
+ name: "final_result",
3946
+ },
3947
+ }),
3948
+ ...(cachedFinal.system !== undefined && {
3949
+ system: cachedFinal.system,
3950
+ }),
3951
+ ...(cachedFinal.tools &&
3952
+ cachedFinal.tools.length > 0 && {
3953
+ tools: cachedFinal.tools,
3954
+ }),
3955
+ messages: cachedFinal.messages,
3956
+ });
3957
+ // Mid-flight aborts are covered by the shared abortHandler via
3958
+ // activeStream; already-fired aborts were handled by the
3959
+ // terminal-entry re-check above.
3960
+ activeStream = finalizationStream;
3961
+ const message = await finalizationStream.finalMessage();
3962
+ activeStream = undefined;
3963
+ return message;
4073
3964
  });
4074
- // Mid-flight aborts are covered by the shared abortHandler via
4075
- // activeStream; already-fired aborts were handled by the
4076
- // terminal-entry re-check above.
4077
- activeStream = finalizationStream;
4078
- const response = await finalizationStream.finalMessage();
4079
- activeStream = undefined;
4080
3965
  usage.input += response.usage?.input_tokens || 0;
4081
3966
  usage.output += response.usage?.output_tokens || 0;
4082
3967
  turnCacheUsage.read +=
@@ -4184,26 +4069,37 @@ export class GoogleVertexProvider extends BaseProvider {
4184
4069
  // is rebuilt for the shrunk limit rather than inherited
4185
4070
  // through the spread below.
4186
4071
  const backstopThinking = this.buildClaudeThinkingParam(options, terminalMaxTokens, false);
4187
- const backstopStream = await client.messages.stream({
4188
- ...requestParams,
4189
- max_tokens: terminalMaxTokens,
4190
- // Assigned outright, never a conditional spread: spreading
4191
- // requestParams above may already carry a `thinking` sized
4192
- // for the original max_tokens, and only adding a key when
4193
- // backstopThinking is truthy would leave that stale value
4194
- // in place instead of clearing it.
4195
- thinking: backstopThinking,
4196
- tool_choice: { type: "none" },
4072
+ const response = await this.withClaudeGeneration({
4073
+ stepNumber: maxSteps + 1,
4074
+ model: requestParams.model,
4075
+ maxTokens: terminalMaxTokens,
4076
+ temperature: requestParams.temperature,
4077
+ topP: requestParams.top_p,
4197
4078
  system: cachedBackstop.system,
4198
- ...(cachedBackstop.tools &&
4199
- cachedBackstop.tools.length > 0 && {
4200
- tools: cachedBackstop.tools,
4201
- }),
4202
4079
  messages: cachedBackstop.messages,
4080
+ }, async () => {
4081
+ const backstopStream = await client.messages.stream({
4082
+ ...requestParams,
4083
+ max_tokens: terminalMaxTokens,
4084
+ // Assigned outright, never a conditional spread: spreading
4085
+ // requestParams above may already carry a `thinking` sized
4086
+ // for the original max_tokens, and only adding a key when
4087
+ // backstopThinking is truthy would leave that stale value
4088
+ // in place instead of clearing it.
4089
+ thinking: backstopThinking,
4090
+ tool_choice: { type: "none" },
4091
+ system: cachedBackstop.system,
4092
+ ...(cachedBackstop.tools &&
4093
+ cachedBackstop.tools.length > 0 && {
4094
+ tools: cachedBackstop.tools,
4095
+ }),
4096
+ messages: cachedBackstop.messages,
4097
+ });
4098
+ activeStream = backstopStream;
4099
+ const message = await backstopStream.finalMessage();
4100
+ activeStream = undefined;
4101
+ return message;
4203
4102
  });
4204
- activeStream = backstopStream;
4205
- const response = await backstopStream.finalMessage();
4206
- activeStream = undefined;
4207
4103
  usage.input += response.usage?.input_tokens || 0;
4208
4104
  usage.output += response.usage?.output_tokens || 0;
4209
4105
  turnCacheUsage.read +=
@@ -4363,53 +4259,59 @@ export class GoogleVertexProvider extends BaseProvider {
4363
4259
  if (turnCacheUsage.creation > 0) {
4364
4260
  usage.cacheCreationTokens = turnCacheUsage.creation;
4365
4261
  }
4366
- const turnOutputAttribute = spanContentAttribute({
4367
- text: aggregatedTurnText,
4368
- ...(structuredOutputRef.value
4369
- ? { structuredOutput: structuredOutputRef.value }
4370
- : {}),
4371
- });
4372
- turnSpan.setAttribute(LANGFUSE_ATTR.OBSERVATION_OUTPUT, turnOutputAttribute);
4373
- turnSpan.setAttribute(LANGFUSE_ATTR.TRACE_OUTPUT, turnOutputAttribute);
4374
- // Turn usage is metadata-only (not usage_details) — see the note at the
4375
- // top of this method on why it must not contribute to the cost rollup.
4376
- turnSpan.setAttribute(LANGFUSE_ATTR.OBSERVATION_METADATA, spanJsonAttribute({
4377
- toolsOffered: offeredToolNames,
4378
- toolCount: offeredToolNames.length,
4379
- maxSteps,
4380
- steps: step,
4381
- toolCallCount: metadata.totalToolExecutions,
4382
- toolsCalled: toolsUsedRef.filter((name) => name !== "final_result"),
4383
- structuredOutput: useFinalResultTool,
4384
- ...(stopReasonRef.value ? { stopReason: stopReasonRef.value } : {}),
4385
- usage: {
4386
- input: usage.input,
4387
- output: usage.output,
4388
- input_cached_tokens: turnCacheUsage.read,
4389
- input_cache_creation: turnCacheUsage.creation,
4390
- input_cache_creation_5m: turnCacheUsage.creation5m,
4391
- input_cache_creation_1h: turnCacheUsage.creation1h,
4262
+ observe(turnSpan, "span", {
4263
+ output: {
4264
+ text: aggregatedTurnText,
4265
+ ...(structuredOutputRef.value
4266
+ ? { structuredOutput: structuredOutputRef.value }
4267
+ : {}),
4392
4268
  },
4393
- }));
4269
+ // Turn usage is metadata-only (not usage_details) — see the note at the
4270
+ // top of this method on why it must not contribute to the cost rollup.
4271
+ metadata: {
4272
+ toolsOffered: offeredToolNames,
4273
+ toolCount: offeredToolNames.length,
4274
+ maxSteps,
4275
+ steps: step,
4276
+ toolCallCount: metadata.totalToolExecutions,
4277
+ toolsCalled: toolsUsedRef.filter((name) => name !== "final_result"),
4278
+ structuredOutput: useFinalResultTool,
4279
+ ...(stopReasonRef.value ? { stopReason: stopReasonRef.value } : {}),
4280
+ usage: {
4281
+ input: usage.input,
4282
+ output: usage.output,
4283
+ input_cached_tokens: turnCacheUsage.read,
4284
+ input_cache_creation: turnCacheUsage.creation,
4285
+ input_cache_creation_5m: turnCacheUsage.creation5m,
4286
+ input_cache_creation_1h: turnCacheUsage.creation1h,
4287
+ },
4288
+ },
4289
+ });
4394
4290
  // How the turn ended, on the trace itself, so an evaluator can select
4395
4291
  // only turns that finished ("completed") and skip capped, timed-out,
4396
4292
  // aborted and failed ones. A trace holding several turns keeps the last
4397
4293
  // one's; each turn's own is in its observation metadata above.
4398
4294
  if (stopReasonRef.value) {
4399
- turnSpan.setAttribute(`${LANGFUSE_ATTR.TRACE_METADATA}.stopReason`, stopReasonRef.value);
4295
+ observeTrace(turnSpan, {
4296
+ metadata: { stopReason: stopReasonRef.value },
4297
+ });
4400
4298
  }
4401
4299
  turnSpan.setStatus({ code: SpanStatusCode.OK });
4402
4300
  channel.close();
4403
4301
  }
4404
4302
  catch (err) {
4405
- turnSpan.setAttribute(LANGFUSE_ATTR.OBSERVATION_METADATA, spanJsonAttribute({
4406
- toolsOffered: offeredToolNames,
4407
- toolCount: offeredToolNames.length,
4408
- maxSteps,
4409
- structuredOutput: useFinalResultTool,
4410
- stopReason: "provider-error",
4411
- }));
4412
- turnSpan.setAttribute(`${LANGFUSE_ATTR.TRACE_METADATA}.stopReason`, "provider-error");
4303
+ observe(turnSpan, "span", {
4304
+ level: "ERROR",
4305
+ statusMessage: err instanceof Error ? err.message : String(err),
4306
+ metadata: {
4307
+ toolsOffered: offeredToolNames,
4308
+ toolCount: offeredToolNames.length,
4309
+ maxSteps,
4310
+ structuredOutput: useFinalResultTool,
4311
+ stopReason: "provider-error",
4312
+ },
4313
+ });
4314
+ observeTrace(turnSpan, { metadata: { stopReason: "provider-error" } });
4413
4315
  turnSpan.setStatus({
4414
4316
  code: SpanStatusCode.ERROR,
4415
4317
  message: err instanceof Error ? err.message : String(err),
@@ -4785,7 +4687,7 @@ export class GoogleVertexProvider extends BaseProvider {
4785
4687
  let hitStepLimit = false;
4786
4688
  let hitContextLimit = false;
4787
4689
  let synthesizedFinalAnswer = false;
4788
- let modelFinished = false;
4690
+ const modelFinished = false;
4789
4691
  const currentMessages = [...messages];
4790
4692
  // In-loop context guard: stop calling tools when the accumulated
4791
4693
  // conversation approaches the model's real context window, instead of
@@ -4855,6 +4757,7 @@ export class GoogleVertexProvider extends BaseProvider {
4855
4757
  // engine must never spend it.
4856
4758
  const baseAdapter = createAnthropicLoopAdapter({
4857
4759
  client,
4760
+ provider: this.providerName,
4858
4761
  maxSteps: agenticStepBudget,
4859
4762
  toolsRecord: options.tools ?? {},
4860
4763
  toolFailureBreaker: {
@@ -5130,7 +5033,15 @@ export class GoogleVertexProvider extends BaseProvider {
5130
5033
  tools,
5131
5034
  messages: currentMessages,
5132
5035
  });
5133
- const response = await withTimeout(client.messages.create({
5036
+ const response = await this.withClaudeGeneration({
5037
+ stepNumber: maxSteps + 1,
5038
+ model: requestParams.model,
5039
+ maxTokens: terminalMaxTokens,
5040
+ temperature: requestParams.temperature,
5041
+ topP: requestParams.top_p,
5042
+ system: cachedFinal.system,
5043
+ messages: cachedFinal.messages,
5044
+ }, () => withTimeout(client.messages.create({
5134
5045
  ...requestParams,
5135
5046
  max_tokens: terminalMaxTokens,
5136
5047
  // Resized, not inherited: terminalMaxTokens can sit below the
@@ -5150,7 +5061,7 @@ export class GoogleVertexProvider extends BaseProvider {
5150
5061
  tools: cachedFinal.tools,
5151
5062
  }),
5152
5063
  messages: cachedFinal.messages,
5153
- }, { signal: internalAbort.signal }), generateTimeoutMs, "Anthropic finalization call timed out");
5064
+ }, { signal: internalAbort.signal }), generateTimeoutMs, "Anthropic finalization call timed out"));
5154
5065
  totalInputTokens += response.usage?.input_tokens || 0;
5155
5066
  totalOutputTokens += response.usage?.output_tokens || 0;
5156
5067
  totalCacheReadTokens += response.usage?.cache_read_input_tokens || 0;
@@ -5242,7 +5153,15 @@ export class GoogleVertexProvider extends BaseProvider {
5242
5153
  // rebuilt for the shrunk limit rather than inherited through the
5243
5154
  // spread below.
5244
5155
  const backstopThinking = this.buildClaudeThinkingParam(options, terminalMaxTokens, false);
5245
- const response = await withTimeout(client.messages.create({
5156
+ const response = await this.withClaudeGeneration({
5157
+ stepNumber: maxSteps + 1,
5158
+ model: requestParams.model,
5159
+ maxTokens: terminalMaxTokens,
5160
+ temperature: requestParams.temperature,
5161
+ topP: requestParams.top_p,
5162
+ system: cachedBackstop.system,
5163
+ messages: cachedBackstop.messages,
5164
+ }, () => withTimeout(client.messages.create({
5246
5165
  ...requestParams,
5247
5166
  max_tokens: terminalMaxTokens,
5248
5167
  // Assigned outright, never a conditional spread: spreading
@@ -5258,7 +5177,7 @@ export class GoogleVertexProvider extends BaseProvider {
5258
5177
  tools: cachedBackstop.tools,
5259
5178
  }),
5260
5179
  messages: cachedBackstop.messages,
5261
- }, { signal: internalAbort.signal }), generateTimeoutMs, "Anthropic backstop call timed out");
5180
+ }, { signal: internalAbort.signal }), generateTimeoutMs, "Anthropic backstop call timed out"));
5262
5181
  totalInputTokens += response.usage?.input_tokens || 0;
5263
5182
  totalOutputTokens += response.usage?.output_tokens || 0;
5264
5183
  totalCacheReadTokens +=
@@ -5569,6 +5488,14 @@ export class GoogleVertexProvider extends BaseProvider {
5569
5488
  // For non-image models, call the parent stream method
5570
5489
  return super.stream(optionsOrPrompt);
5571
5490
  }
5491
+ observeMediaGeneration(model, run) {
5492
+ const call = { provider: this.providerName, model };
5493
+ return withGeneration(call, async (span) => {
5494
+ const result = await run();
5495
+ observeGeneration(span, { ...call, usage: result?.usage });
5496
+ return result;
5497
+ });
5498
+ }
5572
5499
  /**
5573
5500
  * Override generate to route ALL models to native SDKs
5574
5501
  * No more @ai-sdk/google-vertex dependency
@@ -5585,12 +5512,11 @@ export class GoogleVertexProvider extends BaseProvider {
5585
5512
  // Vertex's generate() bypasses BaseProvider.generate entirely, so
5586
5513
  // without this wrapping the provider span never gets emitted.
5587
5514
  return withClientSpan({
5588
- name: "neurolink.provider.generate",
5515
+ name: "provider.generate",
5589
5516
  tracer: tracers.provider,
5517
+ detached: true,
5590
5518
  attributes: {
5591
- [ATTR.GEN_AI_SYSTEM]: this.providerName,
5592
- [ATTR.GEN_AI_MODEL]: modelName,
5593
- [ATTR.GEN_AI_OPERATION]: "generate",
5519
+ [ATTR.NL_MODEL]: modelName,
5594
5520
  [ATTR.NL_PROVIDER]: this.providerName,
5595
5521
  },
5596
5522
  }, async (generateSpan) => {
@@ -5610,8 +5536,7 @@ export class GoogleVertexProvider extends BaseProvider {
5610
5536
  // of producing video bytes.
5611
5537
  if (requestKind === "video") {
5612
5538
  logger.info("[GoogleVertex] Routing video-mode generate to handleVideoGeneration", { model: modelName });
5613
- const videoResult = await this.handleVideoGeneration(options, generateStartTime);
5614
- this.attachUsageAndCostAttributes(generateSpan, modelName, videoResult?.usage);
5539
+ const videoResult = await this.observeMediaGeneration(modelName, () => this.handleVideoGeneration(options, generateStartTime));
5615
5540
  this.emitGenerationEnd(modelName, videoResult, generateStartTime, true);
5616
5541
  return videoResult;
5617
5542
  }
@@ -5632,8 +5557,7 @@ export class GoogleVertexProvider extends BaseProvider {
5632
5557
  // gemini-3.1-flash-image-preview on the text path.
5633
5558
  if (requestKind === "image") {
5634
5559
  logger.info("[GoogleVertex] Routing image generation model to executeImageGeneration", { model: modelName });
5635
- const imageResult = await this.executeImageGeneration(options);
5636
- this.attachUsageAndCostAttributes(generateSpan, modelName, imageResult?.usage);
5560
+ const imageResult = await this.observeMediaGeneration(modelName, () => this.executeImageGeneration(options));
5637
5561
  this.emitGenerationEnd(modelName, imageResult, generateStartTime, true);
5638
5562
  return imageResult;
5639
5563
  }
@@ -5654,7 +5578,7 @@ export class GoogleVertexProvider extends BaseProvider {
5654
5578
  // span here. Without this the test:tracing "Message Build Span"
5655
5579
  // assertion has to skip on every native-Vertex run.
5656
5580
  const processedOptions = await withSpan({
5657
- name: "neurolink.message.build",
5581
+ name: "message.build",
5658
5582
  tracer: tracers.provider,
5659
5583
  attributes: {
5660
5584
  [ATTR.NL_PROVIDER]: this.providerName,
@@ -5681,7 +5605,7 @@ export class GoogleVertexProvider extends BaseProvider {
5681
5605
  : {}),
5682
5606
  prompt: inputPrompt,
5683
5607
  });
5684
- generateSpan.setAttribute(LANGFUSE_ATTR.OBSERVATION_INPUT, generationInputAttribute);
5608
+ generateSpan.setAttribute(LangfuseOtelSpanAttributes.OBSERVATION_INPUT, generationInputAttribute);
5685
5609
  try {
5686
5610
  let result;
5687
5611
  // Wrap the actual native generate call in `neurolink.executeGeneration`
@@ -5690,15 +5614,15 @@ export class GoogleVertexProvider extends BaseProvider {
5690
5614
  // native @google/genai / @anthropic-ai/vertex-sdk path. This
5691
5615
  // provider overrides generate(), so the span is added here.
5692
5616
  result = await withSpan({
5693
- name: "neurolink.executeGeneration",
5617
+ name: "executeGeneration",
5694
5618
  tracer: tracers.provider,
5619
+ detached: true,
5695
5620
  attributes: {
5696
- [ATTR.GEN_AI_SYSTEM]: this.providerName,
5697
- [ATTR.GEN_AI_MODEL]: modelName,
5698
- "neurolink.path": isAnthropicModel(modelName)
5621
+ [ATTR.NL_MODEL]: modelName,
5622
+ path: isAnthropicModel(modelName)
5699
5623
  ? "native.anthropic"
5700
5624
  : "native.google-genai",
5701
- [LANGFUSE_ATTR.OBSERVATION_INPUT]: generationInputAttribute,
5625
+ [LangfuseOtelSpanAttributes.OBSERVATION_INPUT]: generationInputAttribute,
5702
5626
  },
5703
5627
  }, async (executionSpan) => {
5704
5628
  let nativeResult;
@@ -5716,16 +5640,15 @@ export class GoogleVertexProvider extends BaseProvider {
5716
5640
  });
5717
5641
  nativeResult = await this.runNativeGenerateWithMiddleware(mergedOptions, modelName, (effectiveOptions) => this.executeNativeGemini3GenerateWithSchemaFallback(effectiveOptions, modelName));
5718
5642
  }
5719
- executionSpan.setAttribute(LANGFUSE_ATTR.OBSERVATION_OUTPUT, spanContentAttribute(nativeResult?.content ?? ""));
5643
+ executionSpan.setAttribute(LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT, spanContentAttribute(nativeResult?.content ?? ""));
5720
5644
  return nativeResult;
5721
5645
  });
5722
- this.attachUsageAndCostAttributes(generateSpan, modelName, result?.usage);
5723
5646
  // Pipe through TTS-of-AI-response when caller asks for it. The
5724
5647
  // shared `synthesizeAIResponseIfNeeded` no-ops when tts is not
5725
5648
  // enabled / useAiResponse is false, so the cost is zero on
5726
5649
  // non-TTS paths.
5727
5650
  result = await this.synthesizeAIResponseIfNeeded(result, options);
5728
- generateSpan.setAttribute(LANGFUSE_ATTR.OBSERVATION_OUTPUT, spanContentAttribute(result?.content ?? ""));
5651
+ generateSpan.setAttribute(LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT, spanContentAttribute(result?.content ?? ""));
5729
5652
  // Fire onFinish lifecycle callback for the native generate path.
5730
5653
  // Pipeline A providers get this for free via the AI SDK middleware
5731
5654
  // wrapper (LifecycleMiddleware); native @google/genai bypasses
@@ -6389,54 +6312,35 @@ export class GoogleVertexProvider extends BaseProvider {
6389
6312
  }
6390
6313
  return wrapped;
6391
6314
  }
6392
- /**
6393
- * Attach `gen_ai.usage.*` and `neurolink.cost` attributes to a span.
6394
- * Pulled out so the generate / stream / image-gen paths share one
6395
- * implementation, and so observability/tracing tests find consistent
6396
- * attributes regardless of which native sub-route fulfilled the request.
6397
- */
6398
- attachUsageAndCostAttributes(span, modelName, usage) {
6399
- if (!span || !usage) {
6400
- return;
6401
- }
6402
- const inputTokens = usage.input ?? usage.inputTokens ?? 0;
6403
- const outputTokens = usage.output ?? usage.outputTokens ?? 0;
6404
- const totalTokens = usage.total ?? usage.totalTokens ?? inputTokens + outputTokens;
6405
- const cacheReadTokens = usage.cacheReadTokens ?? 0;
6406
- const cacheCreationTokens = usage.cacheCreationTokens ?? 0;
6407
- if (inputTokens > 0) {
6408
- span.setAttribute("gen_ai.usage.input_tokens", inputTokens);
6409
- }
6410
- if (outputTokens > 0) {
6411
- span.setAttribute("gen_ai.usage.output_tokens", outputTokens);
6412
- }
6413
- if (totalTokens > 0) {
6414
- span.setAttribute("gen_ai.usage.total_tokens", totalTokens);
6415
- }
6416
- if (cacheReadTokens > 0) {
6417
- span.setAttribute("gen_ai.usage.cache_read_input_tokens", cacheReadTokens);
6418
- }
6419
- if (cacheCreationTokens > 0) {
6420
- span.setAttribute("gen_ai.usage.cache_creation_input_tokens", cacheCreationTokens);
6421
- }
6422
- try {
6423
- // Pass cache tokens through so calculateCost prices the ~0.1x cache-read
6424
- // and ~1.25x cache-write tiers instead of billing cached content at the
6425
- // full input rate.
6426
- const cost = calculateCost(this.providerName, modelName, {
6427
- input: inputTokens,
6428
- output: outputTokens,
6429
- total: totalTokens,
6430
- cacheReadTokens,
6431
- cacheCreationTokens,
6315
+ withClaudeGeneration(sent, send) {
6316
+ const call = { provider: this.providerName, model: sent.model };
6317
+ return withStepSpan(sent.stepNumber, () => withGeneration({
6318
+ ...call,
6319
+ modelParameters: {
6320
+ temperature: sent.temperature,
6321
+ maxTokens: sent.maxTokens,
6322
+ topP: sent.topP,
6323
+ },
6324
+ input: {
6325
+ system: sent.system,
6326
+ messages: sanitizeAnthropicMessagesForTrace(sent.messages),
6327
+ },
6328
+ }, async (span) => {
6329
+ const response = await send();
6330
+ observeGeneration(span, {
6331
+ ...call,
6332
+ responseModel: response.model,
6333
+ output: response.content,
6334
+ usage: {
6335
+ inputTokens: response.usage?.input_tokens,
6336
+ outputTokens: response.usage?.output_tokens,
6337
+ cacheReadTokens: response.usage?.cache_read_input_tokens ?? undefined,
6338
+ cacheCreationTokens: response.usage?.cache_creation_input_tokens ?? undefined,
6339
+ },
6340
+ finishReason: response.stop_reason ?? undefined,
6432
6341
  });
6433
- if (typeof cost === "number" && cost > 0) {
6434
- span.setAttribute("neurolink.cost", cost);
6435
- }
6436
- }
6437
- catch {
6438
- // Pricing table miss is not fatal — leave the attribute unset.
6439
- }
6342
+ return response;
6343
+ }));
6440
6344
  }
6441
6345
  /**
6442
6346
  * Emit `generation:end` so the Pipeline B observability listener creates