dd-trace
Version:
Datadog APM tracing client for JavaScript
251 lines (204 loc) • 7.6 kB
JavaScript
'use strict'
const { UNKNOWN_MODEL_PROVIDER } = require('../../constants/tags')
const { safeJsonParse } = require('../../util')
const LLMObsPlugin = require('../base')
const { appendMessage } = require('./util')
const ALLOWED_METADATA_KEYS = new Set([
'max_tokens',
'stop_sequences',
'temperature',
'top_k',
'top_p',
])
class AnthropicLLMObsPlugin extends LLMObsPlugin {
static integration = 'anthropic' // used for llmobs telemetry
static id = 'anthropic'
static prefix = 'tracing:apm:anthropic:request'
constructor () {
super(...arguments)
this.addSub('apm:anthropic:request:chunk', ({ ctx, chunk, done }) => {
ctx.chunks ??= []
const chunks = ctx.chunks
if (chunk) chunks.push(chunk)
if (!done) return
const response = { content: [] }
for (const chunk of chunks) {
switch (chunk.type) {
case 'message_start': {
const { message } = chunk
if (!message) continue
const { role, usage } = message
if (role) response.role = role
if (usage) response.usage = usage
break
}
case 'content_block_start': {
const contentBlock = chunk.content_block
if (!contentBlock) continue
const { type } = contentBlock
if (type === 'text') {
response.content.push({ type, text: contentBlock.text })
} else if (type === 'thinking') {
response.content.push({ type, thinking: contentBlock.thinking ?? '' })
} else if (type === 'tool_use') {
response.content.push({ type, name: contentBlock.name, input: '', id: contentBlock.id })
}
break
}
case 'content_block_delta': {
const { delta } = chunk
if (!delta) continue
const lastBlock = response.content[response.content.length - 1]
if (!lastBlock) continue
if (delta.type === 'thinking_delta') {
const { thinking } = delta
if (thinking) lastBlock.thinking += thinking
} else if (delta.type === 'signature_delta') {
// Signature is for internal verification only; skip it.
} else if (delta.type === 'input_json_delta') {
const partialJson = delta.partial_json
if (partialJson) lastBlock.input += partialJson
} else {
const { text } = delta
if (text) lastBlock.text += text
}
break
}
case 'content_block_stop': {
const lastBlock = response.content[response.content.length - 1]
if (!lastBlock) break
if (lastBlock.type === 'tool_use') {
const input = lastBlock.input ?? '{}'
lastBlock.input = safeJsonParse(input, {})
}
break
}
case 'message_delta': {
const { delta } = chunk
const finishReason = delta?.stop_reason
if (finishReason) response.finish_reason = finishReason
const { usage } = chunk
if (usage) {
const responseUsage = response.usage ?? (response.usage = { input_tokens: 0, output_tokens: 0 })
responseUsage.output_tokens = usage.output_tokens
const cacheCreationTokens = usage.cache_creation_input_tokens
const cacheReadTokens = usage.cache_read_input_tokens
if (cacheCreationTokens) responseUsage.cache_creation_input_tokens = cacheCreationTokens
if (cacheReadTokens) responseUsage.cache_read_input_tokens = cacheReadTokens
}
break
}
case 'error': {
const { error } = chunk
if (!error) continue
response.error = {}
if (error.type) response.error.type = error.type
if (error.message) response.error.message = error.message
break
}
}
ctx.result = response
}
})
}
getLLMObsSpanRegisterOptions (ctx) {
const { options, baseUrl } = ctx
const { model } = options
const modelProvider = this._getModelProvider(baseUrl)
return {
kind: 'llm',
modelName: model,
modelProvider,
}
}
_getModelProvider (baseUrl = '') {
if (baseUrl.includes('anthropic')) {
return 'anthropic'
}
return UNKNOWN_MODEL_PROVIDER
}
setLLMObsTags (ctx) {
const span = ctx.currentStore?.span
if (!span) return
const { options, result } = ctx
this.#tagAnthropicInputMessages(span, options)
this.#tagAnthropicOutputMessages(span, result)
this.#tagAnthropicMetadata(span, options)
this.#tagAnthropicUsage(span, result)
}
#tagAnthropicInputMessages (span, options) {
const { system, messages } = options
const inputMessages = []
if (system) {
appendMessage(inputMessages, { role: 'system', content: system })
}
for (const message of messages) {
appendMessage(inputMessages, message)
}
this._tagger.tagLLMIO(span, inputMessages)
}
#tagAnthropicOutputMessages (span, result) {
if (!result) return
const { content, role } = result
if (typeof content === 'string') {
this._tagger.tagLLMIO(span, null, [{ content, role }])
return
}
const outputMessages = []
for (const block of content) {
if (block.type === 'thinking') {
outputMessages.push({ content: block.thinking ?? '', role: 'reasoning' })
continue
}
const { text } = block
if (typeof text === 'string') {
outputMessages.push({ content: text, role })
} else if (block.type === 'tool_use') {
const toolCall = {
name: block.name,
arguments: safeJsonParse(block.input, {}),
toolId: block.id,
type: block.type,
}
outputMessages.push({ content: text ?? '', role, toolCalls: [toolCall] })
}
}
this._tagger.tagLLMIO(span, null, outputMessages)
}
#tagAnthropicMetadata (span, options) {
const metadata = {}
for (const [key, value] of Object.entries(options)) {
if (ALLOWED_METADATA_KEYS.has(key)) {
metadata[key] = value
}
}
this._tagger.tagMetadata(span, metadata)
}
#tagAnthropicUsage (span, result) {
if (!result) return
const { usage } = result
if (!usage) return
const inputTokens = usage.input_tokens
const outputTokens = usage.output_tokens
const cacheWriteTokens = usage.cache_creation_input_tokens
const cacheReadTokens = usage.cache_read_input_tokens
const metrics = {
inputTokens: (inputTokens ?? 0) + (cacheWriteTokens ?? 0) + (cacheReadTokens ?? 0),
}
if (outputTokens) metrics.outputTokens = outputTokens
const totalTokens = metrics.inputTokens + (outputTokens ?? 0)
if (totalTokens) metrics.totalTokens = totalTokens
if (cacheWriteTokens != null) metrics.cacheWriteTokens = cacheWriteTokens
if (cacheReadTokens != null) metrics.cacheReadTokens = cacheReadTokens
const cacheCreation = usage.cache_creation
if (cacheCreation) {
metrics.cacheWrite5mTokens = cacheCreation.ephemeral_5m_input_tokens ?? 0
metrics.cacheWrite1hTokens = cacheCreation.ephemeral_1h_input_tokens ?? 0
} else if (cacheWriteTokens != null) {
metrics.cacheWrite5mTokens = cacheWriteTokens
metrics.cacheWrite1hTokens = 0
}
this._tagger.tagMetrics(span, metrics)
}
}
module.exports = AnthropicLLMObsPlugin