宽事件已经包含完整的 ai 元数据,但你经常希望在处理器中获取相同的数据:将其持久化、展示给最终用户、据此计费,或将增量进度流式传输给客户端。
AILogger 为此提供了三个方法,无需触碰内部状态。
getMetadata():最终快照
返回一个结构化的 AIMetadata 对象,它与宽事件中的 ai 字段一致。可在任何时间调用,包括运行完成后或在 AI SDK 的 onFinish 中:
server/api/chat.post.ts
import { useLogger } from 'evlog'
import { createAILogger } from 'evlog/ai'
import { generateText } from 'ai'
export default defineEventHandler(async (event) => {
const log = useLogger(event)
const ai = createAILogger(log, {
cost: { 'claude-sonnet-4.6': { input: 3, output: 15 } },
})
await generateText({
model: ai.wrap('anthropic/claude-sonnet-4.6'),
prompt: '总结这份文档',
})
const metadata = ai.getMetadata()
await db.aiRuns.insert({
userId: event.context.userId,
model: metadata.model,
inputTokens: metadata.inputTokens,
outputTokens: metadata.outputTokens,
estimatedCost: metadata.estimatedCost,
finishReason: metadata.finishReason,
responseId: metadata.responseId,
})
return { ok: true }
})
该快照是一个全新的副本:对它的修改永远不会影响底层状态或后续调用。
getEstimatedCost():快速检查成本
getMetadata().estimatedCost 的便捷封装。返回美元成本;如果未提供 cost 映射,或模型不在映射中,则返回 undefined。
const ai = createAILogger(log, {
cost: { 'claude-sonnet-4.6': { input: 3, output: 15 } },
})
await generateText({ model: ai.wrap('anthropic/claude-sonnet-4.6'), prompt })
const cost = ai.getEstimatedCost()
console.log(`这次调用花费了 $${cost?.toFixed(4)}`)
onUpdate(callback):增量更新
订阅元数据更新。每当底层状态刷新时,回调就会触发:
- 在多步骤代理运行中每个步骤触发一次
- 每次
captureEmbed调用触发一次 - 模型发生错误时触发
- 在
createEvlogIntegration的onEnd(v7)或onFinish(v6)时触发
每次调用都会接收一个全新的快照。返回一个取消订阅函数。订阅者错误会被隔离,绝不会破坏 AI 流程。
server/api/agent.post.ts
import { ToolLoopAgent, createAgentUIStreamResponse, stepCountIs } from 'ai'
import { useLogger } from 'evlog'
import { createAILogger } from 'evlog/ai'
export default defineEventHandler(async (event) => {
const log = useLogger(event)
const { messages } = await readBody(event)
const ai = createAILogger(log)
ai.onUpdate((metadata) => {
pushToClient(event, {
type: 'ai-progress',
step: metadata.steps,
tokens: metadata.totalTokens,
cost: metadata.estimatedCost,
})
})
const agent = new ToolLoopAgent({
model: ai.wrap('anthropic/claude-sonnet-4.6'),
tools: { searchWeb, queryDatabase },
stopWhen: stepCountIs(5),
})
return createAgentUIStreamResponse({ agent, uiMessages: messages })
})
用于一次性清理:
const off = ai.onUpdate((metadata) => { /* ... */ })
// later
off()
AIMetadata 形状
AIMetadata 是 getMetadata() 返回并传递给 onUpdate 监听器的快照所对应的公开类型别名。它与宽事件中的 ai 字段具有相同的形状。
import type { AIMetadata, AIMetadataListener } from 'evlog/ai'
function handleProgress(metadata: AIMetadata) {
console.log(`${metadata.calls} 次调用,$${metadata.estimatedCost ?? 0}`)
}
const listener: AIMetadataListener = handleProgress
ai.onUpdate(listener)
中间件记录的每个字段
每个可能出现在 ai.* 下的字段:
| 宽事件字段 | 来源 | 描述 |
|---|---|---|
ai.calls | 调用次数 | 此请求中的 AI 调用次数 |
ai.model | response.modelId | 提供响应的模型 |
ai.models | 所有模型 ID | 所有使用过的模型数组(仅在数量大于 1 时) |
ai.provider | model.provider | 提供商(anthropic、openai、google 等) |
ai.inputTokens | usage.inputTokens.total | 所有调用的输入令牌总数 |
ai.outputTokens | usage.outputTokens.total | 所有调用的输出令牌总数 |
ai.totalTokens | 计算得出 | inputTokens + outputTokens |
ai.cacheReadTokens | usage.inputTokens.cacheRead | 从提示缓存中提供的令牌数 |
ai.cacheWriteTokens | usage.inputTokens.cacheWrite | 写入提示缓存的令牌数 |
ai.reasoningTokens | usage.outputTokens.reasoning | 推理令牌(扩展思考) |
ai.finishReason | finishReason.unified | 生成结束的原因(stop、tool-calls 等) |
ai.toolCalls | 内容/流块 | 默认是工具名称的 string[],启用 toolInputs 时则为 Array<{ name, input }> |
ai.responseId | response.id | 提供商分配的响应 ID(例如 Anthropic 的 msg_...) |
ai.prompt | 首次调用的 params.prompt | 发送给首次模型调用的提示文本格式化内容(仅在启用 prompt 选项时) |
ai.output | 内容/流块 | 上次模型调用生成的文本(仅在启用 output 选项时) |
ai.steps | 步骤计数 | LLM 调用次数(仅在数量大于 1 时) |
ai.stepsUsage | 按步骤累积 | 每个步骤的令牌和工具调用明细(仅在步骤数大于 1 时) |
ai.msToFirstChunk | 流计时 | 首个文本块所需时间(仅流式传输时) |
ai.msToFinish | 流计时 | 流总耗时(仅流式传输时) |
ai.tokensPerSecond | 计算得出 | 每秒输出令牌数(仅流式传输时) |
ai.error | 错误捕获 | 模型调用失败时的错误消息 |
ai.tools | Telemetry 集成 | 每个工具的 { name, durationMs, success, error? }(需要 createEvlogIntegration) |
ai.totalDurationMs | Telemetry 集成 | 生成总耗时(需要 createEvlogIntegration) |
ai.embedding | captureEmbed | { model?, tokens, dimensions?, count? }——嵌入元数据 |
ai.estimatedCost | 计算得出 | 以美元计的预估成本(需要 cost 选项) |