openlit
Version:
OpenTelemetry-native Auto instrumentation library for monitoring LLM Applications, facilitating the integration of observability into your GenAI-driven projects
1,058 lines • 59.2 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.runWithFrameworkLlm = exports.OpenLITCallbackHandler = void 0;
const api_1 = require("@opentelemetry/api");
const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
const config_1 = __importDefault(require("../../config"));
const helpers_1 = __importStar(require("../../helpers"));
Object.defineProperty(exports, "runWithFrameworkLlm", { enumerable: true, get: function () { return helpers_1.runWithFrameworkLlm; } });
const constant_1 = require("../../constant");
const semantic_convention_1 = __importDefault(require("../../semantic-convention"));
const base_wrapper_1 = __importDefault(require("../base-wrapper"));
// ---------------------------------------------------------------------------
// Provider detection (mirrors Python PROVIDER_MAP)
// ---------------------------------------------------------------------------
const PROVIDER_MAP = {
anthropic: 'anthropic',
azure: 'azure',
bedrock: 'aws.bedrock',
bedrock_converse: 'aws.bedrock',
cohere: 'cohere',
google: 'google',
google_genai: 'google',
google_vertexai: 'google',
groq: 'groq',
mistralai: 'mistral_ai',
ollama: 'ollama',
openai: 'openai',
together: 'together',
vertexai: 'google',
fireworks: 'fireworks',
perplexity: 'perplexity',
huggingface: 'huggingface',
deepinfra: 'deepinfra',
anyscale: 'anyscale',
};
function detectProvider(serialized) {
if (!serialized)
return semantic_convention_1.default.GEN_AI_SYSTEM_LANGCHAIN;
const classId = serialized.id || [];
if (Array.isArray(classId)) {
const classPath = classId.join('.').toLowerCase();
for (const [key, val] of Object.entries(PROVIDER_MAP)) {
if (classPath.includes(key))
return val;
}
}
return semantic_convention_1.default.GEN_AI_SYSTEM_LANGCHAIN;
}
// ---------------------------------------------------------------------------
// Model name extraction (mirrors Python extract_model_name)
// ---------------------------------------------------------------------------
const MODEL_PATHS_BY_ID = [
['ChatGoogleGenerativeAI', ['kwargs', 'model'], 'serialized'],
['ChatVertexAI', ['kwargs', 'model_name'], 'serialized'],
['ChatMistralAI', ['kwargs', 'model'], 'serialized'],
['OpenAI', ['invocation_params', 'model_name'], 'kwargs'],
['ChatOpenAI', ['invocation_params', 'model_name'], 'kwargs'],
['AzureChatOpenAI', ['invocation_params', 'model'], 'kwargs'],
['AzureChatOpenAI', ['invocation_params', 'model_name'], 'kwargs'],
['AzureChatOpenAI', ['invocation_params', 'azure_deployment'], 'kwargs'],
['HuggingFacePipeline', ['invocation_params', 'model_id'], 'kwargs'],
['BedrockChat', ['kwargs', 'model_id'], 'serialized'],
['Bedrock', ['kwargs', 'model_id'], 'serialized'],
['BedrockLLM', ['kwargs', 'model_id'], 'serialized'],
['ChatBedrock', ['kwargs', 'model_id'], 'serialized'],
['ChatBedrockConverse', ['kwargs', 'model_id'], 'serialized'],
['LlamaCpp', ['invocation_params', 'model_path'], 'kwargs'],
['WatsonxLLM', ['invocation_params', 'model_id'], 'kwargs'],
];
const MODEL_PATTERNS = [
['ChatAnthropic', 'model', 'anthropic'],
['Anthropic', 'model', 'anthropic'],
['ChatTongyi', 'model_name', null],
['ChatCohere', 'model', null],
['Cohere', 'model', null],
['HuggingFaceHub', 'model', null],
['ChatAnyscale', 'model_name', null],
['TextGen', 'model', 'text-gen'],
['Ollama', 'model', null],
['OllamaLLM', 'model', null],
['ChatOllama', 'model', null],
['ChatFireworks', 'model', null],
['ChatPerplexity', 'model', null],
['VLLM', 'model', null],
['Xinference', 'model_uid', null],
['ChatOCIGenAI', 'model_id', null],
['DeepInfra', 'model_id', null],
];
const FALLBACK_PATHS = [
[['kwargs', 'model_name'], 'serialized'],
[['kwargs', 'model'], 'serialized'],
[['kwargs', 'model_id'], 'serialized'],
[['invocation_params', 'model_name'], 'kwargs'],
[['invocation_params', 'model'], 'kwargs'],
[['invocation_params', 'model_id'], 'kwargs'],
];
function _extractByPath(serialized, kwargs, keys, selectFrom) {
let obj = selectFrom === 'kwargs' ? kwargs : serialized;
if (obj == null)
return null;
for (const key of keys) {
if (typeof obj === 'object' && obj !== null) {
obj = obj[key];
}
else {
return null;
}
if (obj == null)
return null;
}
return obj ? String(obj) : null;
}
function _getClassName(serialized) {
if (!serialized)
return null;
const id = serialized.id;
if (Array.isArray(id) && id.length > 0)
return id[id.length - 1];
return null;
}
function _extractModelFromRepr(serialized, pattern) {
if (!serialized)
return null;
const repr = serialized.repr || '';
if (repr) {
const re = new RegExp(`${pattern}='(.*?)'`);
const match = re.exec(repr);
if (match)
return match[1];
}
return null;
}
function extractModelName(serialized, kwargs) {
const className = _getClassName(serialized);
for (const [modelId, keys, selectFrom] of MODEL_PATHS_BY_ID) {
if (className === modelId) {
const result = _extractByPath(serialized, kwargs, keys, selectFrom);
if (result)
return result;
}
}
for (const [modelId, pattern, defaultVal] of MODEL_PATTERNS) {
if (className === modelId) {
const result = _extractModelFromRepr(serialized, pattern);
if (result)
return result;
if (defaultVal)
return defaultVal;
}
}
for (const [keys, selectFrom] of FALLBACK_PATHS) {
const result = _extractByPath(serialized, kwargs, keys, selectFrom);
if (result)
return result;
}
return className || 'unknown';
}
function extractModelParameters(kwargs) {
const params = {};
const ip = kwargs?.invocation_params || {};
const paramKeys = [
'temperature', 'max_tokens', 'max_completion_tokens',
'top_p', 'top_k', 'frequency_penalty', 'presence_penalty',
'request_timeout', 'stop_sequences', 'seed',
];
for (const key of paramKeys) {
if (ip[key] != null)
params[key] = ip[key];
}
return params;
}
// ---------------------------------------------------------------------------
// Chain type detection (mirrors Python)
// ---------------------------------------------------------------------------
const SKIP_CHAIN_CLASS_PREFIXES = new Set([
'RunnableSequence', 'RunnableParallel', 'RunnableLambda',
'RunnablePassthrough', 'RunnableAssign', 'RunnablePick',
'RunnableBranch', 'RunnableEach', 'Prompt', 'PromptTemplate',
'ChatPromptTemplate', 'MessagesPlaceholder',
'SystemMessagePromptTemplate', 'HumanMessagePromptTemplate',
'AIMessagePromptTemplate', 'BasePromptTemplate',
'StrOutputParser', 'JsonOutputParser', 'PydanticOutputParser',
]);
function isInternalChain(serialized, name) {
if (serialized?.id) {
const classPath = serialized.id;
if (Array.isArray(classPath) && classPath.length > 0) {
const cn = String(classPath[classPath.length - 1]);
if (SKIP_CHAIN_CLASS_PREFIXES.has(cn))
return true;
if (cn.startsWith('Runnable'))
return true;
}
}
if (SKIP_CHAIN_CLASS_PREFIXES.has(name))
return true;
return false;
}
function detectObservationType(serialized, callbackType, name) {
if (callbackType === 'tool')
return 'tool';
if (callbackType === 'retriever')
return 'retriever';
if (callbackType === 'llm')
return 'generation';
if (callbackType === 'chain') {
if (serialized?.id) {
const classPath = serialized.id;
if (classPath.some((part) => String(part).toLowerCase().includes('agent'))) {
return 'agent';
}
}
if (name && name.toLowerCase().includes('agent'))
return 'agent';
return 'chain';
}
return 'span';
}
// ---------------------------------------------------------------------------
// Message formatting helpers (mirrors Python utils.py)
// ---------------------------------------------------------------------------
const ROLE_MAP = {
system: 'system',
human: 'user',
ai: 'assistant',
tool: 'tool',
function: 'tool',
};
function buildInputMessagesFromLangChain(messages) {
try {
const structured = [];
for (const msgList of messages) {
for (const msg of msgList) {
const role = msg._getType?.() || msg.type || 'user';
const content = msg.content ?? String(msg);
const otelRole = ROLE_MAP[role] || 'user';
structured.push({ role: otelRole, parts: buildParts(content) });
}
}
return structured;
}
catch {
return [];
}
}
function buildInputMessagesFromPrompts(prompts) {
try {
return prompts.map(p => ({
role: 'user',
parts: [{ type: 'text', content: typeof p === 'string' ? p : String(p) }],
}));
}
catch {
return [];
}
}
function buildInputMessages(messagesOrPrompts) {
if (!messagesOrPrompts)
return [];
if (Array.isArray(messagesOrPrompts) && messagesOrPrompts.length > 0) {
const first = messagesOrPrompts[0];
if (typeof first === 'string')
return buildInputMessagesFromPrompts(messagesOrPrompts);
if (Array.isArray(first) && first.length > 0 && first[0]?.content !== undefined) {
return buildInputMessagesFromLangChain(messagesOrPrompts);
}
return buildInputMessagesFromPrompts(messagesOrPrompts);
}
return [];
}
function buildParts(content) {
if (typeof content === 'string')
return [{ type: 'text', content }];
if (Array.isArray(content)) {
const parts = [];
for (const part of content) {
if (typeof part === 'string') {
parts.push({ type: 'text', content: part });
}
else if (typeof part === 'object' && part !== null) {
const ptype = part.type || 'text';
if (ptype === 'text') {
parts.push({ type: 'text', content: part.text || '' });
}
else if (ptype === 'image_url') {
const url = part.image_url;
if (typeof url === 'string')
parts.push({ type: 'image', url });
else if (url?.url)
parts.push({ type: 'image', url: url.url });
}
else {
parts.push({ type: ptype, content: String(part) });
}
}
}
return parts.length > 0 ? parts : [{ type: 'text', content: '' }];
}
return [{ type: 'text', content: String(content) }];
}
function shouldCaptureMessageContent() {
return config_1.default.captureMessageContent ?? config_1.default.traceContent ?? true;
}
function normalizeToolCalls(rawToolCalls) {
return rawToolCalls.map((call) => ({
id: call?.id || call?.tool_call_id || '',
type: call?.type || 'function',
name: call?.name || call?.function?.name || '',
arguments: call?.args ?? call?.arguments ?? call?.function?.arguments ?? {},
}));
}
function stringifyToolCallArgument(value) {
if (typeof value === 'string')
return value;
try {
return JSON.stringify(value ?? {});
}
catch {
return '[unserializable]';
}
}
// ---------------------------------------------------------------------------
// Conversation ID extraction (mirrors Python _resolve_conversation_id)
// ---------------------------------------------------------------------------
function resolveConversationId(metadata) {
if (!metadata)
return null;
for (const key of ['thread_id', 'conversation_id', 'session_id']) {
if (metadata[key])
return String(metadata[key]);
}
const configurable = metadata.configurable || {};
for (const key of ['thread_id', 'conversation_id']) {
if (configurable[key])
return String(configurable[key]);
}
return null;
}
// ---------------------------------------------------------------------------
// Token calculation (approximate, mirrors Python general_tokens)
// ---------------------------------------------------------------------------
function generalTokens(text) {
return Math.ceil(text.length / 2);
}
// ---------------------------------------------------------------------------
// Callback Handler
// ---------------------------------------------------------------------------
let handlerInstance = null;
class OpenLITCallbackHandler {
constructor(tracer) {
this.name = 'openlit_callback_handler';
this.lc_serializable = false;
this.awaitHandlers = false;
this.spans = new Map();
this.skippedRuns = new Map();
this.tracer = tracer;
}
// ---- Helpers -----------------------------------------------------------
_getNameFromCallback(serialized, kwargs) {
if (kwargs?.name)
return kwargs.name;
if (serialized) {
if (serialized.kwargs?.name)
return serialized.kwargs.name;
if (serialized.name)
return serialized.name;
if (serialized.id) {
const id = serialized.id;
if (Array.isArray(id) && id.length > 0)
return id[id.length - 1];
}
}
return 'unknown';
}
_resolveParentRunId(parentRunId) {
const visited = new Set();
let current = parentRunId;
while (current && this.skippedRuns.has(current)) {
if (visited.has(current))
break;
visited.add(current);
current = this.skippedRuns.get(current);
}
return current;
}
_getParentContext(parentRunId) {
const resolved = this._resolveParentRunId(parentRunId);
if (!resolved)
return undefined;
const holder = this.spans.get(resolved);
if (!holder)
return undefined;
return api_1.trace.setSpan(api_1.context.active(), holder.span);
}
_createSpan(runId, parentRunId, spanName, kind = api_1.SpanKind.INTERNAL) {
const resolved = this._resolveParentRunId(parentRunId);
let parentContext;
if (resolved && this.spans.has(resolved)) {
parentContext = api_1.trace.setSpan(api_1.context.active(), this.spans.get(resolved).span);
}
return this.tracer.startSpan(spanName, { kind }, parentContext);
}
_newHolder(span, parentRunId) {
return {
span,
startTime: Date.now(),
modelName: 'unknown',
modelParameters: {},
provider: '',
serverAddress: '',
serverPort: 0,
parentRunId,
children: [],
streamingContent: [],
tokenTimestamps: [],
inputTokens: 0,
outputTokens: 0,
cacheReadInputTokens: 0,
cacheCreationInputTokens: 0,
promptContent: '',
inputMessagesRaw: null,
prompts: [],
inputMessagesStructured: [],
systemInstructions: null,
toolDefinitions: null,
toolCalls: null,
finishReason: 'stop',
isAgentChain: false,
responseId: null,
suppressionActive: false,
};
}
_setCommonAttributes(span, operationType) {
span.setAttribute(semantic_convention_1.default.GEN_AI_OPERATION, operationType);
span.setAttribute(semantic_conventions_1.ATTR_TELEMETRY_SDK_NAME, constant_1.SDK_NAME);
span.setAttribute(semantic_convention_1.default.ATTR_DEPLOYMENT_ENVIRONMENT, config_1.default.environment || 'default');
span.setAttribute(semantic_conventions_1.ATTR_SERVICE_NAME, config_1.default.applicationName || 'default');
span.setAttribute(semantic_convention_1.default.GEN_AI_SDK_VERSION, constant_1.SDK_VERSION);
}
_setModelParameters(span, params) {
if (params.temperature != null)
span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_TEMPERATURE, params.temperature);
if (params.max_tokens != null)
span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_MAX_TOKENS, params.max_tokens);
if (params.max_completion_tokens != null)
span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_MAX_TOKENS, params.max_completion_tokens);
if (params.top_p != null)
span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_TOP_P, params.top_p);
if (params.top_k != null)
span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_TOP_K, params.top_k);
if (params.frequency_penalty != null)
span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_FREQUENCY_PENALTY, params.frequency_penalty);
if (params.presence_penalty != null)
span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_PRESENCE_PENALTY, params.presence_penalty);
if (params.seed != null)
span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_SEED, Number(params.seed));
const stop = params.stop || params.stop_sequences;
if (stop) {
span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_STOP_SEQUENCES, Array.isArray(stop) ? stop : [stop]);
}
}
_endSpan(runId, error) {
const holder = this.spans.get(runId);
if (!holder)
return;
for (const childId of holder.children || []) {
const child = this.spans.get(childId);
if (child) {
try {
child.span.end();
}
catch { /* already ended */ }
}
}
if (error) {
holder.span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error });
}
else {
holder.span.setStatus({ code: api_1.SpanStatusCode.OK });
}
holder.span.end();
this.spans.delete(runId);
}
// ---- Chain Callbacks ---------------------------------------------------
handleChainStart(chain, inputs, runId, parentRunId, _tags, metadata, _runType, name) {
try {
const resolvedName = this._getNameFromCallback(chain, { name });
const obsType = detectObservationType(chain, 'chain', resolvedName);
if (obsType !== 'agent' && isInternalChain(chain, resolvedName)) {
this.skippedRuns.set(runId, parentRunId);
return;
}
if (obsType !== 'agent' && (0, helpers_1.isLangGraphActive)() && !parentRunId) {
this.skippedRuns.set(runId, parentRunId);
return;
}
if (obsType !== 'agent' && parentRunId) {
this.skippedRuns.set(runId, parentRunId);
return;
}
const operationType = obsType === 'agent'
? semantic_convention_1.default.GEN_AI_OPERATION_TYPE_AGENT
: semantic_convention_1.default.GEN_AI_OPERATION_TYPE_FRAMEWORK;
const spanName = obsType === 'agent'
? `invoke_agent ${resolvedName}`
: `invoke_workflow ${resolvedName}`;
const span = this._createSpan(runId, parentRunId, spanName);
const holder = this._newHolder(span, parentRunId);
if (obsType === 'agent')
holder.isAgentChain = true;
this.spans.set(runId, holder);
if (parentRunId) {
const parentResolved = this._resolveParentRunId(parentRunId);
if (parentResolved && this.spans.has(parentResolved)) {
this.spans.get(parentResolved).children.push(runId);
}
}
span.setAttribute(semantic_convention_1.default.GEN_AI_PROVIDER_NAME_OTEL, semantic_convention_1.default.GEN_AI_SYSTEM_LANGCHAIN);
this._setCommonAttributes(span, operationType);
if (obsType === 'agent') {
span.setAttribute(semantic_convention_1.default.GEN_AI_AGENT_NAME, resolvedName);
span.setAttribute(semantic_convention_1.default.GEN_AI_AGENT_ID, runId);
}
else {
span.setAttribute(semantic_convention_1.default.GEN_AI_WORKFLOW_NAME, resolvedName);
}
const convId = resolveConversationId(metadata);
if (convId)
span.setAttribute(semantic_convention_1.default.GEN_AI_CONVERSATION_ID, convId);
if (config_1.default.captureMessageContent && inputs) {
try {
const inputStr = typeof inputs === 'string' ? inputs : JSON.stringify(inputs);
span.setAttribute(semantic_convention_1.default.GEN_AI_WORKFLOW_INPUT, inputStr.slice(0, 2000));
}
catch { /* non-blocking */ }
}
(0, helpers_1.applyCustomSpanAttributes)(span);
}
catch { /* non-blocking */ }
}
handleChainEnd(outputs, runId) {
try {
this.skippedRuns.delete(runId);
const holder = this.spans.get(runId);
if (!holder)
return;
const duration = (Date.now() - holder.startTime) / 1000;
holder.span.setAttribute(semantic_convention_1.default.GEN_AI_CLIENT_OPERATION_DURATION, duration);
if (config_1.default.captureMessageContent && outputs) {
try {
const outputStr = typeof outputs === 'string' ? outputs : JSON.stringify(outputs);
holder.span.setAttribute(semantic_convention_1.default.GEN_AI_WORKFLOW_OUTPUT, outputStr.slice(0, 2000));
}
catch { /* non-blocking */ }
}
this._endSpan(runId);
}
catch { /* non-blocking */ }
}
handleChainError(error, runId) {
try {
this.skippedRuns.delete(runId);
if (this.spans.has(runId)) {
const span = this.spans.get(runId).span;
const errorType = error?.constructor?.name || '_OTHER';
span.setAttribute(semantic_convention_1.default.ERROR_TYPE, errorType);
}
this._endSpan(runId, String(error));
}
catch { /* non-blocking */ }
}
// ---- LLM Callbacks -----------------------------------------------------
handleChatModelStart(llm, messages, runId, parentRunId, _extraParams, _tags, metadata, kwargs) {
try {
const modelName = extractModelName(llm, kwargs || {});
const modelParams = extractModelParameters(kwargs || {});
const provider = detectProvider(llm);
const spanName = `chat ${modelName}`;
const span = this._createSpan(runId, parentRunId, spanName, api_1.SpanKind.CLIENT);
const holder = this._newHolder(span, parentRunId);
holder.modelName = modelName;
holder.modelParameters = modelParams;
holder.provider = provider;
holder.suppressionActive = true;
(0, helpers_1.setFrameworkLlmActive)();
(0, helpers_1.setFrameworkParentContext)(api_1.trace.setSpan(api_1.context.active(), span));
this.spans.set(runId, holder);
if (parentRunId) {
const parentResolved = this._resolveParentRunId(parentRunId);
if (parentResolved && this.spans.has(parentResolved)) {
this.spans.get(parentResolved).children.push(runId);
}
}
span.setAttribute(semantic_convention_1.default.GEN_AI_PROVIDER_NAME_OTEL, provider);
this._setCommonAttributes(span, semantic_convention_1.default.GEN_AI_OPERATION_TYPE_CHAT);
span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_MODEL, modelName);
this._setModelParameters(span, modelParams);
const convId = resolveConversationId(metadata);
if (convId)
span.setAttribute(semantic_convention_1.default.GEN_AI_CONVERSATION_ID, convId);
// Server address from invocation_params or provider defaults
const ip = kwargs?.invocation_params || {};
const apiBase = ip.api_base || ip.base_url;
if (apiBase) {
try {
const url = new URL(apiBase);
holder.serverAddress = url.hostname || '';
holder.serverPort = url.port ? Number(url.port) : 443;
}
catch { /* ignore */ }
}
if (!holder.serverAddress) {
const [defaultHost, defaultPort] = (0, helpers_1.getServerAddressForProvider)(provider);
if (defaultHost) {
holder.serverAddress = defaultHost;
holder.serverPort = defaultPort;
}
}
if (messages) {
const formatted = [];
for (const msgList of messages) {
for (const msg of msgList) {
const role = msg._getType?.() || msg.type || 'unknown';
const content = msg.content ?? String(msg);
formatted.push(`${role}: ${content}`);
}
}
const promptStr = formatted.join('\n');
holder.promptContent = promptStr;
holder.inputTokens = generalTokens(promptStr);
holder.inputMessagesRaw = messages;
if (config_1.default.captureMessageContent) {
// System instructions
const sysInstructions = [];
for (const msgList of messages) {
for (const msg of msgList) {
const role = msg._getType?.() || msg.type || '';
if (role === 'system') {
const content = msg.content ?? '';
if (content)
sysInstructions.push({ type: 'text', content: String(content) });
}
}
}
if (sysInstructions.length > 0) {
holder.systemInstructions = sysInstructions;
span.setAttribute(semantic_convention_1.default.GEN_AI_SYSTEM_INSTRUCTIONS, JSON.stringify(sysInstructions));
}
// Tool definitions
const tools = ip.tools || ip.functions;
if (tools && Array.isArray(tools)) {
holder.toolDefinitions = tools;
span.setAttribute(semantic_convention_1.default.GEN_AI_TOOL_DEFINITIONS, JSON.stringify(tools));
}
// Structured input messages
try {
holder.inputMessagesStructured = buildInputMessages(messages);
}
catch { /* non-blocking */ }
}
}
(0, helpers_1.applyCustomSpanAttributes)(span);
}
catch { /* non-blocking */ }
}
handleLLMStart(serialized, prompts, runId, parentRunId, _extraParams, _tags, metadata, kwargs) {
try {
const modelName = extractModelName(serialized, kwargs || {});
const modelParams = extractModelParameters(kwargs || {});
const provider = detectProvider(serialized);
const spanName = `chat ${modelName}`;
const span = this._createSpan(runId, parentRunId, spanName, api_1.SpanKind.CLIENT);
const holder = this._newHolder(span, parentRunId);
holder.modelName = modelName;
holder.modelParameters = modelParams;
holder.provider = provider;
holder.prompts = prompts || [];
holder.suppressionActive = true;
(0, helpers_1.setFrameworkLlmActive)();
(0, helpers_1.setFrameworkParentContext)(api_1.trace.setSpan(api_1.context.active(), span));
this.spans.set(runId, holder);
if (parentRunId) {
const parentResolved = this._resolveParentRunId(parentRunId);
if (parentResolved && this.spans.has(parentResolved)) {
this.spans.get(parentResolved).children.push(runId);
}
}
span.setAttribute(semantic_convention_1.default.GEN_AI_PROVIDER_NAME_OTEL, provider);
this._setCommonAttributes(span, semantic_convention_1.default.GEN_AI_OPERATION_TYPE_CHAT);
span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_MODEL, modelName);
this._setModelParameters(span, modelParams);
const convId = resolveConversationId(metadata);
if (convId)
span.setAttribute(semantic_convention_1.default.GEN_AI_CONVERSATION_ID, convId);
// Server address
const ip = kwargs?.invocation_params || {};
const apiBase = ip.api_base || ip.base_url;
if (apiBase) {
try {
const url = new URL(apiBase);
holder.serverAddress = url.hostname || '';
holder.serverPort = url.port ? Number(url.port) : 443;
}
catch { /* ignore */ }
}
if (!holder.serverAddress) {
const [defaultHost, defaultPort] = (0, helpers_1.getServerAddressForProvider)(provider);
if (defaultHost) {
holder.serverAddress = defaultHost;
holder.serverPort = defaultPort;
}
}
if (prompts && prompts.length > 0) {
const promptStr = prompts.join('\n');
holder.inputTokens = generalTokens(promptStr);
if (config_1.default.captureMessageContent) {
try {
holder.inputMessagesStructured = buildInputMessages(prompts);
}
catch { /* non-blocking */ }
}
}
(0, helpers_1.applyCustomSpanAttributes)(span);
}
catch { /* non-blocking */ }
}
handleLLMNewToken(token, _idx, runId, _chunk) {
try {
const holder = this.spans.get(runId);
if (!holder)
return;
const now = Date.now();
if (!holder.firstTokenTime)
holder.firstTokenTime = now;
holder.tokenTimestamps.push(now);
if (token)
holder.streamingContent.push(token);
}
catch { /* non-blocking */ }
}
handleLLMEnd(output, runId) {
try {
const holder = this.spans.get(runId);
if (!holder)
return;
const { span, startTime, modelName, modelParameters = {}, streamingContent = [], tokenTimestamps = [], firstTokenTime, } = holder;
const endTime = Date.now();
const duration = (endTime - startTime) / 1000;
span.setAttribute(semantic_convention_1.default.GEN_AI_CLIENT_OPERATION_DURATION, duration);
const isStreaming = streamingContent.length > 0;
let ttft = 0;
let tbt = 0;
if (isStreaming) {
if (firstTokenTime)
ttft = (firstTokenTime - startTime) / 1000;
if (tokenTimestamps.length > 1) {
const diffs = tokenTimestamps.slice(1).map((t, i) => t - tokenTimestamps[i]);
tbt = diffs.reduce((a, b) => a + b, 0) / diffs.length / 1000;
}
}
else {
ttft = duration;
}
// Extract tokens and content from output
let inputTokens = holder.inputTokens;
let outputTokens = 0;
let completionContent = streamingContent.join('');
let responseModel = modelName;
let responseId = null;
// From llm_output (top-level)
if (output?.llm_output) {
const lu = output.llm_output;
const tu = lu.token_usage || lu.usage || {};
inputTokens = tu.prompt_tokens || tu.input_tokens || inputTokens;
outputTokens = tu.completion_tokens || tu.output_tokens || outputTokens;
responseModel = lu.model_name || lu.model || modelName;
responseId = lu.id || null;
// Cache token extraction
const promptDetails = tu.prompt_tokens_details || tu.input_tokens_details || {};
let cached = promptDetails.cached_tokens || 0;
const inputDetails = tu.input_tokens_details || {};
let creation = inputDetails.cache_creation_tokens || 0;
const langchainInput = tu.input_token_details || {};
if (!cached)
cached = langchainInput.cache_read || 0;
if (!creation)
creation = langchainInput.cache_creation || 0;
holder.cacheReadInputTokens = cached;
holder.cacheCreationInputTokens = creation;
}
// From generations
const generations = output?.generations || [];
for (const genList of generations) {
for (const gen of (Array.isArray(genList) ? genList : [genList])) {
const msg = gen?.message || gen;
// Usage from usage_metadata
const um = msg?.usage_metadata;
if (um) {
if (!inputTokens)
inputTokens = um.input_tokens || um.prompt_tokens || 0;
if (!outputTokens)
outputTokens = um.output_tokens || um.completion_tokens || 0;
// Cache tokens from usage_metadata
const pd = um.prompt_tokens_details || um.input_tokens_details || {};
if (!holder.cacheReadInputTokens)
holder.cacheReadInputTokens = pd.cached_tokens || 0;
const langchainInput = um.input_token_details || {};
if (!holder.cacheReadInputTokens)
holder.cacheReadInputTokens = langchainInput.cache_read || 0;
if (!holder.cacheCreationInputTokens)
holder.cacheCreationInputTokens = langchainInput.cache_creation || 0;
}
// Token usage from response_metadata
if (msg?.response_metadata) {
const rm = msg.response_metadata;
const tokenUsage = rm.token_usage;
if (tokenUsage) {
inputTokens = tokenUsage.prompt_tokens || tokenUsage.input_tokens || inputTokens;
outputTokens = tokenUsage.completion_tokens || tokenUsage.output_tokens || outputTokens;
}
if (rm.usage) {
inputTokens = rm.usage.inputTokens || rm.usage.input_tokens || inputTokens;
outputTokens = rm.usage.outputTokens || rm.usage.output_tokens || outputTokens;
}
if (rm['amazon-bedrock-invocationMetrics']) {
const bm = rm['amazon-bedrock-invocationMetrics'];
inputTokens = bm.inputTokenCount || inputTokens;
outputTokens = bm.outputTokenCount || outputTokens;
}
}
// Content
const genContent = gen?.text || msg?.content;
if (genContent && typeof genContent === 'string' && genContent.length > completionContent.length) {
completionContent = genContent;
}
// Finish reason
const fr = gen?.generationInfo?.finish_reason || msg?.response_metadata?.finish_reason;
if (fr)
holder.finishReason = fr;
// Tool calls. Keep last-writer-wins semantics across generations so
// choices are not merged into one assistant message.
const tc = msg?.tool_calls || msg?.additional_kwargs?.tool_calls || gen?.message?.tool_calls;
if (tc && Array.isArray(tc) && tc.length > 0) {
holder.toolCalls = normalizeToolCalls(tc);
}
}
}
// Fallback token estimation
if (!outputTokens && completionContent)
outputTokens = generalTokens(completionContent);
if (!inputTokens && holder.promptContent)
inputTokens = generalTokens(holder.promptContent);
// Cost
const pricingInfo = config_1.default.pricingInfo || {};
const cost = helpers_1.default.getChatModelCost(modelName, pricingInfo, inputTokens, outputTokens);
// Provider for span attributes
const provider = holder.provider || semantic_convention_1.default.GEN_AI_SYSTEM_LANGCHAIN;
const serverAddress = holder.serverAddress || '';
const serverPort = holder.serverPort || 0;
// Set span attributes
span.setAttribute(semantic_convention_1.default.GEN_AI_RESPONSE_MODEL, responseModel);
span.setAttribute(semantic_convention_1.default.GEN_AI_USAGE_INPUT_TOKENS, inputTokens);
span.setAttribute(semantic_convention_1.default.GEN_AI_USAGE_OUTPUT_TOKENS, outputTokens);
span.setAttribute(semantic_convention_1.default.GEN_AI_CLIENT_TOKEN_USAGE, inputTokens + outputTokens);
span.setAttribute(semantic_convention_1.default.GEN_AI_USAGE_COST, cost);
span.setAttribute(semantic_convention_1.default.GEN_AI_RESPONSE_FINISH_REASON, [holder.finishReason]);
span.setAttribute(semantic_convention_1.default.GEN_AI_OUTPUT_TYPE, typeof completionContent === 'string' ? 'text' : 'json');
span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_IS_STREAM, isStreaming);
if (responseId)
span.setAttribute(semantic_convention_1.default.GEN_AI_RESPONSE_ID, responseId);
if (serverAddress) {
span.setAttribute(semantic_convention_1.default.SERVER_ADDRESS, serverAddress);
span.setAttribute(semantic_convention_1.default.SERVER_PORT, serverPort);
}
if (isStreaming && ttft > 0) {
span.setAttribute(semantic_convention_1.default.GEN_AI_SERVER_TTFT, ttft);
if (tbt > 0)
span.setAttribute(semantic_convention_1.default.GEN_AI_SERVER_TBT, tbt);
}
// Cache tokens
if (holder.cacheReadInputTokens) {
span.setAttribute(semantic_convention_1.default.GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, holder.cacheReadInputTokens);
}
if (holder.cacheCreationInputTokens) {
span.setAttribute(semantic_convention_1.default.GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, holder.cacheCreationInputTokens);
}
// Tool calls on span
if (holder.toolCalls && holder.toolCalls.length > 0) {
const names = holder.toolCalls.map((t) => t.name || t.function?.name || '').filter(Boolean);
const ids = holder.toolCalls.map((t) => t.id || '').filter(Boolean);
const args = holder.toolCalls.map((t) => stringifyToolCallArgument(t.arguments ?? t.function?.arguments));
if (names.length)
span.setAttribute(semantic_convention_1.default.GEN_AI_TOOL_NAME, names.join(', '));
if (ids.length)
span.setAttribute(semantic_convention_1.default.GEN_AI_TOOL_CALL_ID, ids.join(', '));
if (args.length)
span.setAttribute(semantic_convention_1.default.GEN_AI_TOOL_CALL_ARGUMENTS, args);
}
let outputMessagesJson = null;
// Content attributes (gated by captureMessageContent)
if (shouldCaptureMessageContent()) {
const inputRaw = holder.inputMessagesRaw || holder.prompts || [];
const inputMessagesStructured = holder.inputMessagesStructured || [];
const inputMsgs = inputMessagesStructured.length > 0
? inputMessagesStructured
: buildInputMessages(inputRaw);
const outputToolCalls = holder.toolCalls && holder.toolCalls.length > 0 ? holder.toolCalls : undefined;
outputMessagesJson = helpers_1.default.buildOutputMessages(completionContent, holder.finishReason, outputToolCalls);
if (inputMsgs.length > 0)
span.setAttribute(semantic_convention_1.default.GEN_AI_INPUT_MESSAGES, JSON.stringify(inputMsgs));
if (outputMessagesJson && (outputMessagesJson !== '[]' || completionContent || outputToolCalls)) {
span.setAttribute(semantic_convention_1.default.GEN_AI_OUTPUT_MESSAGES, outputMessagesJson);
}
}
// Emit inference event (always emitted; content within is gated)
const eventAttrs = {
[semantic_convention_1.default.GEN_AI_OPERATION]: semantic_convention_1.default.GEN_AI_OPERATION_TYPE_CHAT,
[semantic_convention_1.default.GEN_AI_REQUEST_MODEL]: modelName,
[semantic_convention_1.default.GEN_AI_RESPONSE_MODEL]: responseModel,
[semantic_convention_1.default.GEN_AI_USAGE_INPUT_TOKENS]: inputTokens,
[semantic_convention_1.default.GEN_AI_USAGE_OUTPUT_TOKENS]: outputTokens,
[semantic_convention_1.default.GEN_AI_RESPONSE_FINISH_REASON]: [holder.finishReason],
[semantic_convention_1.default.GEN_AI_OUTPUT_TYPE]: typeof completionContent === 'string' ? 'text' : 'json',
};
if (serverAddress)
eventAttrs[semantic_convention_1.default.SERVER_ADDRESS] = serverAddress;
if (serverPort)
eventAttrs[semantic_convention_1.default.SERVER_PORT] = serverPort;
if (responseId)
eventAttrs[semantic_convention_1.default.GEN_AI_RESPONSE_ID] = responseId;
if (shouldCaptureMessageContent()) {
const inputRaw = holder.inputMessagesRaw || holder.prompts || [];
const inputMessagesStructured = holder.inputMessagesStructured || [];
const inputMsgs = inputMessagesStructured.length > 0
? inputMessagesStructured
: buildInputMessages(inputRaw);
if (inputMsgs.length > 0)
eventAttrs[semantic_convention_1.default.GEN_AI_INPUT_MESSAGES] = JSON.stringify(inputMsgs);
if (outputMessagesJson && outputMessagesJson !== '[]')
eventAttrs[semantic_convention_1.default.GEN_AI_OUTPUT_MESSAGES] = outputMessagesJson;
if (holder.systemInstructions) {
eventAttrs[semantic_convention_1.default.GEN_AI_SYSTEM_INSTRUCTIONS] = JSON.stringify(holder.systemInstructions);
}
if (holder.toolDefinitions) {
eventAttrs[semantic_convention_1.default.GEN_AI_TOOL_DEFINITIONS] = JSON.stringify(holder.toolDefinitions);
}
// Request params for event
for (const [k, attr] of [
['temperature', semantic_convention_1.default.GEN_AI_REQUEST_TEMPERATURE],
['top_p', semantic_convention_1.default.GEN_AI_REQUEST_TOP_P],
['frequency_penalty', semantic_convention_1.default.GEN_AI_REQUEST_FREQUENCY_PENALTY],
['presence_penalty', semantic_convention_1.default.GEN_AI_REQUEST_PRESENCE_PENALTY],
]) {
if (modelParameters[k] != null)
eventAttrs[attr] = modelParameters[k];
}
const maxT = modelParameters.max_tokens || modelParameters.max_completion_tokens;
if (maxT != null)
eventAttrs[semantic_convention_1.default.GEN_AI_REQUEST_MAX_TOKENS] = maxT;
}
if (typeof helpers_1.default.emitInferenceEvent === 'function') {
helpers_1.default.emitInferenceEvent(span, eventAttrs);
}
// Record metrics
const metricParams = {
genAIEndpoint: 'langchain.chat_model',
model: responseModel,
cost,
aiSystem: provider,
serverAddress,
serverPort,
};
base_wrapper_1.default.recordMetrics(span, metricParams);
if (holder.suppressionActive) {
(0, helpers_1.resetFrameworkLlmActive)();
(0, helpers_1.clearFrameworkParentContext)();
}
this._endSpan(runId);
}
catch { /* non-blocking */ }
}
handleLLMError(error, runId) {
try {
const holder = this.spans.get(runId);
if (!holder)
return;
if (holder.suppressionActive) {
(0, helpers_1.resetFrameworkLlmActive)();
(0, helpers_1.clearFrameworkParentContext)();
}
const errorType = error?.constructor?.name || '_OTHER';
holder.span.setAttribute(semantic_convention_1.default.ERROR_TYPE, errorType);
helpers_1.default.handleException(holder.span, error instanceof Error ? error : new Error(String(error)));
this._endSpan(runId, String(error));
}
catch { /* non-blocking */ }
}
// ---- Tool Callbacks ----------------------------------------------------
handleToolStart(tool, input, runId, parentRunId, _tags, metadata, kwargs) {
try {
const name = this._getNameFromCallback(tool, kwargs || {});
const spanName = `execute_tool ${name}`;
const span = this._createSpan(runId, parentRunId, spanName);
const holder = this._newHolder(span, parentRunId);
this.spans.set(runId, holder);
if (parentRunId) {
const parentResolved = this._resolveParentRunId(parentRunId);
if (parentResolved && this.spans.has(parentResolved)) {
this.spans.get(parentResolved).children.push(runId);
}
}
span.setAttribute(semantic_convention_1.default.GEN_AI_PROVIDER_NAME_OTEL, semantic_convention_1.default.GEN_AI_SYSTEM_LANGCHAIN);
this._setCommonAttributes(span, semantic_convention_1.default.GEN_AI_OPERATION_TYPE_TOOLS);
span.setAttribute(semantic_convention_1.default.GEN_AI_TOOL_NAME, name);
span.setAttribute(semantic_convention_1.default.GEN_AI_TOOL_TYPE_OTEL, 'function');
span.setAttribute(semantic_convention_1.default.GEN_AI_TOOL_CALL_ID, runId);
const description = tool?.description;
if (description)
span.setAttribute(semantic_convention_1.default.GEN_AI_TOOL_DESCRIPTION, String(description));
const convId = resolveConversationId(metadata);
if (convId)
span.se