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
JavaScript
"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