@juspay/neurolink
Version:
The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002
113 lines (112 loc) • 5.39 kB
JavaScript
import { createObservationAttributes, createTraceAttributes, LangfuseOtelSpanAttributes, } from "@langfuse/tracing";
import { WARNING_FINISH_REASONS } from "../constants/telemetry.js";
import { calculateCost } from "../utils/pricing.js";
import { extractTokenUsage } from "../utils/tokenUtils.js";
import { ATTR, spanContentAttribute, spanModelRequestAttribute, } from "./attributes.js";
import { assistantMessages } from "./chatMessages.js";
function redactedIO(attrs) {
return {
...attrs,
...(attrs.input !== undefined && {
input: spanContentAttribute(attrs.input),
}),
...(attrs.output !== undefined && {
output: spanContentAttribute(attrs.output),
}),
};
}
/**
* @description Stamps Langfuse observation attributes on a span, plus a `gen_ai.observation.type` marker so the exporter pipeline recognizes it whatever its tracer scope. Input and output are credential-masked and capped at `SPAN_ATTRIBUTE_MAX_CHARS` before serialization.
* @param span {Span} - Span to describe.
* @param type {ObservationType} - Observation type Langfuse renders the span as.
* @param attrs {ObservationAttributes} - Input, output, model, parameters, usage, cost, level and metadata to record.
* @returns {void} Nothing.
*/
export function observe(span, type, attrs) {
span.setAttributes({
...createObservationAttributes(type, redactedIO(attrs)),
[ATTR.GEN_AI_OBSERVATION]: type,
});
}
/**
* @description Sets the trace-level input, output and metadata on a span, credential-masked and capped like `observe`. Each metadata entry is written as its own `langfuse.trace.metadata.<key>` attribute, redacted like input and output.
* @param span {Span} - Span whose trace is described.
* @param io {TraceContent} - Trace input, output and metadata.
* @returns {void} Nothing.
*/
export function observeTrace(span, io) {
const { metadata, ...content } = io;
span.setAttributes(createTraceAttributes(redactedIO(content)));
for (const [key, value] of Object.entries(metadata ?? {})) {
if (value !== undefined) {
span.setAttribute(`${LangfuseOtelSpanAttributes.TRACE_METADATA}.${key}`, spanContentAttribute(value));
}
}
}
/**
* @description Maps any provider's token usage onto Langfuse usage-detail keys.
* @param usage {RawUsageObject | undefined | null} - Token usage in any naming convention NeuroLink normalizes.
* @returns {Record<string, number>} `input`, `output` and `total`, plus `cache_read_input_tokens`, `cache_creation_input_tokens` and `reasoning` when reported.
*/
export function usageDetails(usage) {
const tokens = extractTokenUsage(usage, { calculateCacheSavings: false });
const details = {
input: tokens.input,
output: tokens.output,
total: tokens.total,
};
if (tokens.cacheReadTokens !== undefined) {
details.cache_read_input_tokens = tokens.cacheReadTokens;
}
if (tokens.cacheCreationTokens !== undefined) {
details.cache_creation_input_tokens = tokens.cacheCreationTokens;
}
if (tokens.reasoning !== undefined) {
details.reasoning = tokens.reasoning;
}
return details;
}
/**
* @description Picks the Langfuse level for a model call that finished without throwing.
* @param finishReason {string | undefined} - Provider finish reason, in any casing.
* @param aborted {boolean | undefined} - Whether the caller aborted or closed the call early.
* @returns {ObservationLevel | undefined} `"WARNING"` for truncated, filtered, aborted or empty output, otherwise `undefined`.
*/
export function observationLevel(finishReason, aborted) {
if (aborted ||
WARNING_FINISH_REASONS.has(finishReason?.toLowerCase() ?? "")) {
return "WARNING";
}
return undefined;
}
/**
* @description Records a model request on its span as a Langfuse generation: model, defined parameters, input, output, usage, cost and level. Safe to call once with the request and again with the response; unset fields leave earlier values in place.
* @param span {Span} - Span of one model request.
* @param call {GenerationRecord} - What was sent and what came back.
* @returns {void} Nothing.
*/
export function observeGeneration(span, call) {
const parameters = {};
for (const [key, value] of Object.entries(call.modelParameters ?? {})) {
if (value !== undefined) {
parameters[key] = value;
}
}
const pricedModel = call.model ?? call.responseModel;
const cost = call.usage && call.provider && pricedModel
? calculateCost(call.provider, pricedModel, extractTokenUsage(call.usage, { calculateCacheSavings: false }))
: 0;
observe(span, "generation", {
model: call.responseModel ?? call.model,
modelParameters: Object.keys(parameters).length > 0 ? parameters : undefined,
input: call.input === undefined
? undefined
: spanModelRequestAttribute(call.input),
output: assistantMessages(call.output),
usageDetails: call.usage ? usageDetails(call.usage) : undefined,
costDetails: cost > 0 ? { total: cost } : undefined,
completionStartTime: call.completionStartTime,
level: observationLevel(call.finishReason, call.aborted),
metadata: call.provider ? { provider: call.provider } : undefined,
});
}