@juspay/neurolink 13.5.0 → 13.5.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.
- package/CHANGELOG.md +3 -3
- package/dist/adapters/video/videoAnalyzer.d.ts +1 -0
- package/dist/adapters/video/videoAnalyzer.js +4 -0
- package/dist/autoresearch/index.d.ts +1 -0
- package/dist/autoresearch/index.js +4 -0
- package/dist/autoresearch/worker.js +4 -0
- package/dist/browser/neurolink.min.js +467 -467
- package/dist/cli/commands/observability.js +11 -4
- package/dist/cli/commands/telemetry.js +36 -6
- package/dist/cli/utils/formatters.d.ts +17 -0
- package/dist/cli/utils/formatters.js +39 -0
- package/dist/constants/langfuse.d.ts +17 -1
- package/dist/constants/langfuse.js +23 -1
- package/dist/constants/telemetry.d.ts +6 -0
- package/dist/constants/telemetry.js +6 -0
- package/dist/core/baseProvider.js +2 -0
- package/dist/core/geminiLoopAdapter.js +19 -0
- package/dist/core/loopEngine.js +10 -7
- package/dist/core/modules/StreamHandler.js +5 -4
- package/dist/core/modules/TelemetryHandler.d.ts +5 -1
- package/dist/core/modules/TelemetryHandler.js +16 -0
- package/dist/core/modules/ToolsManager.js +3 -0
- package/dist/core/nativeGenerateLoop.js +1 -1
- package/dist/evaluation/index.d.ts +1 -0
- package/dist/evaluation/index.js +4 -0
- package/dist/evaluation/ragasEvaluator.js +1 -0
- package/dist/mcp/externalServerManager.js +6 -2
- package/dist/mcp/toolRegistry.js +3 -0
- package/dist/neurolink.d.ts +16 -8
- package/dist/neurolink.js +115 -61
- package/dist/observability/FEATURE-STATUS.md +0 -4
- package/dist/observability/exporterRegistry.d.ts +6 -1
- package/dist/observability/exporterRegistry.js +18 -0
- package/dist/observability/exporterSpanProcessor.d.ts +20 -6
- package/dist/observability/exporterSpanProcessor.js +166 -34
- package/dist/observability/exporters/baseExporter.d.ts +22 -3
- package/dist/observability/exporters/baseExporter.js +87 -5
- package/dist/observability/exporters/langfuseExporter.d.ts +29 -3
- package/dist/observability/exporters/langfuseExporter.js +251 -41
- package/dist/observability/langfuseMasking.d.ts +1 -1
- package/dist/observability/langfuseMasking.js +1 -1
- package/dist/observability/otlpEncoding.d.ts +18 -5
- package/dist/observability/otlpEncoding.js +69 -12
- package/dist/observability/utils/spanSerializer.d.ts +3 -2
- package/dist/observability/utils/spanSerializer.js +5 -3
- package/dist/processors/base/BaseFileProcessor.d.ts +1 -0
- package/dist/processors/base/BaseFileProcessor.js +4 -0
- package/dist/processors/index.d.ts +1 -0
- package/dist/processors/index.js +4 -0
- package/dist/processors/registry/ProcessorRegistry.d.ts +1 -0
- package/dist/processors/registry/ProcessorRegistry.js +5 -0
- package/dist/providers/amazonBedrock/client.js +7 -5
- package/dist/providers/amazonSagemaker.js +32 -12
- package/dist/providers/anthropic/client.js +1 -0
- package/dist/providers/anthropic/loopAdapter.js +7 -3
- package/dist/providers/googleAiStudio/client.js +2 -0
- package/dist/providers/googleVertex/client.d.ts +6 -5
- package/dist/providers/googleVertex/client.js +46 -19
- package/dist/providers/openaiChatCompletionsBase.js +4 -4
- package/dist/proxy/codexUsage.js +59 -2
- package/dist/proxy/proxyTracer.d.ts +25 -1
- package/dist/proxy/proxyTracer.js +146 -22
- package/dist/proxy/proxyTranslationEngine.js +4 -2
- package/dist/proxy/sseInterceptor.js +6 -4
- package/dist/rag/chunkers/BaseChunker.js +1 -0
- package/dist/rag/index.d.ts +1 -0
- package/dist/rag/index.js +4 -0
- package/dist/rag/ragIntegration.js +1 -0
- package/dist/rag/reranker/reranker.js +2 -0
- package/dist/rag/retrieval/vectorQueryTool.js +1 -0
- package/dist/server/routes/codexProxyRoutes.js +3 -1
- package/dist/server/utils/validation.d.ts +3 -3
- package/dist/services/server/ai/observability/instrumentation.d.ts +8 -7
- package/dist/services/server/ai/observability/instrumentation.js +217 -47
- package/dist/services/server/ai/observability/langfuseSetup.d.ts +19 -1
- package/dist/services/server/ai/observability/langfuseSetup.js +62 -4
- package/dist/services/server/ai/observability/otlpSetup.d.ts +12 -2
- package/dist/services/server/ai/observability/otlpSetup.js +55 -4
- package/dist/telemetry/aiMetrics.d.ts +56 -0
- package/dist/telemetry/aiMetrics.js +229 -0
- package/dist/telemetry/attributes.d.ts +24 -1
- package/dist/telemetry/attributes.js +94 -9
- package/dist/telemetry/chatMessages.d.ts +6 -0
- package/dist/telemetry/chatMessages.js +37 -0
- package/dist/telemetry/index.d.ts +1 -1
- package/dist/telemetry/index.js +1 -1
- package/dist/telemetry/observation.d.ts +5 -5
- package/dist/telemetry/observation.js +128 -39
- package/dist/telemetry/spanSanitizer.d.ts +2 -2
- package/dist/telemetry/spanSanitizer.js +76 -8
- package/dist/telemetry/telemetryStarter.d.ts +16 -0
- package/dist/telemetry/telemetryStarter.js +28 -0
- package/dist/telemetry/traceContext.d.ts +3 -3
- package/dist/telemetry/traceContext.js +3 -3
- package/dist/telemetry/tracers.d.ts +13 -1
- package/dist/telemetry/tracers.js +49 -10
- package/dist/telemetry/withSpan.d.ts +19 -3
- package/dist/telemetry/withSpan.js +149 -20
- package/dist/types/exporter.d.ts +98 -3
- package/dist/types/livekit.d.ts +16 -1
- package/dist/types/loopEngine.d.ts +16 -1
- package/dist/types/observability.d.ts +38 -5
- package/dist/types/proxy.d.ts +2 -0
- package/dist/types/span.d.ts +26 -0
- package/dist/utils/timeout.js +4 -2
- package/dist/voice/livekit/realtimeMcpTools.js +11 -2
- package/dist/voice/livekit/realtimeMemory.d.ts +20 -2
- package/dist/voice/livekit/realtimeMemory.js +139 -46
- package/dist/voice/livekit/realtimeVoiceAgent.js +20 -8
- package/dist/voice/livekit/turnRecorder.js +22 -9
- package/dist/workflow/core/ensembleExecutor.js +2 -0
- package/dist/workflow/core/judgeScorer.js +1 -0
- package/dist/workflow/core/responseConditioner.js +1 -0
- package/dist/workflow/core/workflowRunner.js +6 -1
- package/dist/workflow/index.d.ts +1 -0
- package/dist/workflow/index.js +4 -0
- package/docs-site/static/search-index.json +32 -27
- package/package.json +21 -2
package/dist/processors/index.js
CHANGED
|
@@ -64,6 +64,10 @@
|
|
|
64
64
|
* });
|
|
65
65
|
* ```
|
|
66
66
|
*/
|
|
67
|
+
// Imported on its own, this entry starts tracing at its first span, as the
|
|
68
|
+
// main entry does. The import sits here rather than in the shared tracers so
|
|
69
|
+
// that the client entry stays free of the OpenTelemetry Node SDK.
|
|
70
|
+
import "../services/server/ai/observability/instrumentation.js";
|
|
67
71
|
// =============================================================================
|
|
68
72
|
// BASE PROCESSOR INFRASTRUCTURE (single source of truth for ALL types)
|
|
69
73
|
// =============================================================================
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
* ```
|
|
45
45
|
*/
|
|
46
46
|
import type { FileInfo, ProcessorFileProcessingResult, ProcessedFileBase, ProcessOptions, ProcessorMatch, RegistryOptions, RegistryProcessResult, ProcessorRegistration } from "../../types/index.js";
|
|
47
|
+
import "../../services/server/ai/observability/instrumentation.js";
|
|
47
48
|
/**
|
|
48
49
|
* Central registry for file processors.
|
|
49
50
|
* Uses singleton pattern and priority-based selection.
|
|
@@ -45,6 +45,10 @@
|
|
|
45
45
|
*/
|
|
46
46
|
import { withSpan } from "../../telemetry/withSpan.js";
|
|
47
47
|
import { tracers } from "../../telemetry/tracers.js";
|
|
48
|
+
// Starts tracing at the first span, as this module always did: the shared
|
|
49
|
+
// tracers only start what a loaded entry registered, and this path can be
|
|
50
|
+
// imported without the package entry. The client entry stays free of it.
|
|
51
|
+
import "../../services/server/ai/observability/instrumentation.js";
|
|
48
52
|
// =============================================================================
|
|
49
53
|
// UTILITY FUNCTIONS
|
|
50
54
|
// =============================================================================
|
|
@@ -404,6 +408,7 @@ export class ProcessorRegistry {
|
|
|
404
408
|
return withSpan({
|
|
405
409
|
name: "neurolink.processor.processFile",
|
|
406
410
|
tracer: tracers.processor,
|
|
411
|
+
observationType: "span",
|
|
407
412
|
attributes: {
|
|
408
413
|
"processor.filename": fileInfo.name ?? "unknown",
|
|
409
414
|
"processor.mimetype": fileInfo.mimetype ?? "unknown",
|
|
@@ -279,9 +279,9 @@ export class AmazonBedrockProvider extends BaseProvider {
|
|
|
279
279
|
}
|
|
280
280
|
throw error;
|
|
281
281
|
}
|
|
282
|
-
// Emit generation:end
|
|
283
|
-
// Bedrock bypasses the Vercel AI SDK so
|
|
284
|
-
//
|
|
282
|
+
// Emit generation:end for the event listeners and the in-process metrics
|
|
283
|
+
// aggregator. Bedrock bypasses the Vercel AI SDK so nothing else emits it;
|
|
284
|
+
// its Langfuse GENERATION observations are the loop's own per-step spans.
|
|
285
285
|
const generateEmitter = this.neurolink?.getEventEmitter();
|
|
286
286
|
if (generateEmitter) {
|
|
287
287
|
generateEmitter.emit("generation:end", {
|
|
@@ -383,6 +383,7 @@ export class AmazonBedrockProvider extends BaseProvider {
|
|
|
383
383
|
}),
|
|
384
384
|
});
|
|
385
385
|
const { resultPromise } = runAgenticLoop(adapter, conversation, {
|
|
386
|
+
operation: "generate",
|
|
386
387
|
tools: this.toEngineTools(tools),
|
|
387
388
|
abortSignal: signal,
|
|
388
389
|
// `toEngineTools` adds no deadline of its own, so this is the only
|
|
@@ -1261,6 +1262,7 @@ export class AmazonBedrockProvider extends BaseProvider {
|
|
|
1261
1262
|
},
|
|
1262
1263
|
};
|
|
1263
1264
|
return runAgenticLoop(adapter, conversation, {
|
|
1265
|
+
operation: "stream",
|
|
1264
1266
|
tools: this.toEngineTools(tools),
|
|
1265
1267
|
abortSignal: signal,
|
|
1266
1268
|
// `toEngineTools` adds no deadline of its own, so this is the only
|
|
@@ -1368,8 +1370,8 @@ export class AmazonBedrockProvider extends BaseProvider {
|
|
|
1368
1370
|
// Analytics is resolved off `settled` above, so it lands whether or
|
|
1369
1371
|
// not anyone drains the stream.
|
|
1370
1372
|
// Bedrock bypasses the Vercel AI SDK, so experimental_telemetry is
|
|
1371
|
-
// never injected and generation:end is emitted by hand for
|
|
1372
|
-
//
|
|
1373
|
+
// never injected and generation:end is emitted by hand for the
|
|
1374
|
+
// event listeners and the in-process metrics aggregator.
|
|
1373
1375
|
if (streamEmitter) {
|
|
1374
1376
|
// #1741: tell the orchestration's finally block this stream's
|
|
1375
1377
|
// `generation:end` has already been emitted here, exactly as the
|
|
@@ -13,6 +13,7 @@ import { hasNativeDoGenerate, runNativeGenerateLoop, } from "../core/nativeGener
|
|
|
13
13
|
import { generationName } from "../telemetry/observationNames.js";
|
|
14
14
|
import { observeGeneration } from "../telemetry/observation.js";
|
|
15
15
|
import { tracers } from "../telemetry/tracers.js";
|
|
16
|
+
import { markFailure } from "../telemetry/withSpan.js";
|
|
16
17
|
// SageMaker-specific imports
|
|
17
18
|
import { getDefaultSageMakerEndpoint, getSageMakerConfig, getSageMakerModel, getSageMakerModelConfig, } from "./sagemaker/config.js";
|
|
18
19
|
import { handleSageMakerError, SageMakerError } from "./sagemaker/errors.js";
|
|
@@ -375,10 +376,18 @@ export class AmazonSageMakerProvider extends BaseProvider {
|
|
|
375
376
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
376
377
|
}
|
|
377
378
|
catch (error) {
|
|
378
|
-
span
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
379
|
+
markFailure(span, error);
|
|
380
|
+
// markFailure leaves an abort at WARNING; the released pump ended
|
|
381
|
+
// every failure at ERROR, an abort included.
|
|
382
|
+
try {
|
|
383
|
+
span.setStatus({
|
|
384
|
+
code: SpanStatusCode.ERROR,
|
|
385
|
+
message: error instanceof Error ? error.message : String(error),
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
// An error whose text cannot be read is already recorded above.
|
|
390
|
+
}
|
|
382
391
|
channel.error(error);
|
|
383
392
|
}
|
|
384
393
|
finally {
|
|
@@ -390,14 +399,25 @@ export class AmazonSageMakerProvider extends BaseProvider {
|
|
|
390
399
|
settleFinishReason("unknown");
|
|
391
400
|
settleUsage({ inputTokens: 0, outputTokens: 0 });
|
|
392
401
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
402
|
+
// Recording is best effort and span.end() must follow it either
|
|
403
|
+
// way: this pump is detached, so nothing else would catch a throw.
|
|
404
|
+
try {
|
|
405
|
+
observeGeneration(span, {
|
|
406
|
+
...call,
|
|
407
|
+
output: text,
|
|
408
|
+
usage: tokens,
|
|
409
|
+
finishReason: finish,
|
|
410
|
+
completionStartTime: firstChunkAt,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
logger.debug("[SageMaker] Failed to record the stream generation", {
|
|
415
|
+
error: error instanceof Error ? error.message : String(error),
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
finally {
|
|
419
|
+
span.end();
|
|
420
|
+
}
|
|
401
421
|
}
|
|
402
422
|
})();
|
|
403
423
|
return {
|
|
@@ -2208,6 +2208,7 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
2208
2208
|
}
|
|
2209
2209
|
: undefined;
|
|
2210
2210
|
const { stream, resultPromise } = runAgenticLoop(adapter, payload.messages.slice(), {
|
|
2211
|
+
operation: "stream",
|
|
2211
2212
|
tools: engineTools,
|
|
2212
2213
|
...(abortSignal ? { abortSignal } : {}),
|
|
2213
2214
|
...(activeSpan ? { span: activeSpan } : {}),
|
|
@@ -401,9 +401,13 @@ export function createAnthropicLoopAdapter(config) {
|
|
|
401
401
|
? params.thinking.budget_tokens
|
|
402
402
|
: undefined,
|
|
403
403
|
},
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
404
|
+
// Read only by a recording span, so a request nobody traces is not
|
|
405
|
+
// copied and sanitized on every step.
|
|
406
|
+
get input() {
|
|
407
|
+
return {
|
|
408
|
+
system: params.system,
|
|
409
|
+
messages: sanitizeAnthropicMessagesForTrace(params.messages),
|
|
410
|
+
};
|
|
407
411
|
},
|
|
408
412
|
},
|
|
409
413
|
};
|
|
@@ -771,6 +771,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
771
771
|
name: "provider.stream",
|
|
772
772
|
tracer: tracers.provider,
|
|
773
773
|
detached: true,
|
|
774
|
+
rootWhenOrphaned: true,
|
|
774
775
|
attributes: {
|
|
775
776
|
[ATTR.NL_MODEL]: modelName,
|
|
776
777
|
[ATTR.NL_PROVIDER]: this.providerName,
|
|
@@ -1474,6 +1475,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
1474
1475
|
name: "provider.generate",
|
|
1475
1476
|
tracer: tracers.provider,
|
|
1476
1477
|
detached: true,
|
|
1478
|
+
rootWhenOrphaned: true,
|
|
1477
1479
|
attributes: {
|
|
1478
1480
|
[ATTR.NL_MODEL]: modelName,
|
|
1479
1481
|
[ATTR.NL_PROVIDER]: this.providerName,
|
|
@@ -520,11 +520,12 @@ export declare class GoogleVertexProvider extends BaseProvider {
|
|
|
520
520
|
private preserveStreamResultAccessors;
|
|
521
521
|
private withClaudeGeneration;
|
|
522
522
|
/**
|
|
523
|
-
* Emit `generation:end` so
|
|
524
|
-
* the
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
523
|
+
* Emit `generation:end` so event listeners and the in-process metrics
|
|
524
|
+
* aggregator record the generation. Vertex bypasses the AI SDK (and
|
|
525
|
+
* therefore the experimental_telemetry plumbing), so nothing else emits it;
|
|
526
|
+
* the in-process store is never exported, and the Langfuse generation
|
|
527
|
+
* observations are the spans opened where each model call is made.
|
|
528
|
+
* Mirrors the Bedrock + Ollama pattern.
|
|
528
529
|
*/
|
|
529
530
|
private emitGenerationEnd;
|
|
530
531
|
/**
|
|
@@ -43,7 +43,9 @@ 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, spanContentAttribute, spanModelRequestAttribute, tracers, withClientSpan, withClientStreamSpan, withSpan, } from "../../telemetry/index.js";
|
|
46
|
+
import { ATTR, recordsSpanInputs, spanContentAttribute, spanModelRequestAttribute, tracers, withClientSpan, withClientStreamSpan, withSpan, } from "../../telemetry/index.js";
|
|
47
|
+
import { recordsSpanContent } from "../../telemetry/attributes.js";
|
|
48
|
+
import { inTurnlessTree } from "../../telemetry/tracers.js";
|
|
47
49
|
import { observe, observeGeneration, observeTrace, } from "../../telemetry/observation.js";
|
|
48
50
|
import { withGeneration, withStepSpan } from "../../telemetry/withSpan.js";
|
|
49
51
|
import { context as otelContext, SpanKind, SpanStatusCode, trace as otelTrace, } from "@opentelemetry/api";
|
|
@@ -1004,6 +1006,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
1004
1006
|
name: "provider.stream",
|
|
1005
1007
|
tracer: tracers.provider,
|
|
1006
1008
|
detached: true,
|
|
1009
|
+
rootWhenOrphaned: true,
|
|
1007
1010
|
attributes: {
|
|
1008
1011
|
[ATTR.NL_MODEL]: modelName,
|
|
1009
1012
|
[ATTR.NL_PROVIDER]: this.providerName,
|
|
@@ -3472,7 +3475,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
3472
3475
|
// Langfuse/OTel: the native SDK bypasses the Vercel AI SDK's
|
|
3473
3476
|
// experimental_telemetry, so emit spans manually — one turn span, one
|
|
3474
3477
|
// generation span per API call, one tool span per execution — all carrying
|
|
3475
|
-
// langfuse.* attributes
|
|
3478
|
+
// the langfuse.* observation attributes ExporterSpanProcessor exports.
|
|
3476
3479
|
// Usage lives ONLY on the generation spans (Langfuse sums usage across
|
|
3477
3480
|
// observations for trace totals, so repeating it on the turn double-counts).
|
|
3478
3481
|
const offeredToolNames = (tools ?? []).map((anthropicTool) => anthropicTool.name);
|
|
@@ -5515,12 +5518,20 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
5515
5518
|
name: "provider.generate",
|
|
5516
5519
|
tracer: tracers.provider,
|
|
5517
5520
|
detached: true,
|
|
5521
|
+
rootWhenOrphaned: true,
|
|
5518
5522
|
attributes: {
|
|
5519
5523
|
[ATTR.NL_MODEL]: modelName,
|
|
5520
5524
|
[ATTR.NL_PROVIDER]: this.providerName,
|
|
5521
5525
|
},
|
|
5522
5526
|
}, async (generateSpan) => {
|
|
5523
5527
|
const generateStartTime = Date.now();
|
|
5528
|
+
// Neither this span nor executeGeneration is an observation: the
|
|
5529
|
+
// generation spans below carry the model call. Marked internal so no
|
|
5530
|
+
// exporter of ours sends them as duplicates, except for the root a
|
|
5531
|
+
// direct call opened for itself, which the loop hangs from.
|
|
5532
|
+
if (!inTurnlessTree(generateSpan)) {
|
|
5533
|
+
generateSpan.setAttribute(ATTR.NL_INTERNAL, true);
|
|
5534
|
+
}
|
|
5524
5535
|
// One dispatch decision for the whole override: Vertex's generate()
|
|
5525
5536
|
// bypasses BaseProvider.generate(), so it re-runs the same
|
|
5526
5537
|
// resolveRequestKind() the core call sites use rather than keeping
|
|
@@ -5597,15 +5608,21 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
5597
5608
|
const inputPrompt = mergedOptions.input?.text ||
|
|
5598
5609
|
mergedOptions.prompt ||
|
|
5599
5610
|
"";
|
|
5600
|
-
//
|
|
5601
|
-
//
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5611
|
+
// A host exporter on the same provider reads the input and output of
|
|
5612
|
+
// these two spans, so they follow the policy of a generation: masked,
|
|
5613
|
+
// and left out when inputs (or all content) are not recorded. The
|
|
5614
|
+
// input is set before the call so an error path still carries it.
|
|
5615
|
+
const generationInputAttribute = recordsSpanInputs()
|
|
5616
|
+
? spanModelRequestAttribute({
|
|
5617
|
+
...(mergedOptions.systemPrompt
|
|
5618
|
+
? { system: mergedOptions.systemPrompt }
|
|
5619
|
+
: {}),
|
|
5620
|
+
prompt: inputPrompt,
|
|
5621
|
+
})
|
|
5622
|
+
: undefined;
|
|
5623
|
+
if (generationInputAttribute !== undefined) {
|
|
5624
|
+
generateSpan.setAttribute(LangfuseOtelSpanAttributes.OBSERVATION_INPUT, generationInputAttribute);
|
|
5625
|
+
}
|
|
5609
5626
|
try {
|
|
5610
5627
|
let result;
|
|
5611
5628
|
// Wrap the actual native generate call in `neurolink.executeGeneration`
|
|
@@ -5619,10 +5636,15 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
5619
5636
|
detached: true,
|
|
5620
5637
|
attributes: {
|
|
5621
5638
|
[ATTR.NL_MODEL]: modelName,
|
|
5639
|
+
[ATTR.NL_INTERNAL]: true,
|
|
5622
5640
|
path: isAnthropicModel(modelName)
|
|
5623
5641
|
? "native.anthropic"
|
|
5624
5642
|
: "native.google-genai",
|
|
5625
|
-
|
|
5643
|
+
...(generationInputAttribute !== undefined
|
|
5644
|
+
? {
|
|
5645
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_INPUT]: generationInputAttribute,
|
|
5646
|
+
}
|
|
5647
|
+
: {}),
|
|
5626
5648
|
},
|
|
5627
5649
|
}, async (executionSpan) => {
|
|
5628
5650
|
let nativeResult;
|
|
@@ -5640,7 +5662,9 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
5640
5662
|
});
|
|
5641
5663
|
nativeResult = await this.runNativeGenerateWithMiddleware(mergedOptions, modelName, (effectiveOptions) => this.executeNativeGemini3GenerateWithSchemaFallback(effectiveOptions, modelName));
|
|
5642
5664
|
}
|
|
5643
|
-
|
|
5665
|
+
if (recordsSpanContent()) {
|
|
5666
|
+
executionSpan.setAttribute(LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT, spanContentAttribute(nativeResult?.content ?? ""));
|
|
5667
|
+
}
|
|
5644
5668
|
return nativeResult;
|
|
5645
5669
|
});
|
|
5646
5670
|
// Pipe through TTS-of-AI-response when caller asks for it. The
|
|
@@ -5648,7 +5672,9 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
5648
5672
|
// enabled / useAiResponse is false, so the cost is zero on
|
|
5649
5673
|
// non-TTS paths.
|
|
5650
5674
|
result = await this.synthesizeAIResponseIfNeeded(result, options);
|
|
5651
|
-
|
|
5675
|
+
if (recordsSpanContent()) {
|
|
5676
|
+
generateSpan.setAttribute(LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT, spanContentAttribute(result?.content ?? ""));
|
|
5677
|
+
}
|
|
5652
5678
|
// Fire onFinish lifecycle callback for the native generate path.
|
|
5653
5679
|
// Pipeline A providers get this for free via the AI SDK middleware
|
|
5654
5680
|
// wrapper (LifecycleMiddleware); native @google/genai bypasses
|
|
@@ -6343,11 +6369,12 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
6343
6369
|
}));
|
|
6344
6370
|
}
|
|
6345
6371
|
/**
|
|
6346
|
-
* Emit `generation:end` so
|
|
6347
|
-
* the
|
|
6348
|
-
*
|
|
6349
|
-
*
|
|
6350
|
-
*
|
|
6372
|
+
* Emit `generation:end` so event listeners and the in-process metrics
|
|
6373
|
+
* aggregator record the generation. Vertex bypasses the AI SDK (and
|
|
6374
|
+
* therefore the experimental_telemetry plumbing), so nothing else emits it;
|
|
6375
|
+
* the in-process store is never exported, and the Langfuse generation
|
|
6376
|
+
* observations are the spans opened where each model call is made.
|
|
6377
|
+
* Mirrors the Bedrock + Ollama pattern.
|
|
6351
6378
|
*/
|
|
6352
6379
|
emitGenerationEnd(modelName, result, startTime, success, error, prompt) {
|
|
6353
6380
|
const emitter = this.neurolink?.getEventEmitter();
|
|
@@ -1946,9 +1946,9 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
1946
1946
|
// a blanket "stop" for every stream that drained without throwing.
|
|
1947
1947
|
streamMetadata.finishReason = toUnifiedFinishReason(rawFinishReason);
|
|
1948
1948
|
// No-output path: stream completed normally but yielded zero text.
|
|
1949
|
-
// Build an enriched sentinel + stamp the active OTel span
|
|
1950
|
-
//
|
|
1951
|
-
// observation
|
|
1949
|
+
// Build an enriched sentinel + stamp the active OTel span with the
|
|
1950
|
+
// WARNING observation level, which ExporterSpanProcessor exports,
|
|
1951
|
+
// so the observation does not read as a silent success.
|
|
1952
1952
|
if (contentYielded === 0 && toolsUsed.length === 0) {
|
|
1953
1953
|
logger.warn(`${providerName}: Stream produced no output — emitting enriched sentinel`);
|
|
1954
1954
|
const fauxNoOutput = new NoOutputGeneratedError({
|
|
@@ -2078,7 +2078,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
2078
2078
|
...(reasoningTokens > 0 ? { reasoningTokens } : {}),
|
|
2079
2079
|
};
|
|
2080
2080
|
};
|
|
2081
|
-
const stepSpans = new StepSpans();
|
|
2081
|
+
const stepSpans = new StepSpans("stream");
|
|
2082
2082
|
try {
|
|
2083
2083
|
// May grow mid-turn: hydrated tools with wire-unsafe names need
|
|
2084
2084
|
// reverse-mapping even when the initial name set required none.
|
package/dist/proxy/codexUsage.js
CHANGED
|
@@ -42,6 +42,14 @@
|
|
|
42
42
|
import { appendFileSync } from "node:fs";
|
|
43
43
|
import { extractSSEEvents } from "./sseInterceptor.js";
|
|
44
44
|
import { sanitizeForLog } from "../utils/logSanitize.js";
|
|
45
|
+
/**
|
|
46
|
+
* Characters of `response.output_item.done` payloads kept to stand in for a
|
|
47
|
+
* completed response that carries no output. Bounds what the tap holds for a
|
|
48
|
+
* stream whose items it cannot otherwise discard.
|
|
49
|
+
*/
|
|
50
|
+
const OUTPUT_ITEMS_LIMIT_CHARS = 1024 * 1024;
|
|
51
|
+
/** Type of the item that stands where output items past the cap were left out. */
|
|
52
|
+
const OMITTED_ITEMS_TYPE = "neurolink_output_omitted";
|
|
45
53
|
const nonNegativeInt = (value) => typeof value === "number" && Number.isFinite(value) && value > 0
|
|
46
54
|
? Math.floor(value)
|
|
47
55
|
: 0;
|
|
@@ -208,6 +216,42 @@ function createCaptureSink() {
|
|
|
208
216
|
export function createCodexUsageTap() {
|
|
209
217
|
const evidence = { completed: false, terminalBytes: 0 };
|
|
210
218
|
let totalBytes = 0;
|
|
219
|
+
// Items announced by response.output_item.done. The terminal response may
|
|
220
|
+
// arrive with an empty or absent output, leaving these as the only record
|
|
221
|
+
// of what the model said.
|
|
222
|
+
const streamedItems = [];
|
|
223
|
+
let streamedItemChars = 0;
|
|
224
|
+
let omittedItems = 0;
|
|
225
|
+
// An item past the cap leaves a marker in its place, so the record reads as
|
|
226
|
+
// incomplete rather than as a shorter reply; adjacent omissions share one.
|
|
227
|
+
const omitItem = () => {
|
|
228
|
+
omittedItems += 1;
|
|
229
|
+
const last = streamedItems[streamedItems.length - 1];
|
|
230
|
+
if (last?.type === OMITTED_ITEMS_TYPE) {
|
|
231
|
+
streamedItems[streamedItems.length - 1] = {
|
|
232
|
+
...last,
|
|
233
|
+
omitted_items: Number(last.omitted_items) + 1,
|
|
234
|
+
};
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const marker = {
|
|
238
|
+
type: OMITTED_ITEMS_TYPE,
|
|
239
|
+
omitted_items: 1,
|
|
240
|
+
note: `…[truncated: output items past ${OUTPUT_ITEMS_LIMIT_CHARS} characters are not recorded]`,
|
|
241
|
+
};
|
|
242
|
+
streamedItems.push(marker);
|
|
243
|
+
streamedItemChars += JSON.stringify(marker).length;
|
|
244
|
+
};
|
|
245
|
+
const withStreamedOutput = (response) => {
|
|
246
|
+
if ((Array.isArray(response.output) && response.output.length > 0) ||
|
|
247
|
+
streamedItems.length === 0) {
|
|
248
|
+
return response;
|
|
249
|
+
}
|
|
250
|
+
if (omittedItems > 0) {
|
|
251
|
+
evidence.outputItemsOmitted = true;
|
|
252
|
+
}
|
|
253
|
+
return { ...response, output: [...streamedItems] };
|
|
254
|
+
};
|
|
211
255
|
const inspectEvidence = (events) => {
|
|
212
256
|
for (const frame of events) {
|
|
213
257
|
if (frame.data.trim() === "[DONE]") {
|
|
@@ -234,6 +278,19 @@ export function createCodexUsageTap() {
|
|
|
234
278
|
evidence.firstUsefulOutputEvent = String(type);
|
|
235
279
|
}
|
|
236
280
|
}
|
|
281
|
+
if (type === "response.output_item.done" &&
|
|
282
|
+
event.item &&
|
|
283
|
+
typeof event.item === "object" &&
|
|
284
|
+
!Array.isArray(event.item)) {
|
|
285
|
+
if (streamedItemChars + frame.data.length <=
|
|
286
|
+
OUTPUT_ITEMS_LIMIT_CHARS) {
|
|
287
|
+
streamedItems.push(event.item);
|
|
288
|
+
streamedItemChars += frame.data.length;
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
omitItem();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
237
294
|
// Some clients/providers emit complete output items without deltas.
|
|
238
295
|
// Reasoning/control events are not useful client output. Recognize
|
|
239
296
|
// actual tool calls and text only, without inventing an earlier time.
|
|
@@ -277,8 +334,8 @@ export function createCodexUsageTap() {
|
|
|
277
334
|
type.startsWith("response.") &&
|
|
278
335
|
completedResponse &&
|
|
279
336
|
typeof completedResponse === "object" &&
|
|
280
|
-
"output" in completedResponse) {
|
|
281
|
-
evidence.response = completedResponse;
|
|
337
|
+
("output" in completedResponse || streamedItems.length > 0)) {
|
|
338
|
+
evidence.response = withStreamedOutput(completedResponse);
|
|
282
339
|
}
|
|
283
340
|
if (type === "response.completed") {
|
|
284
341
|
evidence.completed = true;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - calculateCost() from pricing.ts for cost tracking
|
|
13
13
|
*/
|
|
14
14
|
import { type Span } from "@opentelemetry/api";
|
|
15
|
-
import type { AccountSelectionContext, ProxyRequestContext, ProxyRequestOrigin, ResponseInfoContext, UpstreamAttemptContext, UsageContext, ProxyUsageAttributionOptions, RuntimeRequestMetadata, ProxyLogTraceContext } from "../types/index.js";
|
|
15
|
+
import type { AccountSelectionContext, JsonValue, ProxyRequestContext, ProxyRequestOrigin, ResponseInfoContext, UpstreamAttemptContext, UsageContext, ProxyUsageAttributionOptions, RuntimeRequestMetadata, ProxyLogTraceContext } from "../types/index.js";
|
|
16
16
|
declare class ProxyTracer {
|
|
17
17
|
private readonly rootSpan;
|
|
18
18
|
private readonly traceContext;
|
|
@@ -43,6 +43,12 @@ declare class ProxyTracer {
|
|
|
43
43
|
private readonly requestedModel;
|
|
44
44
|
private input?;
|
|
45
45
|
private output?;
|
|
46
|
+
/** The parsed response outranks the raw upstream body that arrives after it. */
|
|
47
|
+
private outputIsParsed;
|
|
48
|
+
/** A model call made through `runModelCall` records the generation. */
|
|
49
|
+
private modelCallDelegated;
|
|
50
|
+
/** That call's records carry no content; fixed when the call starts. */
|
|
51
|
+
private modelContentWithheld;
|
|
46
52
|
private responseModel?;
|
|
47
53
|
private finishReason?;
|
|
48
54
|
private firstOutputAt?;
|
|
@@ -65,6 +71,20 @@ declare class ProxyTracer {
|
|
|
65
71
|
startUpstreamAttempt(ctx: UpstreamAttemptContext): Span;
|
|
66
72
|
/** Span covering the SSE stream relay phase. */
|
|
67
73
|
startStream(): Span;
|
|
74
|
+
/**
|
|
75
|
+
* Run a model call inside this request's trace context, so the spans it
|
|
76
|
+
* starts nest under the proxy root and carry the request's user and session.
|
|
77
|
+
* That call's generation holds usage and cost, so the root records none of
|
|
78
|
+
* its own. Its prompt and answer are request and response bodies, so its
|
|
79
|
+
* records carry them only with `NEUROLINK_PROXY_TRACE_BODY_LOGGING=true`.
|
|
80
|
+
*/
|
|
81
|
+
runModelCall<T>(fn: () => T): T;
|
|
82
|
+
/**
|
|
83
|
+
* The delegated call's stream, read and closed under the same content rule:
|
|
84
|
+
* the call records its answer while the stream is consumed, which happens
|
|
85
|
+
* after `runModelCall` has returned.
|
|
86
|
+
*/
|
|
87
|
+
modelStream<T>(iterator: AsyncIterator<T>): AsyncIterator<T>;
|
|
68
88
|
/** Record account selection outcome on the root span. */
|
|
69
89
|
setAccountSelection(ctx: AccountSelectionContext): void;
|
|
70
90
|
/** Attribute fallback metrics to the account that actually served them. */
|
|
@@ -99,6 +119,8 @@ declare class ProxyTracer {
|
|
|
99
119
|
attemptCount: number;
|
|
100
120
|
reason: string;
|
|
101
121
|
}): void;
|
|
122
|
+
/** Record the client request body as the input of the root and its generation. */
|
|
123
|
+
setRequestInput(body: JsonValue): void;
|
|
102
124
|
/** Log the incoming client request body (redacted). */
|
|
103
125
|
logRequestBody(body: string): void;
|
|
104
126
|
/** Log the incoming client request headers (redacted). */
|
|
@@ -127,6 +149,8 @@ declare class ProxyTracer {
|
|
|
127
149
|
getUsage(): UsageContext | undefined;
|
|
128
150
|
/** End the root span with final HTTP status and duration, and emit OTEL metrics. */
|
|
129
151
|
end(responseStatus: number, durationMs: number): void;
|
|
152
|
+
/** Bodies are only worth keeping while something is recording the span. */
|
|
153
|
+
private recordsBodies;
|
|
130
154
|
private levelFor;
|
|
131
155
|
private observeOutcome;
|
|
132
156
|
/**
|