openlit
Version:
OpenTelemetry-native Auto instrumentation library for monitoring LLM Applications, facilitating the integration of observability into your GenAI-driven projects
434 lines • 22.5 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const api_1 = require("@opentelemetry/api");
const config_1 = __importDefault(require("../../config"));
const helpers_1 = __importDefault(require("../../helpers"));
const semantic_convention_1 = __importDefault(require("../../semantic-convention"));
const base_wrapper_1 = __importDefault(require("../base-wrapper"));
function spanCreationAttrs(operationName, requestModel) {
return {
[semantic_convention_1.default.GEN_AI_OPERATION]: operationName,
[semantic_convention_1.default.GEN_AI_PROVIDER_NAME_OTEL]: VercelAIWrapper.aiSystem,
[semantic_convention_1.default.GEN_AI_REQUEST_MODEL]: requestModel,
[semantic_convention_1.default.SERVER_ADDRESS]: VercelAIWrapper.serverAddress,
[semantic_convention_1.default.SERVER_PORT]: VercelAIWrapper.serverPort,
};
}
class VercelAIWrapper extends base_wrapper_1.default {
static _patchGenerateText(tracer) {
const genAIEndpoint = 'vercel_ai.generateText';
return (originalMethod) => {
return async function (...args) {
const params = args[0] || {};
const modelId = params.model?.modelId || 'unknown';
const spanName = `${semantic_convention_1.default.GEN_AI_OPERATION_TYPE_CHAT} ${modelId}`;
const span = tracer.startSpan(spanName, {
kind: api_1.SpanKind.CLIENT,
attributes: spanCreationAttrs(semantic_convention_1.default.GEN_AI_OPERATION_TYPE_CHAT, modelId),
});
return api_1.context
.with(api_1.trace.setSpan(api_1.context.active(), span), async () => {
return originalMethod.apply(this, args);
})
.then((response) => {
return VercelAIWrapper._chatComplete({
args,
genAIEndpoint,
response,
span,
outputType: semantic_convention_1.default.GEN_AI_OUTPUT_TYPE_TEXT,
});
})
.catch((e) => {
helpers_1.default.handleException(span, e);
base_wrapper_1.default.recordMetrics(span, {
genAIEndpoint,
model: modelId,
aiSystem: VercelAIWrapper.aiSystem,
serverAddress: VercelAIWrapper.serverAddress,
serverPort: VercelAIWrapper.serverPort,
errorType: e?.constructor?.name || '_OTHER',
});
span.end();
throw e;
});
};
};
}
static _patchStreamText(tracer) {
const genAIEndpoint = 'vercel_ai.streamText';
return (originalMethod) => {
return async function (...args) {
const params = args[0] || {};
const modelId = params.model?.modelId || 'unknown';
const spanName = `${semantic_convention_1.default.GEN_AI_OPERATION_TYPE_CHAT} ${modelId}`;
const span = tracer.startSpan(spanName, {
kind: api_1.SpanKind.CLIENT,
attributes: spanCreationAttrs(semantic_convention_1.default.GEN_AI_OPERATION_TYPE_CHAT, modelId),
});
const startTime = Date.now();
const chunkTimestamps = [];
try {
const response = await api_1.context.with(api_1.trace.setSpan(api_1.context.active(), span), async () => originalMethod.apply(this, args));
try {
const originalTextStream = response.textStream;
if (originalTextStream && typeof originalTextStream.getReader === 'function') {
const reader = originalTextStream.getReader();
const wrappedTextStream = new ReadableStream({
async pull(controller) {
const { done, value } = await reader.read();
if (done) {
controller.close();
}
else {
chunkTimestamps.push(Date.now());
controller.enqueue(value);
}
},
cancel() {
reader.cancel();
},
});
Object.defineProperty(response, 'textStream', {
value: wrappedTextStream,
writable: true,
configurable: true,
});
}
}
catch (_) {
// Stream interception is best-effort; TTFT/TBT won't be captured
}
Promise.resolve(response.usage)
.then(async (usage) => {
let metricParams;
try {
const ttft = chunkTimestamps.length > 0 ? (chunkTimestamps[0] - startTime) / 1000 : 0;
let tbt = 0;
if (chunkTimestamps.length > 1) {
const timeDiffs = chunkTimestamps.slice(1).map((t, i) => t - chunkTimestamps[i]);
tbt = timeDiffs.reduce((a, b) => a + b, 0) / timeDiffs.length / 1000;
}
const finishReason = await Promise.resolve(response.finishReason).catch(() => 'stop');
const text = await Promise.resolve(response.text).catch(() => '');
const toolCalls = await Promise.resolve(response.toolCalls).catch(() => undefined);
const responseDetails = await Promise.resolve(response.response).catch(() => undefined);
const result = {
usage: {
promptTokens: usage?.promptTokens || 0,
completionTokens: usage?.completionTokens || 0,
},
finishReason: finishReason || 'stop',
text: text || '',
toolCalls,
response: responseDetails,
};
metricParams = await VercelAIWrapper._chatCommonSetter({
args,
genAIEndpoint,
result,
span,
isStream: true,
outputType: semantic_convention_1.default.GEN_AI_OUTPUT_TYPE_TEXT,
ttft,
tbt,
});
}
catch (e) {
helpers_1.default.handleException(span, e);
}
finally {
span.end();
if (metricParams) {
base_wrapper_1.default.recordMetrics(span, metricParams);
}
}
})
.catch((e) => {
helpers_1.default.handleException(span, e);
base_wrapper_1.default.recordMetrics(span, {
genAIEndpoint,
model: modelId,
aiSystem: VercelAIWrapper.aiSystem,
serverAddress: VercelAIWrapper.serverAddress,
serverPort: VercelAIWrapper.serverPort,
errorType: e?.constructor?.name || '_OTHER',
});
span.end();
});
return response;
}
catch (e) {
helpers_1.default.handleException(span, e);
base_wrapper_1.default.recordMetrics(span, {
genAIEndpoint,
model: modelId,
aiSystem: VercelAIWrapper.aiSystem,
serverAddress: VercelAIWrapper.serverAddress,
serverPort: VercelAIWrapper.serverPort,
errorType: e?.constructor?.name || '_OTHER',
});
span.end();
throw e;
}
};
};
}
static _patchGenerateObject(tracer) {
const genAIEndpoint = 'vercel_ai.generateObject';
return (originalMethod) => {
return async function (...args) {
const params = args[0] || {};
const modelId = params.model?.modelId || 'unknown';
const spanName = `${semantic_convention_1.default.GEN_AI_OPERATION_TYPE_CHAT} ${modelId}`;
const span = tracer.startSpan(spanName, {
kind: api_1.SpanKind.CLIENT,
attributes: spanCreationAttrs(semantic_convention_1.default.GEN_AI_OPERATION_TYPE_CHAT, modelId),
});
return api_1.context
.with(api_1.trace.setSpan(api_1.context.active(), span), async () => {
return originalMethod.apply(this, args);
})
.then((response) => {
const result = {
...response,
text: JSON.stringify(response.object || {}),
};
return VercelAIWrapper._chatComplete({
args,
genAIEndpoint,
response,
span,
outputType: semantic_convention_1.default.GEN_AI_OUTPUT_TYPE_JSON,
resultOverride: result,
});
})
.catch((e) => {
helpers_1.default.handleException(span, e);
base_wrapper_1.default.recordMetrics(span, {
genAIEndpoint,
model: modelId,
aiSystem: VercelAIWrapper.aiSystem,
serverAddress: VercelAIWrapper.serverAddress,
serverPort: VercelAIWrapper.serverPort,
errorType: e?.constructor?.name || '_OTHER',
});
span.end();
throw e;
});
};
};
}
static _patchEmbed(tracer) {
const genAIEndpoint = 'vercel_ai.embed';
return (originalMethod) => {
return async function (...args) {
const params = args[0] || {};
const modelId = params.model?.modelId || 'unknown';
const spanName = `${semantic_convention_1.default.GEN_AI_OPERATION_TYPE_EMBEDDING} ${modelId}`;
const span = tracer.startSpan(spanName, {
kind: api_1.SpanKind.CLIENT,
attributes: spanCreationAttrs(semantic_convention_1.default.GEN_AI_OPERATION_TYPE_EMBEDDING, modelId),
});
return api_1.context.with(api_1.trace.setSpan(api_1.context.active(), span), async () => {
const captureContent = config_1.default.captureMessageContent;
let metricParams;
try {
const response = await originalMethod.apply(this, args);
const pricingInfo = config_1.default.pricingInfo || {};
const inputTokens = response.usage?.tokens || 0;
const cost = helpers_1.default.getEmbedModelCost(modelId, pricingInfo, inputTokens);
VercelAIWrapper.setBaseSpanAttributes(span, {
genAIEndpoint,
model: modelId,
cost,
aiSystem: VercelAIWrapper.aiSystem,
serverAddress: VercelAIWrapper.serverAddress,
serverPort: VercelAIWrapper.serverPort,
});
span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_IS_STREAM, false);
span.setAttribute(semantic_convention_1.default.GEN_AI_USAGE_INPUT_TOKENS, inputTokens);
if (captureContent && params.value !== undefined) {
const inputStr = typeof params.value === 'string' ? params.value : JSON.stringify(params.value);
span.setAttribute(semantic_convention_1.default.GEN_AI_INPUT_MESSAGES, inputStr);
}
metricParams = {
genAIEndpoint,
model: modelId,
cost,
aiSystem: VercelAIWrapper.aiSystem,
serverAddress: VercelAIWrapper.serverAddress,
serverPort: VercelAIWrapper.serverPort,
};
return response;
}
catch (e) {
helpers_1.default.handleException(span, e);
metricParams = {
genAIEndpoint,
model: modelId,
aiSystem: VercelAIWrapper.aiSystem,
serverAddress: VercelAIWrapper.serverAddress,
serverPort: VercelAIWrapper.serverPort,
errorType: e?.constructor?.name || '_OTHER',
};
throw e;
}
finally {
span.end();
if (metricParams)
base_wrapper_1.default.recordMetrics(span, metricParams);
}
});
};
};
}
static async _chatComplete({ args, genAIEndpoint, response, span, outputType, resultOverride, }) {
let metricParams;
try {
metricParams = await VercelAIWrapper._chatCommonSetter({
args,
genAIEndpoint,
result: resultOverride || response,
span,
isStream: false,
outputType,
});
return response;
}
catch (e) {
helpers_1.default.handleException(span, e);
throw e;
}
finally {
span.end();
if (metricParams) {
base_wrapper_1.default.recordMetrics(span, metricParams);
}
}
}
static async _chatCommonSetter({ args, genAIEndpoint, result, span, isStream, outputType, ttft = 0, tbt = 0, }) {
const captureContent = config_1.default.captureMessageContent;
const params = args[0] || {};
const modelId = params.model?.modelId || 'unknown';
span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_TEMPERATURE, params.temperature ?? 1);
span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_TOP_P, params.topP ?? 1);
if (params.maxTokens != null) {
span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_MAX_TOKENS, params.maxTokens);
}
span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_IS_STREAM, isStream);
if (params.seed != null) {
span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_SEED, Number(params.seed));
}
if (params.frequencyPenalty) {
span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_FREQUENCY_PENALTY, params.frequencyPenalty);
}
if (params.presencePenalty) {
span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_PRESENCE_PENALTY, params.presencePenalty);
}
if (params.stopSequences) {
span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_STOP_SEQUENCES, Array.isArray(params.stopSequences) ? params.stopSequences : [params.stopSequences]);
}
if (params.topK != null) {
span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_TOP_K, params.topK);
}
const messages = params.messages || (params.prompt ? [{ role: 'user', content: params.prompt }] : []);
if (captureContent) {
span.setAttribute(semantic_convention_1.default.GEN_AI_INPUT_MESSAGES, helpers_1.default.buildInputMessages(messages, params.system));
}
const responseId = result.response?.id;
const responseModel = result.response?.modelId || modelId;
const inputTokens = result.usage?.promptTokens || 0;
const outputTokens = result.usage?.completionTokens || 0;
const pricingInfo = config_1.default.pricingInfo || {};
const cost = helpers_1.default.getChatModelCost(modelId, pricingInfo, inputTokens, outputTokens);
VercelAIWrapper.setBaseSpanAttributes(span, {
genAIEndpoint,
model: modelId,
cost,
aiSystem: VercelAIWrapper.aiSystem,
serverAddress: VercelAIWrapper.serverAddress,
serverPort: VercelAIWrapper.serverPort,
});
span.setAttribute(semantic_convention_1.default.GEN_AI_RESPONSE_MODEL, responseModel);
if (responseId) {
span.setAttribute(semantic_convention_1.default.GEN_AI_RESPONSE_ID, responseId);
}
span.setAttribute(semantic_convention_1.default.GEN_AI_USAGE_INPUT_TOKENS, inputTokens);
span.setAttribute(semantic_convention_1.default.GEN_AI_USAGE_OUTPUT_TOKENS, outputTokens);
const finishReason = result.finishReason || 'stop';
span.setAttribute(semantic_convention_1.default.GEN_AI_RESPONSE_FINISH_REASON, [finishReason]);
span.setAttribute(semantic_convention_1.default.GEN_AI_OUTPUT_TYPE, outputType);
if (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);
}
if (result.toolCalls?.length > 0) {
const toolNames = result.toolCalls.map((t) => t.toolName || '').filter(Boolean);
const toolIds = result.toolCalls.map((t) => t.toolCallId || '').filter(Boolean);
const toolArgs = result.toolCalls.map((t) => JSON.stringify(t.args || {})).filter(Boolean);
if (toolNames.length > 0) {
span.setAttribute(semantic_convention_1.default.GEN_AI_TOOL_NAME, toolNames.join(', '));
}
if (toolIds.length > 0) {
span.setAttribute(semantic_convention_1.default.GEN_AI_TOOL_CALL_ID, toolIds.join(', '));
}
if (toolArgs.length > 0) {
span.setAttribute(semantic_convention_1.default.GEN_AI_TOOL_ARGS, toolArgs.join(', '));
}
}
const normalizedToolCalls = result.toolCalls?.map((t) => ({
id: t.toolCallId || '',
name: t.toolName || '',
arguments: t.args || {},
}));
let inputMessagesJson;
let outputMessagesJson;
if (captureContent) {
outputMessagesJson = helpers_1.default.buildOutputMessages(result.text || '', finishReason, normalizedToolCalls);
span.setAttribute(semantic_convention_1.default.GEN_AI_OUTPUT_MESSAGES, outputMessagesJson);
inputMessagesJson = helpers_1.default.buildInputMessages(messages, params.system);
}
if (!config_1.default.disableEvents) {
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]: modelId,
[semantic_convention_1.default.GEN_AI_RESPONSE_MODEL]: responseModel,
[semantic_convention_1.default.SERVER_ADDRESS]: VercelAIWrapper.serverAddress,
[semantic_convention_1.default.SERVER_PORT]: VercelAIWrapper.serverPort,
[semantic_convention_1.default.GEN_AI_RESPONSE_FINISH_REASON]: [finishReason],
[semantic_convention_1.default.GEN_AI_OUTPUT_TYPE]: outputType,
[semantic_convention_1.default.GEN_AI_USAGE_INPUT_TOKENS]: inputTokens,
[semantic_convention_1.default.GEN_AI_USAGE_OUTPUT_TOKENS]: outputTokens,
};
if (responseId) {
eventAttrs[semantic_convention_1.default.GEN_AI_RESPONSE_ID] = responseId;
}
if (captureContent) {
if (inputMessagesJson)
eventAttrs[semantic_convention_1.default.GEN_AI_INPUT_MESSAGES] = inputMessagesJson;
if (outputMessagesJson)
eventAttrs[semantic_convention_1.default.GEN_AI_OUTPUT_MESSAGES] = outputMessagesJson;
}
helpers_1.default.emitInferenceEvent(span, eventAttrs);
}
return {
genAIEndpoint,
model: modelId,
cost,
aiSystem: VercelAIWrapper.aiSystem,
serverAddress: VercelAIWrapper.serverAddress,
serverPort: VercelAIWrapper.serverPort,
};
}
}
VercelAIWrapper.aiSystem = semantic_convention_1.default.GEN_AI_SYSTEM_VERCEL_AI;
VercelAIWrapper.serverAddress = 'vercel.ai';
VercelAIWrapper.serverPort = 443;
exports.default = VercelAIWrapper;
//# sourceMappingURL=wrapper.js.map