@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.
- package/CHANGELOG.md +14 -3
- package/dist/auth/anthropicOAuth.js +3 -3
- package/dist/auth/middleware/AuthMiddleware.d.ts +1 -1
- package/dist/auth/sessionManager.js +4 -4
- package/dist/auth/tokenStore.js +4 -4
- package/dist/browser/neurolink.min.js +474 -473
- package/dist/cli/commands/observability.d.ts +1 -6
- package/dist/cli/commands/observability.js +0 -106
- package/dist/cli/commands/proxy.js +9 -18
- package/dist/cli/commands/telemetry.d.ts +3 -13
- package/dist/cli/commands/telemetry.js +7 -354
- package/dist/cli/loop/session.js +1 -1
- package/dist/client/httpClient.js +1 -1
- package/dist/client/sseClient.js +1 -1
- package/dist/client/streamingClient.js +2 -2
- package/dist/client/wsClient.js +1 -1
- package/dist/constants/langfuse.d.ts +25 -0
- package/dist/constants/langfuse.js +38 -0
- package/dist/constants/telemetry.d.ts +20 -0
- package/dist/constants/telemetry.js +34 -0
- package/dist/core/baseProvider.d.ts +2 -1
- package/dist/core/baseProvider.js +4 -11
- package/dist/core/conversationMemoryManager.js +5 -8
- package/dist/core/factory.js +1 -1
- package/dist/core/geminiLoopAdapter.d.ts +8 -1
- package/dist/core/geminiLoopAdapter.js +25 -1
- package/dist/core/loopEngine.js +69 -40
- package/dist/core/modules/MessageBuilder.js +2 -2
- package/dist/core/modules/StreamHandler.js +2 -2
- package/dist/core/modules/TelemetryHandler.d.ts +2 -18
- package/dist/core/modules/TelemetryHandler.js +1 -66
- package/dist/core/modules/ToolsManager.js +40 -31
- package/dist/core/nativeGenerateLoop.js +78 -8
- package/dist/core/redisConversationMemoryManager.js +8 -15
- package/dist/core/toolExecutionGuards.js +1 -13
- package/dist/index.d.ts +5 -12
- package/dist/index.js +5 -37
- package/dist/mcp/batching/requestBatcher.js +1 -1
- package/dist/mcp/externalServerManager.js +3 -21
- package/dist/mcp/toolDiscoveryService.js +4 -16
- package/dist/mcp/toolRegistry.js +2 -5
- package/dist/memory/memoryRetrievalTools.js +1 -1
- package/dist/neurolink.d.ts +16 -27
- package/dist/neurolink.js +788 -792
- package/dist/observability/exporterSpanProcessor.d.ts +50 -0
- package/dist/observability/exporterSpanProcessor.js +181 -0
- package/dist/observability/exporters/langfuseExporter.d.ts +13 -34
- package/dist/observability/exporters/langfuseExporter.js +101 -154
- package/dist/observability/index.d.ts +1 -0
- package/dist/observability/index.js +1 -0
- package/dist/observability/langfuseMasking.d.ts +8 -0
- package/dist/observability/langfuseMasking.js +47 -0
- package/dist/observability/otlpEncoding.d.ts +20 -0
- package/dist/observability/otlpEncoding.js +85 -0
- package/dist/providers/amazonBedrock/client.js +142 -176
- package/dist/providers/amazonBedrock/loopAdapter.js +14 -1
- package/dist/providers/amazonSagemaker.js +118 -80
- package/dist/providers/anthropic/client.js +3 -52
- package/dist/providers/anthropic/loopAdapter.js +24 -1
- package/dist/providers/anthropic/rateLimitCapture.js +9 -12
- package/dist/providers/googleAiStudio/client.d.ts +4 -5
- package/dist/providers/googleAiStudio/client.js +38 -40
- package/dist/providers/googleVertex/client.d.ts +2 -7
- package/dist/providers/googleVertex/client.js +282 -378
- package/dist/providers/googleVertex/constants.js +2 -2
- package/dist/providers/litellm/client.d.ts +2 -9
- package/dist/providers/litellm/client.js +1 -63
- package/dist/providers/openAI/client.d.ts +1 -8
- package/dist/providers/openAI/client.js +0 -65
- package/dist/providers/openaiChatCompletionsBase.d.ts +1 -0
- package/dist/providers/openaiChatCompletionsBase.js +61 -4
- package/dist/providers/replicate.js +22 -4
- package/dist/proxy/codexUsage.js +7 -0
- package/dist/proxy/proxyFetch.js +19 -25
- package/dist/proxy/proxyTracer.d.ts +14 -7
- package/dist/proxy/proxyTracer.js +129 -83
- package/dist/proxy/proxyTranslationEngine.js +0 -1
- package/dist/proxy/requestLogger.d.ts +1 -1
- package/dist/proxy/requestLogger.js +39 -45
- package/dist/server/middleware/common.js +1 -1
- package/dist/server/routes/agentRoutes.js +4 -4
- package/dist/server/routes/claudeProxyRoutes.js +99 -196
- package/dist/server/routes/codexProxyRoutes.js +621 -591
- package/dist/server/routes/healthRoutes.js +6 -6
- package/dist/server/routes/mcpRoutes.js +7 -7
- package/dist/server/routes/memoryRoutes.js +7 -7
- package/dist/server/routes/openApiRoutes.js +3 -3
- package/dist/server/routes/toolRoutes.js +5 -5
- package/dist/server/routes/transcribeRoutes.js +1 -1
- package/dist/server/utils/validation.d.ts +3 -3
- package/dist/services/server/ai/observability/instrumentation.d.ts +34 -201
- package/dist/services/server/ai/observability/instrumentation.js +220 -1267
- package/dist/services/server/ai/observability/langfuseSetup.d.ts +14 -0
- package/dist/services/server/ai/observability/langfuseSetup.js +55 -0
- package/dist/services/server/ai/observability/otlpSetup.d.ts +16 -0
- package/dist/services/server/ai/observability/otlpSetup.js +58 -0
- package/dist/session/globalSessionState.js +0 -11
- package/dist/telemetry/attributes.d.ts +23 -46
- package/dist/telemetry/attributes.js +76 -162
- package/dist/telemetry/chatMessages.d.ts +13 -0
- package/dist/telemetry/chatMessages.js +59 -0
- package/dist/telemetry/index.d.ts +5 -19
- package/dist/telemetry/index.js +5 -26
- package/dist/telemetry/observation.d.ts +37 -0
- package/dist/telemetry/observation.js +112 -0
- package/dist/telemetry/observationNames.d.ts +18 -0
- package/dist/telemetry/observationNames.js +24 -0
- package/dist/telemetry/spanSanitizer.d.ts +39 -0
- package/dist/telemetry/spanSanitizer.js +92 -0
- package/dist/telemetry/spanSerialization.d.ts +13 -0
- package/dist/telemetry/spanSerialization.js +46 -0
- package/dist/telemetry/tracers.d.ts +37 -18
- package/dist/telemetry/tracers.js +97 -19
- package/dist/telemetry/withSpan.d.ts +47 -25
- package/dist/telemetry/withSpan.js +158 -71
- package/dist/types/cli.d.ts +0 -11
- package/dist/types/decision.d.ts +3 -0
- package/dist/types/exporter.d.ts +54 -0
- package/dist/types/exporter.js +0 -4
- package/dist/types/generate.d.ts +4 -1
- package/dist/types/isolatedAgent.d.ts +5 -4
- package/dist/types/loopEngine.d.ts +18 -28
- package/dist/types/observability.d.ts +127 -160
- package/dist/types/providers.d.ts +15 -0
- package/dist/types/proxy.d.ts +41 -2
- package/dist/utils/anthropicTraceSanitizer.d.ts +2 -2
- package/dist/utils/anthropicTraceSanitizer.js +3 -1
- package/dist/utils/conversationMemory.js +4 -4
- package/dist/utils/fileDetector.js +1 -1
- package/dist/utils/messageBuilder.js +1 -1
- package/dist/utils/noOutputSentinel.d.ts +4 -16
- package/dist/utils/noOutputSentinel.js +12 -28
- package/dist/utils/providerRetry.js +4 -4
- package/dist/voice/livekit/brain.js +1 -1
- package/dist/voice/livekit/realtimeMemory.d.ts +1 -1
- package/dist/voice/livekit/realtimeMemory.js +66 -1
- package/dist/voice/livekit/realtimeVoiceAgent.js +1 -1
- package/docs-site/static/search-index.json +42 -63
- package/package.json +10 -4
- package/dist/telemetry/telemetryService.d.ts +0 -79
- package/dist/telemetry/telemetryService.js +0 -572
- package/dist/utils/observabilityHelpers.d.ts +0 -27
- 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 {
|
|
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
|
-
|
|
296
|
-
|
|
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
|
-
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
["
|
|
272
|
-
["
|
|
273
|
-
["
|
|
274
|
-
["
|
|
275
|
-
["
|
|
276
|
-
["
|
|
277
|
-
["
|
|
278
|
-
["
|
|
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`
|
|
68
|
-
*
|
|
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
|
|
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: "
|
|
771
|
+
name: "provider.stream",
|
|
751
772
|
tracer: tracers.provider,
|
|
773
|
+
detached: true,
|
|
752
774
|
attributes: {
|
|
753
|
-
[ATTR.
|
|
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: "
|
|
1474
|
+
name: "provider.generate",
|
|
1458
1475
|
tracer: tracers.provider,
|
|
1476
|
+
detached: true,
|
|
1459
1477
|
attributes: {
|
|
1460
|
-
[ATTR.
|
|
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: "
|
|
2108
|
+
name: "executeGeneration",
|
|
2100
2109
|
tracer: tracers.provider,
|
|
2110
|
+
detached: true,
|
|
2101
2111
|
attributes: {
|
|
2102
|
-
[ATTR.
|
|
2103
|
-
|
|
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.
|
|
2119
|
+
this.emitGenerationEnd(modelName, result, generateStartTime, true, undefined, inputPrompt);
|
|
2111
2120
|
return result;
|
|
2112
2121
|
}
|
|
2113
2122
|
catch (error) {
|
|
2114
|
-
this.
|
|
2123
|
+
this.emitGenerationEnd(modelName, null, generateStartTime, false, error, inputPrompt);
|
|
2115
2124
|
throw error;
|
|
2116
2125
|
}
|
|
2117
2126
|
}
|
|
2118
2127
|
/**
|
|
2119
|
-
* Emit `generation:end`
|
|
2120
|
-
*
|
|
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
|
-
|
|
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 || "",
|