跳转到主要内容
看不见就修不了。Manager 每次调用都往 ai_call 写一行 —— 成功、失败、取消都写 —— 包括 token 数、成本、延迟、以及驱动这次调用的 prompt 版本。这一张表撑起了 /admin/cost 后台,也是你定位失控模型或挂掉 provider 唯一需要看的地方。

前置条件

  • 数据库已迁移(pnpm db:push),ai_call 表存在。
  • 实际跑过几次 AI 调用 —— 不然表是空的。
  • 看一遍 src/ai/manager.tsrecordCall(),每行数据都是从这里诞生的。

关键字段

// src/db/ai.schema.ts(节选)
ai_call {
  id              text PRIMARY KEY
  userId          text NULL                            // 关联 user
  provider        text NOT NULL                        // 'openai' | 'mock' | ...
  model           text NOT NULL                        // 'gpt-4o-mini'
  operation       text NOT NULL                        // 'chat' | 'chat_stream' | 'image'
  promptId        text NULL                            // 关联 prompt
  promptVersionId text NULL                            // 关联 prompt_version
  status          text NOT NULL DEFAULT 'pending'      // 'success'|'error'|'cancelled'|...
  streamed        boolean NOT NULL DEFAULT false
  cached          boolean NOT NULL DEFAULT false
  inputTokens     integer NOT NULL DEFAULT 0
  outputTokens    integer NOT NULL DEFAULT 0
  costMicroCents  bigint  NOT NULL DEFAULT 0           // 百分之一美分 —— 除 1_000_000 = 美元
  ttftMs          integer NULL                         // 仅流式
  totalMs         integer NULL
  errorMessage    text NULL
  createdAt       timestamp NOT NULL DEFAULT now()
}
userId(provider, model)statuscreatedAtpromptId 上的索引覆盖了后台 要跑的所有查询。

为什么用 micro-cents?

某些 OpenAI 模型 1k 输入 token 收 $0.0001,200 token 也就 0.002 美分。用「美分」存 就被迫上浮点和精度丢失。micro-cents = 百分之一美分
  • $1.00 = 100¢ = 10_000 micro-cents
  • $0.0001 = 0.01¢ = 1 micro-cent
  • 显示为美元:microCents / 1_000_000
<CostBadge /> 已经替你做了这步,别再自己写一遍。

步骤:写一份用量报告

三条查询:日成本、按模型的错误率、p95 延迟。全部直接打 ai_call

近 30 天日花费

SELECT
  date_trunc('day', created_at)            AS day,
  SUM(cost_micro_cents) / 1000000.0        AS dollars,
  COUNT(*)                                 AS calls
FROM ai_call
WHERE created_at >= now() - interval '30 days'
  AND status = 'success'
GROUP BY 1
ORDER BY 1 DESC;

各模型错误率

SELECT
  model,
  COUNT(*) FILTER (WHERE status = 'error')::float / COUNT(*) AS error_rate,
  COUNT(*)                                                   AS attempts
FROM ai_call
WHERE created_at >= now() - interval '7 days'
GROUP BY model
HAVING COUNT(*) > 10
ORDER BY error_rate DESC;

各模型 p95 总延迟

SELECT
  model,
  percentile_cont(0.95) WITHIN GROUP (ORDER BY total_ms) AS p95_ms
FROM ai_call
WHERE created_at >= now() - interval '24 hours'
  AND status = 'success'
  AND total_ms IS NOT NULL
GROUP BY model;

后台仪表盘

/admin/cost(被 adminActionClientuser.role === 'admin' 守护)读 ai_call 渲染上面三条查询,再加一个花费排行榜。要加自己的小组件就在 src/app/[locale]/(app)/admin/cost/ 下放一个 server component,import db,跑查询, 渲染,不需要额外接线。

验证生效

  1. 发一次 chat 调用 —— 任意模型任意 provider 都行。
  2. psqlSELECT provider, model, status, cost_micro_cents, total_ms FROM ai_call ORDER BY created_at DESC LIMIT 1; —— 应该有一行。
  3. 用 admin 身份打开 /admin/cost。「今日」总额应该反映出这次调用。
  4. 故意造个错误:传一个不存在的 model id。再查表,会多出一行 status = 'error'errorMessage 有内容。

常见坑

  • 价目表漏配 → 成本 = 0。 getTokenPrice 会回退到 mock:any,能跑出极小的数, 但凡是要真收钱的模型都该有显式一行。审计一下: SELECT model, SUM(cost_micro_cents) FROM ai_call GROUP BY model HAVING SUM(cost_micro_cents) < 100;
  • micro-cents 和 cents 弄混了(差 100 倍)。 花了两小时纳闷毛利怎么这么好看? 你除的是 10_000 而不是 1_000_000。
  • 忽略失败调用。 失败也要钱 —— Anthropic 对超过 TTFT 后失败的流照样收费。 「成功的占比」可以过 status = 'success',算成本要全部加起来。
  • 缓存命中看着像免费。 Manager 给缓存行打 cached = truecostMicroCents = 0。 这是对的(你没付钱),但做容量规划时也要把缓存行从延迟报表里排除。
  • ttftMs 在非流式时为空。 只有 operation = 'chat_stream' 才填 ttftMs。 对普通 chat 行聚合 ttftMs 全是 NULL

官方文档