UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

230 lines (185 loc) 6.82 kB
'use strict' const LLMObsPlugin = require('./base') function isIterable (obj) { if (obj == null) { return false } return typeof obj[Symbol.iterator] === 'function' } class OpenAiLLMObsPlugin extends LLMObsPlugin { static id = 'openai' static integration = 'openai' static prefix = 'tracing:apm:openai:request' getLLMObsSpanRegisterOptions (ctx) { const resource = ctx.methodName const methodName = gateResource(normalizeOpenAIResourceName(resource)) if (!methodName) return // we will not trace all openai methods for llmobs const inputs = ctx.args[0] // completion, chat completion, and embeddings take one argument const operation = getOperation(methodName) const kind = operation === 'embedding' ? 'embedding' : 'llm' const { modelProvider, client } = this._getModelProviderAndClient(ctx.basePath) const name = `${client}.${methodName}` return { modelProvider, modelName: inputs.model, kind, name } } setLLMObsTags (ctx) { const span = ctx.currentStore?.span const resource = ctx.methodName const methodName = gateResource(normalizeOpenAIResourceName(resource)) if (!methodName) return // we will not trace all openai methods for llmobs const inputs = ctx.args[0] // completion, chat completion, and embeddings take one argument const response = ctx.result?.data // no result if error const error = !!span.context()._tags.error const operation = getOperation(methodName) if (operation === 'completion') { this._tagCompletion(span, inputs, response, error) } else if (operation === 'chat') { this._tagChatCompletion(span, inputs, response, error) } else if (operation === 'embedding') { this._tagEmbedding(span, inputs, response, error) } if (!error) { const metrics = this._extractMetrics(response) this._tagger.tagMetrics(span, metrics) } } _getModelProviderAndClient (baseUrl = '') { if (baseUrl.includes('azure')) { return { modelProvider: 'azure_openai', client: 'AzureOpenAI' } } else if (baseUrl.includes('deepseek')) { return { modelProvider: 'deepseek', client: 'DeepSeek' } } return { modelProvider: 'openai', client: 'OpenAI' } } _extractMetrics (response) { const metrics = {} const tokenUsage = response.usage if (tokenUsage) { const inputTokens = tokenUsage.prompt_tokens if (inputTokens) metrics.inputTokens = inputTokens const outputTokens = tokenUsage.completion_tokens if (outputTokens) metrics.outputTokens = outputTokens const totalTokens = tokenUsage.total_toksn || (inputTokens + outputTokens) if (totalTokens) metrics.totalTokens = totalTokens } return metrics } _tagEmbedding (span, inputs, response, error) { const { model, ...parameters } = inputs const metadata = { encoding_format: parameters.encoding_format || 'float' } if (inputs.dimensions) metadata.dimensions = inputs.dimensions this._tagger.tagMetadata(span, metadata) let embeddingInputs = inputs.input if (!Array.isArray(embeddingInputs)) embeddingInputs = [embeddingInputs] const embeddingInput = embeddingInputs.map(input => ({ text: input })) if (error) { this._tagger.tagEmbeddingIO(span, embeddingInput) return } const float = Array.isArray(response.data[0].embedding) let embeddingOutput if (float) { const embeddingDim = response.data[0].embedding.length embeddingOutput = `[${response.data.length} embedding(s) returned with size ${embeddingDim}]` } else { embeddingOutput = `[${response.data.length} embedding(s) returned]` } this._tagger.tagEmbeddingIO(span, embeddingInput, embeddingOutput) } _tagCompletion (span, inputs, response, error) { let { prompt, model, ...parameters } = inputs if (!Array.isArray(prompt)) prompt = [prompt] const completionInput = prompt.map(p => ({ content: p })) const completionOutput = error ? [{ content: '' }] : response.choices.map(choice => ({ content: choice.text })) this._tagger.tagLLMIO(span, completionInput, completionOutput) this._tagger.tagMetadata(span, parameters) } _tagChatCompletion (span, inputs, response, error) { const { messages, model, ...parameters } = inputs if (error) { this._tagger.tagLLMIO(span, messages, [{ content: '' }]) return } const outputMessages = [] const { choices } = response if (!isIterable(choices)) { this._tagger.tagLLMIO(span, messages, [{ content: '' }]) return } for (const choice of choices) { const message = choice.message || choice.delta const content = message.content || '' const role = message.role if (message.function_call) { const functionCallInfo = { name: message.function_call.name, arguments: JSON.parse(message.function_call.arguments) } outputMessages.push({ content, role, toolCalls: [functionCallInfo] }) } else if (message.tool_calls) { const toolCallsInfo = [] for (const toolCall of message.tool_calls) { const toolCallInfo = { arguments: JSON.parse(toolCall.function.arguments), name: toolCall.function.name, toolId: toolCall.id, type: toolCall.type } toolCallsInfo.push(toolCallInfo) } outputMessages.push({ content, role, toolCalls: toolCallsInfo }) } else { outputMessages.push({ content, role }) } } this._tagger.tagLLMIO(span, messages, outputMessages) const metadata = Object.entries(parameters).reduce((obj, [key, value]) => { if (!['tools', 'functions'].includes(key)) { obj[key] = value } return obj }, {}) this._tagger.tagMetadata(span, metadata) } } // TODO: this will be moved to the APM integration function normalizeOpenAIResourceName (resource) { switch (resource) { // completions case 'completions.create': return 'createCompletion' // chat completions case 'chat.completions.create': return 'createChatCompletion' // embeddings case 'embeddings.create': return 'createEmbedding' default: return resource } } function gateResource (resource) { return ['createCompletion', 'createChatCompletion', 'createEmbedding'].includes(resource) ? resource : undefined } function getOperation (resource) { switch (resource) { case 'createCompletion': return 'completion' case 'createChatCompletion': return 'chat' case 'createEmbedding': return 'embedding' default: // should never happen return 'unknown' } } module.exports = OpenAiLLMObsPlugin