@juspay/neurolink 12.58.1 → 13.0.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 (143) hide show
  1. package/CHANGELOG.md +14 -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/common.js +1 -1
  81. package/dist/server/routes/agentRoutes.js +4 -4
  82. package/dist/server/routes/claudeProxyRoutes.js +99 -196
  83. package/dist/server/routes/codexProxyRoutes.js +621 -591
  84. package/dist/server/routes/healthRoutes.js +6 -6
  85. package/dist/server/routes/mcpRoutes.js +7 -7
  86. package/dist/server/routes/memoryRoutes.js +7 -7
  87. package/dist/server/routes/openApiRoutes.js +3 -3
  88. package/dist/server/routes/toolRoutes.js +5 -5
  89. package/dist/server/routes/transcribeRoutes.js +1 -1
  90. package/dist/server/utils/validation.d.ts +3 -3
  91. package/dist/services/server/ai/observability/instrumentation.d.ts +34 -201
  92. package/dist/services/server/ai/observability/instrumentation.js +220 -1267
  93. package/dist/services/server/ai/observability/langfuseSetup.d.ts +14 -0
  94. package/dist/services/server/ai/observability/langfuseSetup.js +55 -0
  95. package/dist/services/server/ai/observability/otlpSetup.d.ts +16 -0
  96. package/dist/services/server/ai/observability/otlpSetup.js +58 -0
  97. package/dist/session/globalSessionState.js +0 -11
  98. package/dist/telemetry/attributes.d.ts +23 -46
  99. package/dist/telemetry/attributes.js +76 -162
  100. package/dist/telemetry/chatMessages.d.ts +13 -0
  101. package/dist/telemetry/chatMessages.js +59 -0
  102. package/dist/telemetry/index.d.ts +5 -19
  103. package/dist/telemetry/index.js +5 -26
  104. package/dist/telemetry/observation.d.ts +37 -0
  105. package/dist/telemetry/observation.js +112 -0
  106. package/dist/telemetry/observationNames.d.ts +18 -0
  107. package/dist/telemetry/observationNames.js +24 -0
  108. package/dist/telemetry/spanSanitizer.d.ts +39 -0
  109. package/dist/telemetry/spanSanitizer.js +92 -0
  110. package/dist/telemetry/spanSerialization.d.ts +13 -0
  111. package/dist/telemetry/spanSerialization.js +46 -0
  112. package/dist/telemetry/tracers.d.ts +37 -18
  113. package/dist/telemetry/tracers.js +97 -19
  114. package/dist/telemetry/withSpan.d.ts +47 -25
  115. package/dist/telemetry/withSpan.js +158 -71
  116. package/dist/types/cli.d.ts +0 -11
  117. package/dist/types/decision.d.ts +3 -0
  118. package/dist/types/exporter.d.ts +54 -0
  119. package/dist/types/exporter.js +0 -4
  120. package/dist/types/generate.d.ts +4 -1
  121. package/dist/types/isolatedAgent.d.ts +5 -4
  122. package/dist/types/loopEngine.d.ts +18 -28
  123. package/dist/types/observability.d.ts +127 -160
  124. package/dist/types/providers.d.ts +15 -0
  125. package/dist/types/proxy.d.ts +41 -2
  126. package/dist/utils/anthropicTraceSanitizer.d.ts +2 -2
  127. package/dist/utils/anthropicTraceSanitizer.js +3 -1
  128. package/dist/utils/conversationMemory.js +4 -4
  129. package/dist/utils/fileDetector.js +1 -1
  130. package/dist/utils/messageBuilder.js +1 -1
  131. package/dist/utils/noOutputSentinel.d.ts +4 -16
  132. package/dist/utils/noOutputSentinel.js +12 -28
  133. package/dist/utils/providerRetry.js +4 -4
  134. package/dist/voice/livekit/brain.js +1 -1
  135. package/dist/voice/livekit/realtimeMemory.d.ts +1 -1
  136. package/dist/voice/livekit/realtimeMemory.js +66 -1
  137. package/dist/voice/livekit/realtimeVoiceAgent.js +1 -1
  138. package/docs-site/static/search-index.json +42 -63
  139. package/package.json +10 -4
  140. package/dist/telemetry/telemetryService.d.ts +0 -79
  141. package/dist/telemetry/telemetryService.js +0 -572
  142. package/dist/utils/observabilityHelpers.d.ts +0 -27
  143. package/dist/utils/observabilityHelpers.js +0 -47
