UNPKG

openlit

Version:

OpenTelemetry-native Auto instrumentation library for monitoring LLM Applications, facilitating the integration of observability into your GenAI-driven projects

488 lines 19.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.isFrameworkLlmActive = isFrameworkLlmActive; exports.runWithFrameworkLlm = runWithFrameworkLlm; exports.setFrameworkLlmActive = setFrameworkLlmActive; exports.resetFrameworkLlmActive = resetFrameworkLlmActive; exports.isLangGraphActive = isLangGraphActive; exports.runWithLangGraph = runWithLangGraph; exports.isCreateAgentActive = isCreateAgentActive; exports.runWithCreateAgent = runWithCreateAgent; exports.getLangGraphConversationId = getLangGraphConversationId; exports.runWithLangGraphConversationId = runWithLangGraphConversationId; exports.getFrameworkParentContext = getFrameworkParentContext; exports.setFrameworkParentContext = setFrameworkParentContext; exports.clearFrameworkParentContext = clearFrameworkParentContext; exports.getServerAddressForProvider = getServerAddressForProvider; exports.applyCustomSpanAttributes = applyCustomSpanAttributes; exports.getMergedCustomAttributes = getMergedCustomAttributes; exports.injectAdditionalAttributes = injectAdditionalAttributes; exports.usingAttributes = usingAttributes; const js_tiktoken_1 = require("js-tiktoken"); const api_1 = require("@opentelemetry/api"); const api_logs_1 = require("@opentelemetry/api-logs"); const async_hooks_1 = require("async_hooks"); const semantic_convention_1 = __importDefault(require("./semantic-convention")); const events_1 = __importDefault(require("./otel/events")); const config_1 = __importDefault(require("./config")); /** * AsyncLocalStorage for context-scoped custom span attributes. * Mirrors Python's ContextVar _custom_span_attributes, used by * usingAttributes() and injectAdditionalAttributes(). */ const _customSpanAttributes = new async_hooks_1.AsyncLocalStorage(); // --------------------------------------------------------------------------- // Framework LLM span suppression flags (mirrors Python SDK ContextVars) // --------------------------------------------------------------------------- const _frameworkLlmActive = new async_hooks_1.AsyncLocalStorage(); /** * Returns true when a framework instrumentor (LangChain, LiteLLM, etc.) * owns the current LLM span. Provider-level wrappers (OpenAI, Anthropic, …) * must skip their own span creation when this returns true. */ function isFrameworkLlmActive() { return _frameworkLlmActive.getStore() === true; } /** * Run `fn` with the framework-LLM-active flag set. All provider wrappers * invoked inside `fn` will see `isFrameworkLlmActive() === true`. */ function runWithFrameworkLlm(fn) { return _frameworkLlmActive.run(true, fn); } /** * Set framework-LLM-active flag in the current execution context. * Used by SpanProcessor-based instrumentations (Strands) where * the processor observes spans rather than controlling execution. * Mirrors Python's ContextVar.set(True) in Strands processor. */ function setFrameworkLlmActive() { _frameworkLlmActive.enterWith(true); } /** * Reset framework-LLM-active flag in the current execution context. * Mirrors Python's ContextVar.reset(token) in Strands processor. */ function resetFrameworkLlmActive() { _frameworkLlmActive.enterWith(false); } const _langGraphActive = new async_hooks_1.AsyncLocalStorage(); /** * Returns true when a LangGraph wrapper is controlling execution. * LangChain's callback handler skips its own invoke_workflow span when true. */ function isLangGraphActive() { return _langGraphActive.getStore() === true; } function runWithLangGraph(fn) { return _langGraphActive.run(true, fn); } const _createAgentActive = new async_hooks_1.AsyncLocalStorage(); /** * Returns true when a create_agent span is already being handled * (prevents duplicate spans between LangChain and LangGraph). */ function isCreateAgentActive() { return _createAgentActive.getStore() === true; } function runWithCreateAgent(fn) { return _createAgentActive.run(true, fn); } const _langGraphConversationId = new async_hooks_1.AsyncLocalStorage(); /** * Propagate conversation ID from invoke_workflow to child node spans. * Mirrors Python's set_langgraph_conversation_id / get_langgraph_conversation_id. */ function getLangGraphConversationId() { return _langGraphConversationId.getStore(); } function runWithLangGraphConversationId(conversationId, fn) { return _langGraphConversationId.run(conversationId, fn); } // --------------------------------------------------------------------------- // Framework parent context propagation (mirrors Python context_api.attach) // --------------------------------------------------------------------------- const _frameworkParentContext = new async_hooks_1.AsyncLocalStorage(); /** * Returns the OTel context set by a framework processor (OpenAI Agents, etc.) * so that provider wrappers can create spans as children of framework spans. * Mirrors Python's context_api.attach(set_span_in_context(span)). */ function getFrameworkParentContext() { return _frameworkParentContext.getStore() || undefined; } /** * Set the OTel parent context for provider span creation. * Called by processor-based frameworks that cannot use context.with(). */ function setFrameworkParentContext(ctx) { _frameworkParentContext.enterWith(ctx); } /** * Clear the framework parent context. */ function clearFrameworkParentContext() { _frameworkParentContext.enterWith(undefined); } // --------------------------------------------------------------------------- // Provider default endpoints (mirrors Python PROVIDER_DEFAULT_ENDPOINTS) // --------------------------------------------------------------------------- const PROVIDER_DEFAULT_ENDPOINTS = { openai: ['api.openai.com', 443], anthropic: ['api.anthropic.com', 443], google: ['generativelanguage.googleapis.com', 443], 'gcp.gemini': ['generativelanguage.googleapis.com', 443], 'gcp.vertex_ai': ['aiplatform.googleapis.com', 443], mistral_ai: ['api.mistral.ai', 443], groq: ['api.groq.com', 443], together: ['api.together.xyz', 443], fireworks: ['api.fireworks.ai', 443], perplexity: ['api.perplexity.ai', 443], deepinfra: ['api.deepinfra.com', 443], 'aws.bedrock': ['bedrock-runtime.amazonaws.com', 443], azure: ['openai.azure.com', 443], 'azure.ai.openai': ['openai.azure.com', 443], 'azure.ai.inference': ['inference.ai.azure.com', 443], cohere: ['api.cohere.ai', 443], ollama: ['localhost', 11434], deepseek: ['api.deepseek.com', 443], x_ai: ['api.x.ai', 443], huggingface: ['api-inference.huggingface.co', 443], cursor: ['api2.cursor.sh', 443], }; function getServerAddressForProvider(provider) { return PROVIDER_DEFAULT_ENDPOINTS[provider] || ['', 0]; } /** * Apply global (from init) and context-scoped (from usingAttributes / * injectAdditionalAttributes) custom attributes to a span. * Global attributes are applied first; context attributes override on conflict. * Matches Python's _apply_custom_span_attributes(). */ function applyCustomSpanAttributes(span) { const globalAttrs = config_1.default.customSpanAttributes; if (globalAttrs) { for (const [key, value] of Object.entries(globalAttrs)) { span.setAttribute(key, value); } } const contextAttrs = _customSpanAttributes.getStore(); if (contextAttrs) { for (const [key, value] of Object.entries(contextAttrs)) { span.setAttribute(key, value); } } } /** * Get merged custom attributes (global + context) for use in events. * Returns a flat object; context attributes override global on conflict. */ function getMergedCustomAttributes() { const merged = {}; const globalAttrs = config_1.default.customSpanAttributes; if (globalAttrs) { Object.assign(merged, globalAttrs); } const contextAttrs = _customSpanAttributes.getStore(); if (contextAttrs) { Object.assign(merged, contextAttrs); } return merged; } /** * Run a function with custom span attributes attached to all * auto-instrumented spans created during its execution. * Matches Python's openlit.inject_additional_attributes(). */ function injectAdditionalAttributes(fn, attributes) { return _customSpanAttributes.run(attributes, fn); } /** * Context wrapper that adds custom attributes to all auto-instrumented * spans created within its callback scope. * Matches Python's openlit.using_attributes() context manager. * * Usage: * await usingAttributes({"user.id": "u1", "team": "ml"}, async () => { * await client.chat.completions.create(...); * }); */ function usingAttributes(attributes, fn) { return _customSpanAttributes.run(attributes, fn); } class OpenLitHelper { static openaiTokens(text, model) { try { const encoding = (0, js_tiktoken_1.encodingForModel)(model); return encoding.encode(text).length; } catch { return OpenLitHelper.generalTokens(text); } } static generalTokens(text) { const encoding = (0, js_tiktoken_1.encodingForModel)('gpt2'); return encoding.encode(text).length; } static getChatModelCost(model, pricingInfo, promptTokens, completionTokens) { try { const chatPricing = pricingInfo?.chat; if (!chatPricing) return 0; let modelPricing = chatPricing[model]; if (modelPricing == null && model.includes('/')) { modelPricing = chatPricing[model.split('/', 2)[1]]; } if (modelPricing == null) return 0; const cost = (promptTokens / OpenLitHelper.PROMPT_TOKEN_FACTOR) * modelPricing.promptPrice + (completionTokens / OpenLitHelper.PROMPT_TOKEN_FACTOR) * modelPricing.completionPrice; return isNaN(cost) ? 0 : cost; } catch { return 0; } } static getEmbedModelCost(model, pricingInfo, promptTokens) { try { const embedPricing = pricingInfo?.embeddings; if (!embedPricing) return 0; let unitCost = embedPricing[model]; if (unitCost == null && model.includes('/')) { unitCost = embedPricing[model.split('/', 2)[1]]; } if (unitCost == null) return 0; const cost = (promptTokens / OpenLitHelper.PROMPT_TOKEN_FACTOR) * unitCost; return isNaN(cost) ? 0 : cost; } catch { return 0; } } static getImageModelCost(model, pricingInfo, size, quality) { try { const cost = pricingInfo.images[model][quality][size]; return isNaN(cost) ? 0 : cost; } catch (error) { console.error(`Error in getImageModelCost: ${error}`); return 0; } } static getAudioModelCost(model, pricingInfo, prompt) { try { const cost = (prompt.length / OpenLitHelper.PROMPT_TOKEN_FACTOR) * pricingInfo.audio[model]; return isNaN(cost) ? 0 : cost; } catch (error) { console.error(`Error in getAudioModelCost: ${error}`); return 0; } } static async fetchPricingInfo(pricingJson) { let pricingUrl = 'https://raw.githubusercontent.com/openlit/openlit/main/assets/pricing.json'; if (pricingJson) { let isUrl = false; try { isUrl = !!new URL(pricingJson); } catch { isUrl = false; } if (isUrl) { pricingUrl = pricingJson; } else { try { if (typeof pricingJson === 'string') { const json = JSON.parse(pricingJson); return json; } else { const json = JSON.parse(JSON.stringify(pricingJson)); return json; } } catch { return {}; } } } try { const response = await fetch(pricingUrl); if (response.ok) { return response.json(); } else { throw new Error(`HTTP error occurred while fetching pricing info: ${response.status}`); } } catch (error) { console.error(`Unexpected error occurred while fetching pricing info: ${error}`); return {}; } } /** * Build OTel-spec input messages JSON string from provider messages array. * Format: [{"role": "user", "parts": [{"type": "text", "content": "..."}]}] */ static buildInputMessages(messages, system) { try { const otelMessages = []; if (system) { otelMessages.push({ role: 'system', parts: [{ type: 'text', content: system }] }); } for (const msg of messages || []) { const role = msg.role || 'user'; const content = msg.content; const parts = []; if (typeof content === 'string' && content) { parts.push({ type: 'text', content }); } else if (Array.isArray(content)) { for (const item of content) { const t = item.type; if (t === 'text') { parts.push({ type: 'text', content: item.text || '' }); } else if (t === 'image_url') { const url = item.image_url?.url || ''; if (url && !url.startsWith('data:')) { parts.push({ type: 'uri', modality: 'image', uri: url }); } } else if (t === 'image') { // Anthropic image format const url = item.source?.url || ''; if (url && !url.startsWith('data:')) { parts.push({ type: 'uri', modality: 'image', uri: url }); } } else if (t === 'tool_use') { parts.push({ type: 'tool_call', id: item.id || '', name: item.name || '', arguments: item.input || {} }); } else if (t === 'tool_result') { parts.push({ type: 'tool_call_response', id: item.tool_use_id || '', response: typeof item.content === 'string' ? item.content : JSON.stringify(item.content || '') }); } } } // Handle tool_calls in message (OpenAI assistant format) if (msg.tool_calls && Array.isArray(msg.tool_calls)) { for (const tc of msg.tool_calls) { let args = tc.function?.arguments || {}; if (typeof args === 'string') { try { args = JSON.parse(args); } catch { args = { raw: args }; } } parts.push({ type: 'tool_call', id: tc.id || '', name: tc.function?.name || '', arguments: args }); } } if (parts.length > 0) { otelMessages.push({ role, parts }); } } return JSON.stringify(otelMessages); } catch { return '[]'; } } /** * Build OTel-spec output messages JSON string from provider response. * Format: [{"role": "assistant", "parts": [{"type": "text", "content": "..."}], "finish_reason": "stop"}] */ static buildOutputMessages(text, finishReason, toolCalls) { try { const parts = []; if (text) { parts.push({ type: 'text', content: text }); } if (toolCalls && toolCalls.length > 0) { for (const tc of toolCalls) { let args = tc.function?.arguments || tc.arguments || {}; if (typeof args === 'string') { try { args = JSON.parse(args); } catch { args = { raw: args }; } } parts.push({ type: 'tool_call', id: tc.id || '', name: tc.function?.name || tc.name || '', arguments: args, }); } } return JSON.stringify([{ role: 'assistant', parts, finish_reason: finishReason || 'stop' }]); } catch { return '[]'; } } /** * Emit an inference event via the LoggerProvider, matching Python SDK's * gen_ai.client.inference.operation.details event. * Falls back to span.addEvent if LoggerProvider is not available. */ static emitInferenceEvent(span, attrs) { const eventAttributes = {}; const customAttrs = getMergedCustomAttributes(); for (const [key, value] of Object.entries(customAttrs)) { if (value !== undefined && value !== null) { eventAttributes[key] = value; } } for (const [key, value] of Object.entries(attrs)) { if (value !== undefined && value !== null) { eventAttributes[key] = value; } } if (events_1.default.logger) { events_1.default.logger.emit({ eventName: semantic_convention_1.default.GEN_AI_CLIENT_INFERENCE_OPERATION_DETAILS, context: api_1.trace.setSpan(api_1.context.active(), span), severityNumber: api_logs_1.SeverityNumber.INFO, severityText: 'INFO', body: semantic_convention_1.default.GEN_AI_CLIENT_INFERENCE_OPERATION_DETAILS, attributes: { ...eventAttributes, 'event.name': semantic_convention_1.default.GEN_AI_CLIENT_INFERENCE_OPERATION_DETAILS, }, }); } else { span.addEvent(semantic_convention_1.default.GEN_AI_CLIENT_INFERENCE_OPERATION_DETAILS, eventAttributes); } } static handleException(span, error) { span.recordException(error); span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error.message }); const errorType = error.constructor?.name || '_OTHER'; span.setAttribute(semantic_convention_1.default.ERROR_TYPE, errorType); } static async createStreamProxy(stream, generatorFuncResponse) { return new Proxy(stream, { get(target, prop, receiver) { if (prop === Symbol.asyncIterator) { return () => generatorFuncResponse; } return Reflect.get(target, prop, receiver); } }); } } OpenLitHelper.PROMPT_TOKEN_FACTOR = 1000; exports.default = OpenLitHelper; //# sourceMappingURL=helpers.js.map