@juspay/neurolink 13.4.10 → 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 +2 -3
- package/README.md +3 -2
- 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 +466 -466
- package/dist/cli/commands/decide.js +25 -5
- 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/factories/providerDescriptors.d.ts +4 -3
- package/dist/factories/providerDescriptors.js +6 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp/externalServerManager.js +6 -2
- package/dist/mcp/toolRegistry.js +3 -0
- package/dist/neurolink.d.ts +20 -10
- package/dist/neurolink.js +134 -65
- 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/catalog/provider-catalog.schema.json +64 -3
- package/dist/providers/catalog/schema.js +81 -1
- package/dist/providers/catalog/xor.json +30 -10
- package/dist/providers/configuredDecision.d.ts +8 -1
- package/dist/providers/configuredDecision.js +22 -0
- package/dist/providers/decide/dialects/openaiDecisions.js +8 -0
- package/dist/providers/decide/dialects/systemOne.js +28 -2
- package/dist/providers/decide/guards.d.ts +6 -0
- package/dist/providers/decide/guards.js +22 -0
- 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/providers/systemOneDecision.d.ts +30 -7
- package/dist/providers/systemOneDecision.js +71 -14
- 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/cli.d.ts +2 -0
- package/dist/types/decision.d.ts +117 -10
- 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/providerCatalog.d.ts +12 -1
- package/dist/types/providers.d.ts +2 -0
- package/dist/types/proxy.d.ts +2 -0
- package/dist/types/span.d.ts +26 -0
- package/dist/utils/decisionAnswers.d.ts +7 -1
- package/dist/utils/decisionAnswers.js +43 -2
- package/dist/utils/decisionLimits.d.ts +13 -1
- package/dist/utils/decisionLimits.js +52 -1
- package/dist/utils/decisionMedia.d.ts +4 -2
- package/dist/utils/decisionMedia.js +216 -14
- 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 +56 -48
- package/package.json +21 -2
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Agent as HttpAgent } from "node:http";
|
|
2
|
+
import { Agent as HttpsAgent } from "node:https";
|
|
1
3
|
import { metrics } from "@opentelemetry/api";
|
|
2
4
|
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
3
5
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
@@ -5,14 +7,57 @@ import { MeterProvider, PeriodicExportingMetricReader, } from "@opentelemetry/sd
|
|
|
5
7
|
import { BatchSpanProcessor, } from "@opentelemetry/sdk-trace-base";
|
|
6
8
|
import { OTEL_LOG_PREFIX } from "../../../../constants/telemetry.js";
|
|
7
9
|
import { logger } from "../../../../utils/logger.js";
|
|
10
|
+
// The exporters retry a 429, 502, 503 or 504 after a backoff and hand the retried
|
|
11
|
+
// request the time that was left before the backoff, not after it, so a retry
|
|
12
|
+
// into a collector that never answers outlives the flush budget by the backoff.
|
|
13
|
+
const CUT_AGAIN_MS = 100;
|
|
14
|
+
/**
|
|
15
|
+
* @description Tracks the connections the OTLP exporters open, so a flush or shutdown that has run out of its budget can close them instead of leaving a request in flight that holds the process open.
|
|
16
|
+
* @returns {OtlpConnections} The agent factory to give the exporters and the function that closes what they opened.
|
|
17
|
+
*/
|
|
18
|
+
export function createOtlpConnections() {
|
|
19
|
+
const agents = new Set();
|
|
20
|
+
const destroyAll = () => {
|
|
21
|
+
for (const agent of agents) {
|
|
22
|
+
agent.destroy();
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
agentFor: (protocol) => {
|
|
27
|
+
const agent = protocol === "https:"
|
|
28
|
+
? new HttpsAgent({ keepAlive: true })
|
|
29
|
+
: new HttpAgent({ keepAlive: true });
|
|
30
|
+
agents.add(agent);
|
|
31
|
+
return agent;
|
|
32
|
+
},
|
|
33
|
+
cut: () => {
|
|
34
|
+
destroyAll();
|
|
35
|
+
setTimeout(destroyAll, CUT_AGAIN_MS).unref();
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// The request timeout also bounds how long an exit flush can hold the process
|
|
40
|
+
// when the collector cannot be reached; the OTLP timeout variables still win.
|
|
41
|
+
function requestTimeout(signal, budgetMs) {
|
|
42
|
+
return process.env.OTEL_EXPORTER_OTLP_TIMEOUT ||
|
|
43
|
+
process.env[`OTEL_EXPORTER_OTLP_${signal}_TIMEOUT`]
|
|
44
|
+
? undefined
|
|
45
|
+
: budgetMs;
|
|
46
|
+
}
|
|
8
47
|
/**
|
|
9
48
|
* @description Builds a batching span processor that ships spans to an OTLP/HTTP endpoint.
|
|
10
49
|
* @param endpoint {string} - OTLP base URL; `/v1/traces` is appended.
|
|
50
|
+
* @param budgetMs {number | undefined} - Request timeout in milliseconds, used unless `OTEL_EXPORTER_OTLP_TIMEOUT` or `OTEL_EXPORTER_OTLP_TRACES_TIMEOUT` is set; the exporter's default when omitted.
|
|
51
|
+
* @param connections {OtlpConnections | undefined} - Tracks the connections the exporter opens so they can be closed when a flush runs out of its budget.
|
|
11
52
|
* @returns {SpanProcessor | null} The processor, or `null` (after a warning) when the exporter cannot be created.
|
|
12
53
|
*/
|
|
13
|
-
export function createOtlpSpanProcessor(endpoint) {
|
|
54
|
+
export function createOtlpSpanProcessor(endpoint, budgetMs, connections) {
|
|
14
55
|
try {
|
|
15
|
-
return new BatchSpanProcessor(new OTLPTraceExporter({
|
|
56
|
+
return new BatchSpanProcessor(new OTLPTraceExporter({
|
|
57
|
+
url: `${endpoint}/v1/traces`,
|
|
58
|
+
timeoutMillis: requestTimeout("TRACES", budgetMs),
|
|
59
|
+
...(connections && { httpAgentOptions: connections.agentFor }),
|
|
60
|
+
}), {
|
|
16
61
|
maxQueueSize: 2048,
|
|
17
62
|
maxExportBatchSize: 512,
|
|
18
63
|
scheduledDelayMillis: 1000,
|
|
@@ -31,15 +76,21 @@ export function createOtlpSpanProcessor(endpoint) {
|
|
|
31
76
|
* @description Builds a meter provider that exports metrics to an OTLP/HTTP endpoint and registers it as the global meter provider.
|
|
32
77
|
* @param endpoint {string} - OTLP base URL; `/v1/metrics` is appended.
|
|
33
78
|
* @param resource {Resource} - Resource describing the service.
|
|
79
|
+
* @param budgetMs {number | undefined} - Request timeout in milliseconds, used unless `OTEL_EXPORTER_OTLP_TIMEOUT` or `OTEL_EXPORTER_OTLP_METRICS_TIMEOUT` is set; the exporter's default when omitted.
|
|
80
|
+
* @param connections {OtlpConnections | undefined} - Tracks the connections the exporter opens so they can be closed when a flush runs out of its budget.
|
|
34
81
|
* @returns {MeterProvider | null} The provider, or `null` (after a warning) when it cannot be created.
|
|
35
82
|
*/
|
|
36
|
-
export function createOtlpMeterProvider(endpoint, resource) {
|
|
83
|
+
export function createOtlpMeterProvider(endpoint, resource, budgetMs, connections) {
|
|
37
84
|
try {
|
|
38
85
|
const provider = new MeterProvider({
|
|
39
86
|
resource,
|
|
40
87
|
readers: [
|
|
41
88
|
new PeriodicExportingMetricReader({
|
|
42
|
-
exporter: new OTLPMetricExporter({
|
|
89
|
+
exporter: new OTLPMetricExporter({
|
|
90
|
+
url: `${endpoint}/v1/metrics`,
|
|
91
|
+
timeoutMillis: requestTimeout("METRICS", budgetMs),
|
|
92
|
+
...(connections && { httpAgentOptions: connections.agentFor }),
|
|
93
|
+
}),
|
|
43
94
|
exportIntervalMillis: 15000,
|
|
44
95
|
exportTimeoutMillis: 10000,
|
|
45
96
|
}),
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { MetricConnectionType } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* @description Counts one AI request with its duration, tokens and, when known, cost, labelled by provider and model.
|
|
4
|
+
* @param provider {string} - Provider that served the request.
|
|
5
|
+
* @param model {string} - Model that served the request.
|
|
6
|
+
* @param tokens {number} - Total tokens used.
|
|
7
|
+
* @param durationMs {number} - Request duration in milliseconds.
|
|
8
|
+
* @param cost {number | undefined} - Cost in USD; omitted or non-positive values add nothing to the cost counter.
|
|
9
|
+
* @returns {void} Nothing.
|
|
10
|
+
*/
|
|
11
|
+
export declare function recordAIRequest(provider: string, model: string, tokens: number, durationMs: number, cost?: number): void;
|
|
12
|
+
/**
|
|
13
|
+
* @description Counts one AI provider error, labelled by provider, error name and a bounded `message`: the HTTP status the error carries (`HTTP 429`), or `unspecified`, never the message text.
|
|
14
|
+
* @param provider {string} - Provider that failed.
|
|
15
|
+
* @param error {Error} - The failure.
|
|
16
|
+
* @returns {void} Nothing.
|
|
17
|
+
*/
|
|
18
|
+
export declare function recordAIError(provider: string, error: Error): void;
|
|
19
|
+
/**
|
|
20
|
+
* @description Counts one MCP tool call, labelled by tool, outcome and a duration bucket (`fast`, `medium`, `slow`, `very_slow`).
|
|
21
|
+
* @param toolName {string} - Name of the tool.
|
|
22
|
+
* @param durationMs {number} - Call duration in milliseconds.
|
|
23
|
+
* @param success {boolean} - `false` for a thrown error and for a resolved MCP error result.
|
|
24
|
+
* @returns {void} Nothing.
|
|
25
|
+
*/
|
|
26
|
+
export declare function recordMCPToolCall(toolName: string, durationMs: number, success: boolean): void;
|
|
27
|
+
/**
|
|
28
|
+
* @description Counts one opened connection, labelled by its type.
|
|
29
|
+
* @param type {MetricConnectionType} - `websocket`, `sse` or `http`.
|
|
30
|
+
* @returns {void} Nothing.
|
|
31
|
+
*/
|
|
32
|
+
export declare function recordConnection(type: MetricConnectionType): void;
|
|
33
|
+
/**
|
|
34
|
+
* @description Records one server response time, labelled by endpoint, method and a status bucket (`excellent`, `good`, `acceptable`, `poor`).
|
|
35
|
+
* @param endpoint {string} - Route that answered.
|
|
36
|
+
* @param method {string} - HTTP method.
|
|
37
|
+
* @param durationMs {number} - Response time in milliseconds.
|
|
38
|
+
* @returns {void} Nothing.
|
|
39
|
+
*/
|
|
40
|
+
export declare function recordResponseTime(endpoint: string, method: string, durationMs: number): void;
|
|
41
|
+
/**
|
|
42
|
+
* @description Adds to a counter named `custom_<name>`, created on first use.
|
|
43
|
+
* @param name {string} - Metric name without the `custom_` prefix.
|
|
44
|
+
* @param value {number} - Amount to add.
|
|
45
|
+
* @param labels {Record<string, string> | undefined} - Series labels.
|
|
46
|
+
* @returns {void} Nothing.
|
|
47
|
+
*/
|
|
48
|
+
export declare function recordCustomMetric(name: string, value: number, labels?: Record<string, string>): void;
|
|
49
|
+
/**
|
|
50
|
+
* @description Records into a histogram named `custom_<name>_histogram`, created on first use.
|
|
51
|
+
* @param name {string} - Metric name without the `custom_` prefix and `_histogram` suffix.
|
|
52
|
+
* @param value {number} - Value to record.
|
|
53
|
+
* @param labels {Record<string, string> | undefined} - Series labels.
|
|
54
|
+
* @returns {void} Nothing.
|
|
55
|
+
*/
|
|
56
|
+
export declare function recordCustomHistogram(name: string, value: number, labels?: Record<string, string>): void;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { createNoopMeter, metrics, } from "@opentelemetry/api";
|
|
2
|
+
import { logger } from "../utils/logger.js";
|
|
3
|
+
import { startTelemetry } from "./telemetryStarter.js";
|
|
4
|
+
const METER_NAME = "neurolink-ai";
|
|
5
|
+
let boundProvider;
|
|
6
|
+
let instruments;
|
|
7
|
+
// The API hands out its shared no-op meter until some MeterProvider, the
|
|
8
|
+
// host's or NeuroLink's own, is registered.
|
|
9
|
+
function isMetricsEnabled(meter) {
|
|
10
|
+
return (process.env.NEUROLINK_TELEMETRY_ENABLED === "true" ||
|
|
11
|
+
Boolean(process.env.OTEL_EXPORTER_OTLP_ENDPOINT) ||
|
|
12
|
+
meter !== createNoopMeter());
|
|
13
|
+
}
|
|
14
|
+
function createInstruments(meter) {
|
|
15
|
+
return {
|
|
16
|
+
meter,
|
|
17
|
+
requests: meter.createCounter("ai_requests_total", {
|
|
18
|
+
description: "Total number of AI requests",
|
|
19
|
+
}),
|
|
20
|
+
duration: meter.createHistogram("ai_request_duration_ms", {
|
|
21
|
+
description: "AI request duration in milliseconds",
|
|
22
|
+
}),
|
|
23
|
+
tokens: meter.createCounter("ai_tokens_used_total", {
|
|
24
|
+
description: "Total number of AI tokens used",
|
|
25
|
+
}),
|
|
26
|
+
cost: meter.createCounter("ai_cost_usd_total", {
|
|
27
|
+
description: "Total accumulated AI cost in USD",
|
|
28
|
+
}),
|
|
29
|
+
providerErrors: meter.createCounter("ai_provider_errors_total", {
|
|
30
|
+
description: "Total number of AI provider errors",
|
|
31
|
+
}),
|
|
32
|
+
mcpToolCalls: meter.createCounter("mcp_tool_calls_total", {
|
|
33
|
+
description: "Total number of MCP tool calls",
|
|
34
|
+
}),
|
|
35
|
+
connections: meter.createCounter("connections_total", {
|
|
36
|
+
description: "Total number of connections",
|
|
37
|
+
}),
|
|
38
|
+
responseTime: meter.createHistogram("response_time_ms", {
|
|
39
|
+
description: "Response time in milliseconds",
|
|
40
|
+
}),
|
|
41
|
+
custom: new Map(),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The API hands out a no-op meter until a MeterProvider is registered and
|
|
46
|
+
* never upgrades it, so instruments are bound to the provider that was
|
|
47
|
+
* registered when they were created and rebuilt when it changes.
|
|
48
|
+
*/
|
|
49
|
+
function currentInstruments() {
|
|
50
|
+
const provider = metrics.getMeterProvider();
|
|
51
|
+
if (!instruments || provider !== boundProvider) {
|
|
52
|
+
boundProvider = provider;
|
|
53
|
+
instruments = createInstruments(provider.getMeter(METER_NAME));
|
|
54
|
+
}
|
|
55
|
+
return instruments;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Metrics must never break the call they describe, and are recorded only when
|
|
59
|
+
* a collector, a registered MeterProvider or `NEUROLINK_TELEMETRY_ENABLED=true`
|
|
60
|
+
* asks for them.
|
|
61
|
+
*/
|
|
62
|
+
function record(write) {
|
|
63
|
+
try {
|
|
64
|
+
startTelemetry();
|
|
65
|
+
const current = currentInstruments();
|
|
66
|
+
if (isMetricsEnabled(current.meter)) {
|
|
67
|
+
write(current);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
logger.debug("[Telemetry] Recording a metric failed", {
|
|
72
|
+
error: error instanceof Error ? error.message : String(error),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function durationBucket(durationMs) {
|
|
77
|
+
if (durationMs < 100) {
|
|
78
|
+
return "fast";
|
|
79
|
+
}
|
|
80
|
+
if (durationMs < 500) {
|
|
81
|
+
return "medium";
|
|
82
|
+
}
|
|
83
|
+
if (durationMs < 1000) {
|
|
84
|
+
return "slow";
|
|
85
|
+
}
|
|
86
|
+
return "very_slow";
|
|
87
|
+
}
|
|
88
|
+
function statusBucket(durationMs) {
|
|
89
|
+
if (durationMs < 200) {
|
|
90
|
+
return "excellent";
|
|
91
|
+
}
|
|
92
|
+
if (durationMs < 500) {
|
|
93
|
+
return "good";
|
|
94
|
+
}
|
|
95
|
+
if (durationMs < 1000) {
|
|
96
|
+
return "acceptable";
|
|
97
|
+
}
|
|
98
|
+
return "poor";
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* @description Counts one AI request with its duration, tokens and, when known, cost, labelled by provider and model.
|
|
102
|
+
* @param provider {string} - Provider that served the request.
|
|
103
|
+
* @param model {string} - Model that served the request.
|
|
104
|
+
* @param tokens {number} - Total tokens used.
|
|
105
|
+
* @param durationMs {number} - Request duration in milliseconds.
|
|
106
|
+
* @param cost {number | undefined} - Cost in USD; omitted or non-positive values add nothing to the cost counter.
|
|
107
|
+
* @returns {void} Nothing.
|
|
108
|
+
*/
|
|
109
|
+
export function recordAIRequest(provider, model, tokens, durationMs, cost) {
|
|
110
|
+
record((current) => {
|
|
111
|
+
const labels = { provider, model };
|
|
112
|
+
current.requests.add(1, labels);
|
|
113
|
+
current.duration.record(durationMs, labels);
|
|
114
|
+
current.tokens.add(tokens, labels);
|
|
115
|
+
if (cost !== undefined && Number.isFinite(cost) && cost > 0) {
|
|
116
|
+
current.cost.add(cost, labels);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The `message` label from a bounded set: the HTTP status the error carries,
|
|
122
|
+
* never its text, which is unbounded cardinality and can carry a credential.
|
|
123
|
+
*/
|
|
124
|
+
function errorMessageLabel(error) {
|
|
125
|
+
const status = Reflect.get(error, "status") ?? Reflect.get(error, "statusCode");
|
|
126
|
+
return typeof status === "number" &&
|
|
127
|
+
Number.isInteger(status) &&
|
|
128
|
+
status >= 100 &&
|
|
129
|
+
status <= 599
|
|
130
|
+
? `HTTP ${status}`
|
|
131
|
+
: "unspecified";
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* @description Counts one AI provider error, labelled by provider, error name and a bounded `message`: the HTTP status the error carries (`HTTP 429`), or `unspecified`, never the message text.
|
|
135
|
+
* @param provider {string} - Provider that failed.
|
|
136
|
+
* @param error {Error} - The failure.
|
|
137
|
+
* @returns {void} Nothing.
|
|
138
|
+
*/
|
|
139
|
+
export function recordAIError(provider, error) {
|
|
140
|
+
record((current) => {
|
|
141
|
+
current.providerErrors.add(1, {
|
|
142
|
+
provider,
|
|
143
|
+
error: error.name,
|
|
144
|
+
message: errorMessageLabel(error),
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* @description Counts one MCP tool call, labelled by tool, outcome and a duration bucket (`fast`, `medium`, `slow`, `very_slow`).
|
|
150
|
+
* @param toolName {string} - Name of the tool.
|
|
151
|
+
* @param durationMs {number} - Call duration in milliseconds.
|
|
152
|
+
* @param success {boolean} - `false` for a thrown error and for a resolved MCP error result.
|
|
153
|
+
* @returns {void} Nothing.
|
|
154
|
+
*/
|
|
155
|
+
export function recordMCPToolCall(toolName, durationMs, success) {
|
|
156
|
+
record((current) => {
|
|
157
|
+
current.mcpToolCalls.add(1, {
|
|
158
|
+
tool: toolName,
|
|
159
|
+
success: success.toString(),
|
|
160
|
+
duration_bucket: durationBucket(durationMs),
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* @description Counts one opened connection, labelled by its type.
|
|
166
|
+
* @param type {MetricConnectionType} - `websocket`, `sse` or `http`.
|
|
167
|
+
* @returns {void} Nothing.
|
|
168
|
+
*/
|
|
169
|
+
export function recordConnection(type) {
|
|
170
|
+
record((current) => {
|
|
171
|
+
current.connections.add(1, { connection_type: type });
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* @description Records one server response time, labelled by endpoint, method and a status bucket (`excellent`, `good`, `acceptable`, `poor`).
|
|
176
|
+
* @param endpoint {string} - Route that answered.
|
|
177
|
+
* @param method {string} - HTTP method.
|
|
178
|
+
* @param durationMs {number} - Response time in milliseconds.
|
|
179
|
+
* @returns {void} Nothing.
|
|
180
|
+
*/
|
|
181
|
+
export function recordResponseTime(endpoint, method, durationMs) {
|
|
182
|
+
record((current) => {
|
|
183
|
+
current.responseTime.record(durationMs, {
|
|
184
|
+
endpoint,
|
|
185
|
+
method,
|
|
186
|
+
status_bucket: statusBucket(durationMs),
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* @description Adds to a counter named `custom_<name>`, created on first use.
|
|
192
|
+
* @param name {string} - Metric name without the `custom_` prefix.
|
|
193
|
+
* @param value {number} - Amount to add.
|
|
194
|
+
* @param labels {Record<string, string> | undefined} - Series labels.
|
|
195
|
+
* @returns {void} Nothing.
|
|
196
|
+
*/
|
|
197
|
+
export function recordCustomMetric(name, value, labels) {
|
|
198
|
+
record((current) => {
|
|
199
|
+
const key = `custom_${name}`;
|
|
200
|
+
const existing = current.custom.get(key);
|
|
201
|
+
const counter = existing && "add" in existing
|
|
202
|
+
? existing
|
|
203
|
+
: current.meter.createCounter(key, {
|
|
204
|
+
description: `Custom metric: ${name}`,
|
|
205
|
+
});
|
|
206
|
+
current.custom.set(key, counter);
|
|
207
|
+
counter.add(value, labels ?? {});
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* @description Records into a histogram named `custom_<name>_histogram`, created on first use.
|
|
212
|
+
* @param name {string} - Metric name without the `custom_` prefix and `_histogram` suffix.
|
|
213
|
+
* @param value {number} - Value to record.
|
|
214
|
+
* @param labels {Record<string, string> | undefined} - Series labels.
|
|
215
|
+
* @returns {void} Nothing.
|
|
216
|
+
*/
|
|
217
|
+
export function recordCustomHistogram(name, value, labels) {
|
|
218
|
+
record((current) => {
|
|
219
|
+
const key = `custom_${name}_histogram`;
|
|
220
|
+
const existing = current.custom.get(key);
|
|
221
|
+
const histogram = existing && "record" in existing
|
|
222
|
+
? existing
|
|
223
|
+
: current.meter.createHistogram(key, {
|
|
224
|
+
description: `Custom histogram: ${name}`,
|
|
225
|
+
});
|
|
226
|
+
current.custom.set(key, histogram);
|
|
227
|
+
histogram.record(value, labels ?? {});
|
|
228
|
+
});
|
|
229
|
+
}
|
|
@@ -25,6 +25,7 @@ export declare const ATTR: {
|
|
|
25
25
|
readonly NL_COST: "cost";
|
|
26
26
|
readonly NL_STRUCTURED_OUTPUT: "structured_output";
|
|
27
27
|
readonly NL_HAS_FALLBACK: "has_fallback";
|
|
28
|
+
readonly NL_INTERNAL: "langfuse.internal";
|
|
28
29
|
readonly MCP_SERVER_ID: "mcp.server_id";
|
|
29
30
|
readonly MCP_TOOL_NAME: "mcp.tool_name";
|
|
30
31
|
readonly MCP_TIMEOUT_MS: "mcp.timeout_ms";
|
|
@@ -114,7 +115,29 @@ export declare const SPAN_ATTRIBUTE_MAX_CHARS = 40000;
|
|
|
114
115
|
*/
|
|
115
116
|
export declare function spanAttributeMaxChars(): number;
|
|
116
117
|
/**
|
|
117
|
-
* @description
|
|
118
|
+
* @description Tells whether span inputs (prompts, messages and tool arguments) are recorded. `NEUROLINK_RECORD_INPUTS=false`, in any casing and with or without surrounding spaces, turns it off; any other value, or none, leaves it on, except inside `withoutSpanContent`. Read on every call, so a change applies to the next span.
|
|
119
|
+
* @returns {boolean} `false` when the operator opted out of recording inputs.
|
|
120
|
+
*/
|
|
121
|
+
export declare function recordsSpanInputs(): boolean;
|
|
122
|
+
/**
|
|
123
|
+
* @description Runs `fn` with span content withheld: an observation recorded while it runs, or while anything it starts runs, keeps its model, usage, cost, level and timings but no input or output. The proxy uses it for a model call made on a client's behalf, whose content it records only when bodies are opted in.
|
|
124
|
+
* @param fn {() => T} - Work whose observations carry no content.
|
|
125
|
+
* @returns {T} Whatever `fn` returns.
|
|
126
|
+
*/
|
|
127
|
+
export declare function withoutSpanContent<T>(fn: () => T): T;
|
|
128
|
+
/**
|
|
129
|
+
* @description Tells whether span input and output may be recorded here, i.e. the caller is not inside `withoutSpanContent`.
|
|
130
|
+
* @returns {boolean} `false` while content is withheld.
|
|
131
|
+
*/
|
|
132
|
+
export declare function recordsSpanContent(): boolean;
|
|
133
|
+
/**
|
|
134
|
+
* @description Tells whether a string is an attribute that `spanContentAttribute` or `spanModelRequestAttribute` returned recently, so it is already redacted and capped.
|
|
135
|
+
* @param value {string} - Candidate attribute value.
|
|
136
|
+
* @returns {boolean} `true` when the string came out of one of those functions.
|
|
137
|
+
*/
|
|
138
|
+
export declare function isFinishedAttribute(value: string): boolean;
|
|
139
|
+
/**
|
|
140
|
+
* @description Returns a copy of a value with credentials masked: values under keys such as `access_token`, `password`, `authorization` or `set-cookie`, the value of a `{ name, value }` or `{ key, value }` entry whose name is such a key, bearer tokens, and `name=value` credentials inside text and inside object keys. Purely numeric values after a credential-looking name (e.g. `max_tokens: 4096`) are left alone, and so is an entry whose name ends in `tokens`, `policy`, `type`, `algorithm`, `count` or `limit` (a setting, not a credential).
|
|
118
141
|
* @param value {JsonValue | object} - Content to redact; objects and arrays are walked recursively.
|
|
119
142
|
* @returns {JsonValue | object} A redacted copy, serializing exactly as the input when nothing in it is sensitive.
|
|
120
143
|
* @throws {Error} When the value cannot be walked; tracing callers use `spanContentAttribute`, which turns that into an omitted value.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { INTERNAL_SPAN_ATTRIBUTE, MAX_REDACTED_ATTRIBUTES, MIN_STRING_BUDGET_CHARS, REDACTED_VALUE, TRUNCATION_NOTE_ALLOWANCE_CHARS, } from "../constants/telemetry.js";
|
|
2
3
|
import { messageRole } from "./chatMessages.js";
|
|
3
4
|
import { binaryPlaceholder, declaredMimeType, describeDataUrls, isBinaryKey, redactSecretAssignments, redactSensitiveEntry, } from "./spanSanitizer.js";
|
|
4
5
|
import { capStrings, stringify } from "./spanSerialization.js";
|
|
@@ -29,6 +30,7 @@ export const ATTR = {
|
|
|
29
30
|
NL_COST: "cost",
|
|
30
31
|
NL_STRUCTURED_OUTPUT: "structured_output",
|
|
31
32
|
NL_HAS_FALLBACK: "has_fallback",
|
|
33
|
+
NL_INTERNAL: INTERNAL_SPAN_ATTRIBUTE,
|
|
32
34
|
MCP_SERVER_ID: "mcp.server_id",
|
|
33
35
|
MCP_TOOL_NAME: "mcp.tool_name",
|
|
34
36
|
MCP_TIMEOUT_MS: "mcp.timeout_ms",
|
|
@@ -133,6 +135,57 @@ export function spanAttributeMaxChars() {
|
|
|
133
135
|
}
|
|
134
136
|
return Math.max(SPAN_ATTRIBUTE_MIN_CHARS, configured);
|
|
135
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* @description Tells whether span inputs (prompts, messages and tool arguments) are recorded. `NEUROLINK_RECORD_INPUTS=false`, in any casing and with or without surrounding spaces, turns it off; any other value, or none, leaves it on, except inside `withoutSpanContent`. Read on every call, so a change applies to the next span.
|
|
140
|
+
* @returns {boolean} `false` when the operator opted out of recording inputs.
|
|
141
|
+
*/
|
|
142
|
+
export function recordsSpanInputs() {
|
|
143
|
+
return (recordsSpanContent() &&
|
|
144
|
+
process.env.NEUROLINK_RECORD_INPUTS?.trim().toLowerCase() !== "false");
|
|
145
|
+
}
|
|
146
|
+
const contentWithheld = new AsyncLocalStorage();
|
|
147
|
+
/**
|
|
148
|
+
* @description Runs `fn` with span content withheld: an observation recorded while it runs, or while anything it starts runs, keeps its model, usage, cost, level and timings but no input or output. The proxy uses it for a model call made on a client's behalf, whose content it records only when bodies are opted in.
|
|
149
|
+
* @param fn {() => T} - Work whose observations carry no content.
|
|
150
|
+
* @returns {T} Whatever `fn` returns.
|
|
151
|
+
*/
|
|
152
|
+
export function withoutSpanContent(fn) {
|
|
153
|
+
return contentWithheld.run(true, fn);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* @description Tells whether span input and output may be recorded here, i.e. the caller is not inside `withoutSpanContent`.
|
|
157
|
+
* @returns {boolean} `false` while content is withheld.
|
|
158
|
+
*/
|
|
159
|
+
export function recordsSpanContent() {
|
|
160
|
+
return contentWithheld.getStore() !== true;
|
|
161
|
+
}
|
|
162
|
+
// Strings this module returned as finished attributes. A caller that builds an
|
|
163
|
+
// attribute with `spanContentAttribute` or `spanModelRequestAttribute` and
|
|
164
|
+
// hands it to `observe` would otherwise have it redacted again, and a second
|
|
165
|
+
// pass over serialized JSON reads an escaped newline as part of a credential's
|
|
166
|
+
// value. Redacting a string that already came out of redaction cannot reveal
|
|
167
|
+
// anything new, so recognizing it is safe; a string that was evicted is simply
|
|
168
|
+
// redacted again, as before.
|
|
169
|
+
const finishedAttributes = new Set();
|
|
170
|
+
function finished(attribute) {
|
|
171
|
+
finishedAttributes.delete(attribute);
|
|
172
|
+
finishedAttributes.add(attribute);
|
|
173
|
+
if (finishedAttributes.size > MAX_REDACTED_ATTRIBUTES) {
|
|
174
|
+
const oldest = finishedAttributes.values().next().value;
|
|
175
|
+
if (oldest !== undefined) {
|
|
176
|
+
finishedAttributes.delete(oldest);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return attribute;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* @description Tells whether a string is an attribute that `spanContentAttribute` or `spanModelRequestAttribute` returned recently, so it is already redacted and capped.
|
|
183
|
+
* @param value {string} - Candidate attribute value.
|
|
184
|
+
* @returns {boolean} `true` when the string came out of one of those functions.
|
|
185
|
+
*/
|
|
186
|
+
export function isFinishedAttribute(value) {
|
|
187
|
+
return finishedAttributes.has(value);
|
|
188
|
+
}
|
|
136
189
|
/** Opening of the system prompt a model-request attribute always keeps. */
|
|
137
190
|
const SYSTEM_PROMPT_FLOOR_CHARS = 2_000;
|
|
138
191
|
/** Room the current question always gets in a model-request attribute. */
|
|
@@ -174,6 +227,29 @@ function redactText(text) {
|
|
|
174
227
|
});
|
|
175
228
|
return redacted;
|
|
176
229
|
}
|
|
230
|
+
// A name that says how many, how long or what kind of thing rather than
|
|
231
|
+
// holding the credential (`max_tokens`, `password_policy`, `credential_type`).
|
|
232
|
+
const MEASUREMENT_ENTRY_NAME = /(?:^|_)tokens$|_(?:policy|type|algorithm|count|limit)$/;
|
|
233
|
+
const NUMERIC_TEXT = /^\s*\d+(?:\.\d+)?\s*$/;
|
|
234
|
+
function snakeCased(name) {
|
|
235
|
+
return name
|
|
236
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
237
|
+
.replace(/[\s.-]+/g, "_")
|
|
238
|
+
.toLowerCase();
|
|
239
|
+
}
|
|
240
|
+
// Headers, environment variables and form fields are often lists of
|
|
241
|
+
// `{ name, value }` or `{ key, value }` entries; when the name is a credential
|
|
242
|
+
// the value is one, exactly as if it were stored under that key. A setting
|
|
243
|
+
// that merely mentions a credential, or a purely numeric value, is not one.
|
|
244
|
+
function credentialEntryName(node) {
|
|
245
|
+
const name = "name" in node ? node.name : "key" in node ? node.key : undefined;
|
|
246
|
+
if (typeof name !== "string" || !("value" in node) || !isSensitiveKey(name)) {
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
const isSetting = MEASUREMENT_ENTRY_NAME.test(snakeCased(name)) ||
|
|
250
|
+
(typeof node.value === "string" && NUMERIC_TEXT.test(node.value));
|
|
251
|
+
return isSetting ? undefined : name;
|
|
252
|
+
}
|
|
177
253
|
function redactNode(value, ancestors) {
|
|
178
254
|
if (typeof value === "string") {
|
|
179
255
|
return redactText(describeDataUrls(value));
|
|
@@ -198,17 +274,26 @@ function redactNode(value, ancestors) {
|
|
|
198
274
|
// No prototype, so a key named __proto__ is data rather than an assignment.
|
|
199
275
|
const redacted = Object.create(null);
|
|
200
276
|
const mimeType = declaredMimeType(value);
|
|
277
|
+
const entryName = credentialEntryName(value);
|
|
201
278
|
for (const [key, entry] of Object.entries(value)) {
|
|
202
|
-
|
|
279
|
+
// A key can carry a credential too, such as a URL with an api_key
|
|
280
|
+
// parameter used as a map key.
|
|
281
|
+
const safeKey = redactText(key);
|
|
282
|
+
const sensitiveName = key === "value" && entryName !== undefined
|
|
283
|
+
? entryName
|
|
284
|
+
: isSensitiveKey(key)
|
|
285
|
+
? key
|
|
286
|
+
: undefined;
|
|
287
|
+
if (sensitiveName !== undefined &&
|
|
203
288
|
(typeof entry === "string" ||
|
|
204
289
|
(typeof entry === "object" && entry !== null))) {
|
|
205
|
-
redacted[
|
|
290
|
+
redacted[safeKey] = redactSensitiveEntry(sensitiveName, entry);
|
|
206
291
|
continue;
|
|
207
292
|
}
|
|
208
293
|
const placeholder = isBinaryKey(key)
|
|
209
294
|
? binaryPlaceholder(entry, mimeType)
|
|
210
295
|
: undefined;
|
|
211
|
-
redacted[
|
|
296
|
+
redacted[safeKey] = placeholder ?? redactNode(entry, ancestors);
|
|
212
297
|
}
|
|
213
298
|
return redacted;
|
|
214
299
|
}
|
|
@@ -217,7 +302,7 @@ function redactNode(value, ancestors) {
|
|
|
217
302
|
}
|
|
218
303
|
}
|
|
219
304
|
/**
|
|
220
|
-
* @description Returns a copy of a value with credentials masked: values under keys such as `access_token`, `password`, `authorization` or `set-cookie`, bearer tokens, and `name=value` credentials inside text. Purely numeric values after a credential-looking name (e.g. `max_tokens: 4096`) are left alone.
|
|
305
|
+
* @description Returns a copy of a value with credentials masked: values under keys such as `access_token`, `password`, `authorization` or `set-cookie`, the value of a `{ name, value }` or `{ key, value }` entry whose name is such a key, bearer tokens, and `name=value` credentials inside text and inside object keys. Purely numeric values after a credential-looking name (e.g. `max_tokens: 4096`) are left alone, and so is an entry whose name ends in `tokens`, `policy`, `type`, `algorithm`, `count` or `limit` (a setting, not a credential).
|
|
221
306
|
* @param value {JsonValue | object} - Content to redact; objects and arrays are walked recursively.
|
|
222
307
|
* @returns {JsonValue | object} A redacted copy, serializing exactly as the input when nothing in it is sensitive.
|
|
223
308
|
* @throws {Error} When the value cannot be walked; tracing callers use `spanContentAttribute`, which turns that into an omitted value.
|
|
@@ -246,7 +331,7 @@ function redactSafely(value) {
|
|
|
246
331
|
export function spanContentAttribute(value, maxChars = spanAttributeMaxChars()) {
|
|
247
332
|
const redacted = redactSafely(value);
|
|
248
333
|
if (redacted === null || typeof redacted !== "object") {
|
|
249
|
-
return spanJsonAttribute(redacted, maxChars);
|
|
334
|
+
return finished(spanJsonAttribute(redacted, maxChars));
|
|
250
335
|
}
|
|
251
336
|
let serialized = stringify(redacted);
|
|
252
337
|
let budget = Math.floor(maxChars / 4);
|
|
@@ -254,7 +339,7 @@ export function spanContentAttribute(value, maxChars = spanAttributeMaxChars())
|
|
|
254
339
|
serialized = stringify(capStrings(redacted, budget));
|
|
255
340
|
budget = Math.floor(budget / 4);
|
|
256
341
|
}
|
|
257
|
-
return spanJsonAttribute(serialized, maxChars);
|
|
342
|
+
return finished(spanJsonAttribute(serialized, maxChars));
|
|
258
343
|
}
|
|
259
344
|
/**
|
|
260
345
|
* Serialize an arbitrary value for a span attribute, hard-capped at
|
|
@@ -491,9 +576,9 @@ export function spanModelRequestAttribute(rawRequest, maxChars = spanAttributeMa
|
|
|
491
576
|
...(prompt !== undefined ? [{ role: "user", content: prompt }] : []),
|
|
492
577
|
];
|
|
493
578
|
try {
|
|
494
|
-
return JSON.stringify(conversation);
|
|
579
|
+
return finished(JSON.stringify(conversation));
|
|
495
580
|
}
|
|
496
581
|
catch {
|
|
497
|
-
return spanJsonAttribute(conversation, maxChars);
|
|
582
|
+
return finished(spanJsonAttribute(conversation, maxChars));
|
|
498
583
|
}
|
|
499
584
|
}
|
|
@@ -5,6 +5,12 @@ import type { AssistantOutput } from "../types/index.js";
|
|
|
5
5
|
* @returns {string} The message role, or `user` when it declares none.
|
|
6
6
|
*/
|
|
7
7
|
export declare function messageRole<T>(message: T): string;
|
|
8
|
+
/**
|
|
9
|
+
* @description Removes the arguments of every tool call in assistant messages and keeps each call's name and id: OpenAI `tool_calls` and `function_call` items, Anthropic `tool_use` blocks, AI SDK `tool-call` parts and Gemini `functionCall` parts.
|
|
10
|
+
* @param messages {object[]} - Messages from `assistantMessages`.
|
|
11
|
+
* @returns {object[]} Copies of the messages without tool-call arguments.
|
|
12
|
+
*/
|
|
13
|
+
export declare function withoutToolArguments(messages: object[]): object[];
|
|
8
14
|
/**
|
|
9
15
|
* @description Shapes a model response as an assistant message array that Langfuse renders as a conversation.
|
|
10
16
|
* @param output {AssistantOutput | undefined} - A text string, a reply with `text`, `reasoning` and `toolCalls`, a message array, or provider content parts.
|
|
@@ -32,6 +32,43 @@ export function messageRole(message) {
|
|
|
32
32
|
? message.role
|
|
33
33
|
: "user";
|
|
34
34
|
}
|
|
35
|
+
const TOOL_CALL_TYPES = new Set([
|
|
36
|
+
"function",
|
|
37
|
+
"function_call",
|
|
38
|
+
"tool_use",
|
|
39
|
+
"server_tool_use",
|
|
40
|
+
"tool-call",
|
|
41
|
+
]);
|
|
42
|
+
const TOOL_ARGUMENT_KEYS = new Set(["arguments", "args", "input"]);
|
|
43
|
+
function stripToolArguments(value, isCall = false) {
|
|
44
|
+
if (Array.isArray(value)) {
|
|
45
|
+
return value.map((entry) => stripToolArguments(entry));
|
|
46
|
+
}
|
|
47
|
+
if (typeof value !== "object" || value === null) {
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
const call = isCall ||
|
|
51
|
+
("type" in value &&
|
|
52
|
+
typeof value.type === "string" &&
|
|
53
|
+
TOOL_CALL_TYPES.has(value.type));
|
|
54
|
+
return Object.fromEntries(Object.entries(value)
|
|
55
|
+
.filter(([key]) => !(call && TOOL_ARGUMENT_KEYS.has(key)))
|
|
56
|
+
.map(([key, entry]) => [
|
|
57
|
+
key,
|
|
58
|
+
stripToolArguments(entry, key === "function" || key === "functionCall"),
|
|
59
|
+
]));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* @description Removes the arguments of every tool call in assistant messages and keeps each call's name and id: OpenAI `tool_calls` and `function_call` items, Anthropic `tool_use` blocks, AI SDK `tool-call` parts and Gemini `functionCall` parts.
|
|
63
|
+
* @param messages {object[]} - Messages from `assistantMessages`.
|
|
64
|
+
* @returns {object[]} Copies of the messages without tool-call arguments.
|
|
65
|
+
*/
|
|
66
|
+
export function withoutToolArguments(messages) {
|
|
67
|
+
return messages.map((message) => Object.fromEntries(Object.entries(message).map(([key, value]) => [
|
|
68
|
+
key,
|
|
69
|
+
stripToolArguments(value),
|
|
70
|
+
])));
|
|
71
|
+
}
|
|
35
72
|
/**
|
|
36
73
|
* @description Shapes a model response as an assistant message array that Langfuse renders as a conversation.
|
|
37
74
|
* @param output {AssistantOutput | undefined} - A text string, a reply with `text`, `reasoning` and `toolCalls`, a message array, or provider content parts.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { tracers } from "./tracers.js";
|
|
2
2
|
export { withSpan, withClientSpan, withStreamSpan, withClientStreamSpan, StepSpans, withStepSpan, } from "./withSpan.js";
|
|
3
|
-
export { ATTR, SPAN_ATTRIBUTE_MAX_CHARS, redactSpanContent, spanAttributeMaxChars, spanContentAttribute, spanJsonAttribute, spanModelRequestAttribute, } from "./attributes.js";
|
|
3
|
+
export { ATTR, SPAN_ATTRIBUTE_MAX_CHARS, recordsSpanInputs, redactSpanContent, spanAttributeMaxChars, spanContentAttribute, spanJsonAttribute, spanModelRequestAttribute, } from "./attributes.js";
|
|
4
4
|
export { assistantMessages } from "./chatMessages.js";
|
|
5
5
|
export { observationLevel, observe, observeGeneration, observeTrace, usageDetails, } from "./observation.js";
|
|
6
6
|
export { generationName, stepSpanName, toolSpanName, } from "./observationNames.js";
|
package/dist/telemetry/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { tracers } from "./tracers.js";
|
|
2
2
|
export { withSpan, withClientSpan, withStreamSpan, withClientStreamSpan, StepSpans, withStepSpan, } from "./withSpan.js";
|
|
3
|
-
export { ATTR, SPAN_ATTRIBUTE_MAX_CHARS, redactSpanContent, spanAttributeMaxChars, spanContentAttribute, spanJsonAttribute, spanModelRequestAttribute, } from "./attributes.js";
|
|
3
|
+
export { ATTR, SPAN_ATTRIBUTE_MAX_CHARS, recordsSpanInputs, redactSpanContent, spanAttributeMaxChars, spanContentAttribute, spanJsonAttribute, spanModelRequestAttribute, } from "./attributes.js";
|
|
4
4
|
export { assistantMessages } from "./chatMessages.js";
|
|
5
5
|
export { observationLevel, observe, observeGeneration, observeTrace, usageDetails, } from "./observation.js";
|
|
6
6
|
export { generationName, stepSpanName, toolSpanName, } from "./observationNames.js";
|