@@ -1,3 +1,4 @@
1
+ import { context, SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
1
2
  import { BaseProvider } from "../core/baseProvider.js";
2
3
  import { createStreamChannel } from "../core/streamChannel.js";
3
4
  import { logger } from "../utils/logger.js";
@@ -9,7 +10,8 @@ import { withProviderRetry } from "../utils/providerRetry.js";
9
10
  import { resolveToolChoice } from "../utils/toolChoice.js";
10
11
  import { DEFAULT_MAX_STEPS } from "../core/constants.js";
11
12
  import { hasNativeDoGenerate, runNativeGenerateLoop, } from "../core/nativeGenerateLoop.js";
12
- import { withSpan } from "../telemetry/withSpan.js";
13
+ import { generationName } from "../telemetry/observationNames.js";
14
+ import { observeGeneration } from "../telemetry/observation.js";
13
15
  import { tracers } from "../telemetry/tracers.js";
14
16
  // SageMaker-specific imports
15
17
  import { getDefaultSageMakerEndpoint, getSageMakerConfig, getSageMakerModel, getSageMakerModelConfig, } from "./sagemaker/config.js";
@@ -230,6 +232,7 @@ export class AmazonSageMakerProvider extends BaseProvider {
230
232
  const toolExecutionSummaries = [];
231
233
  const loop = await runNativeGenerateLoop({
232
234
  doGenerate,
235
+ provider: this.providerName,
233
236
  conversation,
234
237
  ...(responseFormat ? { responseFormat } : {}),
235
238
  ...(v3Tools.length > 0 ? { tools: v3Tools } : {}),
@@ -292,94 +295,129 @@ export class AmazonSageMakerProvider extends BaseProvider {
292
295
  * detached pump, so they settle whether or not the caller reads a chunk.
293
296
  */
294
297
  async doStream(options) {
295
- return withSpan({
296
- name: "neurolink.provider.sagemaker.stream",
297
- tracer: tracers.stream,
298
+ const span = tracers.generation.startSpan(generationName("stream"), {
299
+ kind: SpanKind.CLIENT,
298
300
  attributes: {
299
- "provider.name": "sagemaker",
300
- "model.name": this.modelName,
301
301
  "sagemaker.endpoint": this.modelConfig.endpointName,
302
302
  "sagemaker.region": this.sagemakerConfig.region,
303
303
  },
304
- }, async () => {
305
- try {
306
- const messages = await this.buildMessagesForStream(options);
307
- const result = await this.sagemakerModel.doStream({
308
- prompt: messages,
309
- maxTokens: options.maxTokens,
304
+ });
305
+ const call = {
306
+ provider: this.providerName,
307
+ model: this.modelName,
308
+ };
309
+ try {
310
+ const messages = await this.buildMessagesForStream(options);
311
+ observeGeneration(span, {
312
+ ...call,
313
+ modelParameters: {
310
314
  temperature: options.temperature,
311
- });
312
- // Settled from the `finish` part below. A turn that ends without one
313
- // — a truncated or errored stream — still settles, so a caller
314
- // awaiting either promise cannot hang.
315
- let settleFinishReason;
316
- let settleUsage;
317
- const finishReason = new Promise((resolve) => {
318
- settleFinishReason = resolve;
319
- });
320
- const usage = new Promise((resolve) => {
321
- settleUsage = resolve;
322
- });
323
- // The source is drained by a detached pump rather than lazily by the
324
- // consumer. Resolving `finishReason`/`usage` from inside a generator
325
- // ties them to somebody iterating it, and the inherited
326
- // `executeStream` chains analytics off exactly those promises — so a
327
- // caller that awaits `result.analytics` without consuming the stream
328
- // would wait forever. Pumping here means the turn completes, and
329
- // both promises settle, whether or not anyone reads a chunk.
330
- const channel = createStreamChannel();
331
- void (async () => {
332
- const reader = result.stream.getReader();
333
- let sawFinish = false;
334
- try {
335
- while (true) {
336
- const { done, value } = await reader.read();
337
- if (done) {
338
- break;
339
- }
340
- const part = value;
341
- if (part.type === "text-delta" && part.textDelta) {
342
- channel.push({ content: part.textDelta });
343
- continue;
344
- }
345
- if (part.type === "finish") {
346
- sawFinish = true;
347
- settleFinishReason(part.finishReason ?? "stop");
348
- settleUsage({
349
- inputTokens: part.usage?.inputTokens ?? part.usage?.promptTokens ?? 0,
350
- outputTokens: part.usage?.outputTokens ??
351
- part.usage?.completionTokens ??
352
- 0,
353
- });
354
- }
315
+ maxTokens: options.maxTokens,
316
+ },
317
+ input: { messages },
318
+ });
319
+ const result = await context.with(trace.setSpan(context.active(), span), () => this.sagemakerModel.doStream({
320
+ prompt: messages,
321
+ maxTokens: options.maxTokens,
322
+ temperature: options.temperature,
323
+ }));
324
+ // Settled from the `finish` part below. A turn that ends without one
325
+ // — a truncated or errored stream — still settles, so a caller
326
+ // awaiting either promise cannot hang.
327
+ let settleFinishReason;
328
+ let settleUsage;
329
+ const finishReason = new Promise((resolve) => {
330
+ settleFinishReason = resolve;
331
+ });
332
+ const usage = new Promise((resolve) => {
333
+ settleUsage = resolve;
334
+ });
335
+ // The source is drained by a detached pump rather than lazily by the
336
+ // consumer. Resolving `finishReason`/`usage` from inside a generator
337
+ // ties them to somebody iterating it, and the inherited
338
+ // `executeStream` chains analytics off exactly those promises — so a
339
+ // caller that awaits `result.analytics` without consuming the stream
340
+ // would wait forever. Pumping here means the turn completes, and
341
+ // both promises settle, whether or not anyone reads a chunk.
342
+ const channel = createStreamChannel();
343
+ void (async () => {
344
+ const reader = result.stream.getReader();
345
+ let sawFinish = false;
346
+ let text = "";
347
+ let firstChunkAt;
348
+ let finish = "unknown";
349
+ let tokens = { inputTokens: 0, outputTokens: 0 };
350
+ try {
351
+ while (true) {
352
+ const { done, value } = await reader.read();
353
+ if (done) {
354
+ break;
355
355
  }
356
- channel.close();
357
- }
358
- catch (error) {
359
- channel.error(error);
360
- }
361
- finally {
362
- reader.releaseLock();
363
- // A stream that ended without a finish part — truncated or
364
- // errored — must still settle both promises, or the same hang
365
- // returns by a different route.
366
- if (!sawFinish) {
367
- settleFinishReason("unknown");
368
- settleUsage({ inputTokens: 0, outputTokens: 0 });
356
+ const part = value;
357
+ if (part.type === "text-delta" && part.textDelta) {
358
+ firstChunkAt ??= new Date();
359
+ text += part.textDelta;
360
+ channel.push({ content: part.textDelta });
361
+ continue;
362
+ }
363
+ if (part.type === "finish") {
364
+ sawFinish = true;
365
+ finish = part.finishReason ?? "stop";
366
+ tokens = {
367
+ inputTokens: part.usage?.inputTokens ?? part.usage?.promptTokens ?? 0,
368
+ outputTokens: part.usage?.outputTokens ?? part.usage?.completionTokens ?? 0,
369
+ };
370
+ settleFinishReason(finish);
371
+ settleUsage(tokens);
369
372
  }
370
373
  }
371
- })();
372
- return {
373
- stream: channel.iterable,
374
- finishReason,
375
- usage,
376
- warnings: (result.warnings ?? []).map((warning) => warning.message ?? String(warning)),
377
- };
378
- }
379
- catch (error) {
380
- throw this.handleProviderError(error);
374
+ channel.close();
375
+ span.setStatus({ code: SpanStatusCode.OK });
376
+ }
377
+ catch (error) {
378
+ span.setStatus({
379
+ code: SpanStatusCode.ERROR,
380
+ message: error instanceof Error ? error.message : String(error),
381
+ });
382
+ channel.error(error);
383
+ }
384
+ finally {
385
+ reader.releaseLock();
386
+ // A stream that ended without a finish part — truncated or
387
+ // errored — must still settle both promises, or the same hang
388
+ // returns by a different route.
389
+ if (!sawFinish) {
390
+ settleFinishReason("unknown");
391
+ settleUsage({ inputTokens: 0, outputTokens: 0 });
392
+ }
393
+ observeGeneration(span, {
394
+ ...call,
395
+ output: text,
396
+ usage: tokens,
397
+ finishReason: finish,
398
+ completionStartTime: firstChunkAt,
399
+ });
400
+ span.end();
401
+ }
402
+ })();
403
+ return {
404
+ stream: channel.iterable,
405
+ finishReason,
406
+ usage,
407
+ warnings: (result.warnings ?? []).map((warning) => warning.message ?? String(warning)),
408
+ };
409
+ }
410
+ catch (error) {
411
+ span.setStatus({
412
+ code: SpanStatusCode.ERROR,
413
+ message: error instanceof Error ? error.message : String(error),
414
+ });
415
+ if (error instanceof Error) {
416
+ span.recordException(error);
381
417
  }
382
- });
418
+ span.end();
419
+ throw this.handleProviderError(error);
420
+ }
383
421
  }
384
422
  formatProviderError(error) {
385
423
  if (error instanceof SageMakerError) {
@@ -1,5 +1,5 @@
1
1
  import Anthropic from "@anthropic-ai/sdk";
2
- import { SpanKind, trace } from "@opentelemetry/api";
2
+ import { trace } from "@opentelemetry/api";
3
3
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "fs";
4
4
  import { homedir } from "os";
5
5
  import { join } from "path";
@@ -21,7 +21,6 @@ import { getAvailableInputTokens } from "../../constants/contextWindows.js";
21
21
  import { estimateTokens, serializeForEstimate, } from "../../utils/tokenEstimation.js";
22
22
  import { redactUrlCredentials } from "../../utils/logSanitize.js";
23
23
  import { ANTHROPIC_MAX_CACHE_BREAKPOINTS, applyAnthropicHistoryCacheBreakpoints, countAnthropicCacheMarkers, } from "../../utils/anthropicCacheBreakpoints.js";
24
- import { calculateCost } from "../../utils/pricing.js";
25
24
  import { stringifyAnthropicToolOutput } from "./toolOutput.js";
26
25
  import { createAnthropicLoopAdapter } from "./loopAdapter.js";
27
26
  import { DEFAULT_BEFORE_STEP_TIMEOUT_MS } from "../../utils/parameterValidation.js";
@@ -82,7 +81,6 @@ function normalizeAnthropicBaseURL(raw) {
82
81
  }
83
82
  return stripped;
84
83
  }
85
- const streamTracer = trace.getTracer("neurolink.provider.anthropic");
86
84
  /**
87
85
  * Get OAuth token from stored credentials file or environment.
88
86
  * Priority:
@@ -1587,6 +1585,7 @@ export class AnthropicProvider extends BaseProvider {
1587
1585
  const toolExecutionSummaries = [];
1588
1586
  const loop = await runNativeGenerateLoop({
1589
1587
  doGenerate,
1588
+ provider: this.providerName,
1590
1589
  ...createNativeGenerateGuard({
1591
1590
  provider: "anthropic",
1592
1591
  availableInputTokens: getAvailableInputTokens("anthropic", modelId, options.maxTokens ?? undefined),
@@ -1810,53 +1809,10 @@ export class AnthropicProvider extends BaseProvider {
1810
1809
  : undefined;
1811
1810
  // See the generate path: `disabled` is sent, but only ON constrains sampling.
1812
1811
  const thinkingOn = thinking?.type === "enabled";
1813
- // Wrap the native stream in an OTel span to capture provider-level
1814
- // latency and token usage (same span name as the pre-migration path so
1815
- // dashboards stay continuous).
1816
- const streamSpan = streamTracer.startSpan("neurolink.provider.streamText", {
1817
- kind: SpanKind.CLIENT,
1818
- attributes: {
1819
- "gen_ai.system": "anthropic",
1820
- "gen_ai.request.model": modelId,
1821
- },
1822
- });
1823
1812
  const maxSteps = options.maxSteps || DEFAULT_MAX_STEPS;
1824
1813
  const channel = createStreamChannel();
1825
1814
  const { push: pushChunk } = channel;
1826
1815
  const { usagePromise, finishPromise, resolveUsage, resolveFinish } = createDeferredAnalytics();
1827
- usagePromise
1828
- .then((usage) => {
1829
- streamSpan.setAttribute("gen_ai.usage.input_tokens", usage.promptTokens || 0);
1830
- streamSpan.setAttribute("gen_ai.usage.output_tokens", usage.completionTokens || 0);
1831
- if (usage.cacheReadTokens) {
1832
- streamSpan.setAttribute("gen_ai.usage.cached_input_tokens", usage.cacheReadTokens);
1833
- }
1834
- const cost = calculateCost(this.providerName, this.modelName, {
1835
- input: usage.promptTokens || 0,
1836
- output: usage.completionTokens || 0,
1837
- total: usage.totalTokens || 0,
1838
- ...(usage.cacheReadTokens
1839
- ? { cacheReadTokens: usage.cacheReadTokens }
1840
- : {}),
1841
- ...(usage.cacheCreationTokens
1842
- ? { cacheCreationTokens: usage.cacheCreationTokens }
1843
- : {}),
1844
- });
1845
- if (cost && cost > 0) {
1846
- streamSpan.setAttribute("neurolink.cost", cost);
1847
- }
1848
- })
1849
- .catch(() => {
1850
- // usage may never resolve if the stream is aborted before completion
1851
- });
1852
- finishPromise
1853
- .then((reason) => {
1854
- streamSpan.setAttribute("gen_ai.response.finish_reason", reason || "unknown");
1855
- streamSpan.end();
1856
- })
1857
- .catch(() => {
1858
- streamSpan.end();
1859
- });
1860
1816
  let capturedProviderError;
1861
1817
  const client = this.client;
1862
1818
  // Every attempted call, for the no-output sentinel below; the result
@@ -2068,6 +2024,7 @@ export class AnthropicProvider extends BaseProvider {
2068
2024
  };
2069
2025
  const baseAdapter = createAnthropicLoopAdapter({
2070
2026
  client,
2027
+ provider: this.providerName,
2071
2028
  maxSteps,
2072
2029
  toolsRecord,
2073
2030
  buildParams,
@@ -2204,12 +2161,6 @@ export class AnthropicProvider extends BaseProvider {
2204
2161
  },
2205
2162
  };
2206
2163
  }
2207
- // The active span goes with it. Before this loop moved onto the shared
2208
- // engine it called
2209
- // withProviderRetry(fn, trace.getActiveSpan() ?? undefined, label)
2210
- // and that span is where gen_ai.provider.total_attempts is recorded.
2211
- // The engine passed `undefined` in its place, so the attribute silently
2212
- // stopped being emitted for every native Anthropic turn.
2213
2164
  const activeSpan = trace.getActiveSpan();
2214
2165
  // The caller's step-boundary callback, made finite and cancellable
2215
2166
  // before the engine ever sees it. The engine's contract is "already
@@ -25,6 +25,7 @@ import { resolveDeferredTool } from "../../tools/toolDiscovery.js";
25
25
  import { stringifyAnthropicToolOutput } from "./toolOutput.js";
26
26
  import { stringifyFinalResultInput } from "./structuredOutput.js";
27
27
  import { composeAbortSignalsScoped, createTimeoutController, } from "../../utils/timeout.js";
28
+ import { sanitizeAnthropicMessagesForTrace } from "../../utils/anthropicTraceSanitizer.js";
28
29
  import { NeuroLinkError } from "../../utils/errorHandling.js";
29
30
  import { ErrorCategory, ErrorSeverity } from "../../constants/enums.js";
30
31
  /** Map Anthropic's stop_reason onto the unified finish reason. */
@@ -86,6 +87,7 @@ async function readAnthropicStep({ params, client, channel, stepSignal, requestT
86
87
  // absence is the only signal that the response ended early, because the
87
88
  // content blocks that DID arrive are syntactically complete.
88
89
  let sawTerminalEvent = false;
90
+ let responseModel;
89
91
  for await (const rawEvent of events) {
90
92
  if (stepSignal.aborted) {
91
93
  break;
@@ -96,6 +98,7 @@ async function readAnthropicStep({ params, client, channel, stepSignal, requestT
96
98
  // asserted.
97
99
  const event = rawEvent;
98
100
  if (event.type === "message_start") {
101
+ responseModel = event.message?.model ?? responseModel;
99
102
  const usage = event.message?.usage;
100
103
  inputTokens += usage?.input_tokens ?? 0;
101
104
  const startOutput = usage?.output_tokens ?? 0;
@@ -342,6 +345,7 @@ async function readAnthropicStep({ params, client, channel, stepSignal, requestT
342
345
  ...(reasoningTokens ? { reasoningTokens } : {}),
343
346
  },
344
347
  rawStopReason,
348
+ ...(responseModel ? { model: responseModel } : {}),
345
349
  raw: blocks,
346
350
  };
347
351
  }
@@ -383,7 +387,26 @@ export function createAnthropicLoopAdapter(config) {
383
387
  };
384
388
  },
385
389
  async buildStepRequest(conversation, step) {
386
- return { raw: await config.buildParams(conversation, step) };
390
+ const params = await config.buildParams(conversation, step);
391
+ return {
392
+ raw: params,
393
+ generation: {
394
+ provider: config.provider,
395
+ model: params.model,
396
+ modelParameters: {
397
+ temperature: params.temperature,
398
+ maxTokens: params.max_tokens,
399
+ topP: params.top_p,
400
+ thinkingBudget: params.thinking?.type === "enabled"
401
+ ? params.thinking.budget_tokens
402
+ : undefined,
403
+ },
404
+ input: {
405
+ system: params.system,
406
+ messages: sanitizeAnthropicMessagesForTrace(params.messages),
407
+ },
408
+ },
409
+ };
387
410
  },
388
411
  async executeStep(request, channel, signal) {
389
412
  const params = request.raw;
@@ -268,18 +268,15 @@ export function setLimitSpanAttributes(snapshot) {
268
268
  }
269
269
  const { rateLimit } = snapshot;
270
270
  const attrs = [
271
- ["neurolink.claude.quota.session_left_pct", rateLimit.sessionLeftPct],
272
- ["neurolink.claude.quota.weekly_left_pct", rateLimit.weeklyLeftPct],
273
- ["neurolink.claude.quota.requests_remaining", rateLimit.requestsRemaining],
274
- ["neurolink.claude.quota.tokens_remaining", rateLimit.tokensRemaining],
275
- ["neurolink.claude.quota.source", snapshot.quotaSource],
276
- ["neurolink.claude.account", snapshot.account],
277
- ["neurolink.claude.served_by", snapshot.servedBy],
278
- ["neurolink.claude.pool.available", snapshot.pool?.available],
279
- [
280
- "neurolink.claude.pool.best_session_left_pct",
281
- snapshot.pool?.bestSessionLeftPct,
282
- ],
271
+ ["claude.quota.session_left_pct", rateLimit.sessionLeftPct],
272
+ ["claude.quota.weekly_left_pct", rateLimit.weeklyLeftPct],
273
+ ["claude.quota.requests_remaining", rateLimit.requestsRemaining],
274
+ ["claude.quota.tokens_remaining", rateLimit.tokensRemaining],
275
+ ["claude.quota.source", snapshot.quotaSource],
276
+ ["claude.account", snapshot.account],
277
+ ["claude.served_by", snapshot.servedBy],
278
+ ["claude.pool.available", snapshot.pool?.available],
279
+ ["claude.pool.best_session_left_pct", snapshot.pool?.bestSessionLeftPct],
283
280
  ];
284
281
  for (const [key, value] of attrs) {
285
282
  if (value !== undefined) {
@@ -23,6 +23,7 @@ export declare class GoogleAIStudioProvider extends BaseProvider {
23
23
  * @returns A promise that resolves to the generation result, including the image data.
24
24
  */
25
25
  protected executeImageGeneration(options: TextGenerationOptions): Promise<EnhancedGenerateResult>;
26
+ private generateImage;
26
27
  /**
27
28
  * Detect image MIME type from buffer
28
29
  */
@@ -64,12 +65,10 @@ export declare class GoogleAIStudioProvider extends BaseProvider {
64
65
  */
65
66
  generate(optionsOrPrompt: TextGenerationOptions | string): Promise<EnhancedGenerateResult | null>;
66
67
  /**
67
- * Emit `generation:end` so the Pipeline B observability listener creates
68
- * a `model.generation` span for native Google AI Studio generate calls.
69
- * Without this hand-off the native path silently disappears from
70
- * Pipeline B exporters (Langfuse, custom OTEL collectors).
68
+ * Emit `generation:end` for native Google AI Studio generate calls, so
69
+ * analytics listeners see them.
71
70
  */
72
- private emitPipelineBGenerationEvent;
71
+ private emitGenerationEnd;
73
72
  private executeAudioStreamViaGeminiLive;
74
73
  protected getDefaultEmbeddingModel(): string;
75
74
  /**
@@ -8,6 +8,8 @@ import { ERROR_CODES, NeuroLinkError } from "../../utils/errorHandling.js";
8
8
  import { logger } from "../../utils/logger.js";
9
9
  import { drainDetachedPump } from "../../utils/drainDetachedPump.js";
10
10
  import { attachStreamCancel, cancelStream, releaseIterator, } from "../../utils/streamCancellation.js";
11
+ import { observeGeneration } from "../../telemetry/observation.js";
12
+ import { withGeneration } from "../../telemetry/withSpan.js";
11
13
  import { createGeminiLoopAdapter } from "../../core/geminiLoopAdapter.js";
12
14
  import { isDirectTTSRequest } from "../../core/resolveRequestKind.js";
13
15
  import { runAgenticLoop } from "../../core/loopEngine.js";
@@ -360,6 +362,13 @@ export class GoogleAIStudioProvider extends BaseProvider {
360
362
  * @returns A promise that resolves to the generation result, including the image data.
361
363
  */
362
364
  async executeImageGeneration(options) {
365
+ return withGeneration({
366
+ provider: this.providerName,
367
+ model: options.model || this.modelName,
368
+ input: { prompt: options.prompt || options.input?.text || "" },
369
+ }, (span) => this.generateImage(options, span));
370
+ }
371
+ async generateImage(options, span) {
363
372
  // Image-to-image generation takes reference images through the same
364
373
  // `input.images` array, and generate() routes here BEFORE reaching its own
365
374
  // normalization call — so a reference photo in HEIC/BMP/AVIF would arrive
@@ -518,6 +527,12 @@ export class GoogleAIStudioProvider extends BaseProvider {
518
527
  total: this.estimateTokenCount(prompt),
519
528
  },
520
529
  };
530
+ observeGeneration(span, {
531
+ responseModel: imageModelName,
532
+ output: result.content,
533
+ usage: result.usage,
534
+ finishReason: "stop",
535
+ });
521
536
  return await this.enhanceResult(result, options, startTime);
522
537
  }
523
538
  // Check for text content
@@ -577,6 +592,12 @@ export class GoogleAIStudioProvider extends BaseProvider {
577
592
  total: this.estimateTokenCount(prompt),
578
593
  },
579
594
  };
595
+ observeGeneration(span, {
596
+ responseModel: imageModelName,
597
+ output: result.content,
598
+ usage: result.usage,
599
+ finishReason: "stop",
600
+ });
580
601
  return await this.enhanceResult(result, options, startTime);
581
602
  }
582
603
  if ("text" in part && part.text) {
@@ -747,12 +768,11 @@ export class GoogleAIStudioProvider extends BaseProvider {
747
768
  async executeNativeGemini3Stream(options) {
748
769
  const modelName = options.model || this.modelName;
749
770
  return withClientStreamSpan({
750
- name: "neurolink.provider.stream",
771
+ name: "provider.stream",
751
772
  tracer: tracers.provider,
773
+ detached: true,
752
774
  attributes: {
753
- [ATTR.GEN_AI_SYSTEM]: "google-ai",
754
- [ATTR.GEN_AI_MODEL]: modelName,
755
- [ATTR.GEN_AI_OPERATION]: "stream",
775
+ [ATTR.NL_MODEL]: modelName,
756
776
  [ATTR.NL_PROVIDER]: this.providerName,
757
777
  },
758
778
  }, async (span) => {
@@ -927,6 +947,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
927
947
  // performed inline.
928
948
  const baseAdapter = createGeminiLoopAdapter({
929
949
  providerLabel: "GoogleAIStudio",
950
+ provider: this.providerName,
930
951
  maxSteps,
931
952
  // Same threshold the hand-rolled dispatcher used, which is
932
953
  // what makes an always-failing tool dispatch exactly twice.
@@ -1210,10 +1231,6 @@ export class GoogleAIStudioProvider extends BaseProvider {
1210
1231
  if (lastFinishReason !== undefined) {
1211
1232
  metadata.rawFinishReason = lastFinishReason;
1212
1233
  }
1213
- // Set token usage and finish reason on the span
1214
- span.setAttribute(ATTR.GEN_AI_INPUT_TOKENS, totalInputTokens);
1215
- span.setAttribute(ATTR.GEN_AI_OUTPUT_TOKENS, totalOutputTokens);
1216
- span.setAttribute(ATTR.GEN_AI_FINISH_REASON, hitStepLimitWithoutFinalAnswer ? "max_steps" : "stop");
1217
1234
  // Gemini promptTokenCount is OVERLAPPING: it already includes
1218
1235
  // cachedContentTokenCount. Subtract once here so calculateCost
1219
1236
  // bills the cached portion at the cheaper cacheRead rate without
@@ -1454,12 +1471,11 @@ export class GoogleAIStudioProvider extends BaseProvider {
1454
1471
  async executeNativeGemini3Generate(options) {
1455
1472
  const modelName = options.model || this.modelName;
1456
1473
  return withClientSpan({
1457
- name: "neurolink.provider.generate",
1474
+ name: "provider.generate",
1458
1475
  tracer: tracers.provider,
1476
+ detached: true,
1459
1477
  attributes: {
1460
- [ATTR.GEN_AI_SYSTEM]: "google-ai",
1461
- [ATTR.GEN_AI_MODEL]: modelName,
1462
- [ATTR.GEN_AI_OPERATION]: "generate",
1478
+ [ATTR.NL_MODEL]: modelName,
1463
1479
  [ATTR.NL_PROVIDER]: this.providerName,
1464
1480
  },
1465
1481
  }, async (span) => {
@@ -1614,6 +1630,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
1614
1630
  // the turn's text comes from the result.
1615
1631
  const baseAdapter = createGeminiLoopAdapter({
1616
1632
  providerLabel: "GoogleAIStudio",
1633
+ provider: this.providerName,
1617
1634
  maxSteps,
1618
1635
  toolFailureBreaker: { maxRetries: DEFAULT_TOOL_MAX_RETRIES },
1619
1636
  liveTools: options.tools ?? {},
@@ -1819,10 +1836,6 @@ export class GoogleAIStudioProvider extends BaseProvider {
1819
1836
  elapsedMs: turnClock.elapsedMs(),
1820
1837
  });
1821
1838
  }
1822
- // Set token usage and finish reason on the span
1823
- span.setAttribute(ATTR.GEN_AI_INPUT_TOKENS, totalInputTokens);
1824
- span.setAttribute(ATTR.GEN_AI_OUTPUT_TOKENS, totalOutputTokens);
1825
- span.setAttribute(ATTR.GEN_AI_FINISH_REASON, step >= maxSteps ? "max_steps" : "stop");
1826
1839
  // Build EnhancedGenerateResult and route through enhanceResult so
1827
1840
  // analytics / evaluation / tracing stay attached. The native AI
1828
1841
  // Studio generate path bypasses BaseProvider.generate(), so
@@ -2082,10 +2095,6 @@ export class GoogleAIStudioProvider extends BaseProvider {
2082
2095
  });
2083
2096
  }
2084
2097
  // Route ALL models through native @google/genai SDK (no more @ai-sdk/google dependency).
2085
- // Emit Pipeline B `generation:end` so the observability listener
2086
- // creates a `model.generation` span — AI Studio's native path bypasses
2087
- // the AI SDK + experimental_telemetry plumbing the same way Vertex's
2088
- // does, so the event has to be emitted manually.
2089
2098
  const generateStartTime = Date.now();
2090
2099
  const inputPrompt = mergedOptions.input?.text ||
2091
2100
  mergedOptions.prompt ||
@@ -2096,32 +2105,30 @@ export class GoogleAIStudioProvider extends BaseProvider {
2096
2105
  // native @google/genai path. This provider overrides generate() instead
2097
2106
  // of using BaseProvider's shared flow, so the span is added here.
2098
2107
  let result = await withSpan({
2099
- name: "neurolink.executeGeneration",
2108
+ name: "executeGeneration",
2100
2109
  tracer: tracers.provider,
2110
+ detached: true,
2101
2111
  attributes: {
2102
- [ATTR.GEN_AI_SYSTEM]: this.providerName,
2103
- [ATTR.GEN_AI_MODEL]: modelName,
2104
- "neurolink.path": "native.google-genai",
2112
+ [ATTR.NL_MODEL]: modelName,
2113
+ path: "native.google-genai",
2105
2114
  },
2106
2115
  }, async () => this.executeNativeGemini3Generate(mergedOptions));
2107
2116
  // Pipe through TTS-of-AI-response when caller asks for it. No-op when
2108
2117
  // tts is disabled or useAiResponse is false.
2109
2118
  result = await this.synthesizeAIResponseIfNeeded(result, options);
2110
- this.emitPipelineBGenerationEvent(modelName, result, generateStartTime, true, undefined, inputPrompt);
2119
+ this.emitGenerationEnd(modelName, result, generateStartTime, true, undefined, inputPrompt);
2111
2120
  return result;
2112
2121
  }
2113
2122
  catch (error) {
2114
- this.emitPipelineBGenerationEvent(modelName, null, generateStartTime, false, error, inputPrompt);
2123
+ this.emitGenerationEnd(modelName, null, generateStartTime, false, error, inputPrompt);
2115
2124
  throw error;
2116
2125
  }
2117
2126
  }
2118
2127
  /**
2119
- * Emit `generation:end` so the Pipeline B observability listener creates
2120
- * a `model.generation` span for native Google AI Studio generate calls.
2121
- * Without this hand-off the native path silently disappears from
2122
- * Pipeline B exporters (Langfuse, custom OTEL collectors).
2128
+ * Emit `generation:end` for native Google AI Studio generate calls, so
2129
+ * analytics listeners see them.
2123
2130
  */
2124
- emitPipelineBGenerationEvent(modelName, result, startTime, success, error, prompt) {
2131
+ emitGenerationEnd(modelName, result, startTime, success, error, prompt) {
2125
2132
  const emitter = this.neurolink?.getEventEmitter();
2126
2133
  if (!emitter) {
2127
2134
  return;
@@ -2137,12 +2144,6 @@ export class GoogleAIStudioProvider extends BaseProvider {
2137
2144
  result._generationEndEmitted =
2138
2145
  true;
2139
2146
  }
2140
- // The failure path has no result to carry that flag — `result` is null —
2141
- // so mark the error instead. Without this the provider's own
2142
- // `generation:end` (success: false) and the SDK's emitGenerateErrorEvent
2143
- // both fire for one failed native generate, double-counting every failure
2144
- // in analytics and as two Pipeline B spans. The success path above was
2145
- // already deduplicated; only the catch branch was missed.
2146
2147
  if (!success && error && typeof error === "object") {
2147
2148
  error._generationEndEmitted =
2148
2149
  true;
@@ -2151,9 +2152,6 @@ export class GoogleAIStudioProvider extends BaseProvider {
2151
2152
  provider: this.providerName,
2152
2153
  responseTime: Date.now() - startTime,
2153
2154
  timestamp: Date.now(),
2154
- // The Pipeline B listener reads `data.prompt` to populate the
2155
- // `input` span attribute. Without this, the Observability Spans
2156
- // test fails with "input capture not working".
2157
2155
  prompt: prompt || "",
2158
2156
  result: {
2159
2157
  content: result?.content || "",