宽事件已经包含完整的 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) — 增量更新
订阅元数据更新。每当底层状态刷新时,回调就会触发:
- 多步骤 agent 运行中每一步一次
- 每次
captureEmbed调用一次 - 模型出错时
createEvlogIntegration的onFinish中
每次调用都会接收一个全新的快照。返回一个取消订阅函数。订阅者错误会被隔离,绝不会破坏 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 | 所有调用的输入 token 总数 |
ai.outputTokens | usage.outputTokens.total | 所有调用的输出 token 总数 |
ai.totalTokens | 计算得出 | inputTokens + outputTokens |
ai.cacheReadTokens | usage.inputTokens.cacheRead | 从提示缓存中提供的 token |
ai.cacheWriteTokens | usage.inputTokens.cacheWrite | 写入提示缓存的 token |
ai.reasoningTokens | usage.outputTokens.reasoning | 推理 token(扩展思考) |
ai.finishReason | finishReason.unified | 生成结束的原因(stop、tool-calls 等) |
ai.toolCalls | 内容 / 流块 | 默认是工具名称的 string[],或在启用 toolInputs 时为 Array<{ name, input }> |
ai.responseId | response.id | 由提供方分配的响应 ID(例如 Anthropic 的 msg_...) |
ai.steps | 步数 | LLM 调用次数(仅当 > 1 时) |
ai.stepsUsage | 按步累积 | 每一步的 token 与工具调用明细(仅当 > 1 步时) |
ai.msToFirstChunk | 流式时序 | 到第一个文本块的时间(仅流式) |
ai.msToFinish | 流式时序 | 流式总时长(仅流式) |
ai.tokensPerSecond | 计算得出 | 每秒输出 token 数(仅流式) |
ai.error | 错误捕获 | 模型调用失败时的错误消息 |
ai.tools | TelemetryIntegration | 每个工具的 { name, durationMs, success, error? }(需要 createEvlogIntegration) |
ai.totalDurationMs | TelemetryIntegration | 总生成墙钟时间(需要 createEvlogIntegration) |
ai.embedding | captureEmbed | { model?, tokens, dimensions?, count? } — 嵌入元数据 |
ai.estimatedCost | 计算得出 | 以美元计的估算成本(需要 cost 选项) |