跳转到主要内容
看不见就修不了。Manager 每次调用都往 ai_call 写一行 —— 成功、失败、取消都写 —— 包括 Token 数(provider 返回则记录)、延迟、错误信息。这一张表撑起了 /admin/usage 后台,也是你定位失控模型或挂掉 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'
  status          text NOT NULL DEFAULT 'pending'      // 'success'|'error'|'cancelled'|...
  streamed        boolean NOT NULL DEFAULT false
  cached          boolean NOT NULL DEFAULT false
  inputTokens     integer NULL                         // 仅文本类 provider 才有
  outputTokens    integer NULL                         // 仅文本类 provider 才有
  ttftMs          integer NULL                         // 仅流式
  totalMs         integer NULL
  errorMessage    text NULL
  metadata        jsonb DEFAULT '{}'
  createdAt       timestamp NOT NULL DEFAULT now()
}
userId(provider, model)statuscreatedAt 上的索引覆盖了后台要跑的所有查询。

为什么没有成本字段

我们故意不计算或存储美元成本。Provider 定价是个移动靶 —— 缓存折扣、微调加价、协议价档位 —— 任何本地副本几周就会和实际账单对不上。Vibestrap 的职责是记录”发生了什么”(调用、Token、延迟、错误);Provider 后台是”花了多少钱”的唯一权威来源 如果你的产品确实需要在 vibestrap 里看 $(比如你按 provider 调用次数对终端用户收费),自己加一列 cost_micro_cents + 自己写算法。schema 故意保留扩展空间。

为什么 Token 可以为 NULL

返回 Token 的 API(OpenAI / Anthropic / OpenRouter chat)会填 inputTokensoutputTokens。图像 / 视频 / 音频类 API(Replicate / fal.ai)通常不返回,那些行就保持 NULL。别把 NULL 当 0 加,要把它当成”不适用”。

步骤:写一份用量报告

三条直接打 ai_call 的查询。

近 30 天日调用量

SELECT
  date_trunc('day', created_at) AS day,
  COUNT(*)                       AS calls,
  COUNT(*) FILTER (WHERE status = 'success') AS successes
FROM ai_call
WHERE created_at >= now() - interval '30 days'
GROUP BY 1
ORDER BY 1 DESC;

各模型错误率

SELECT
  provider || '/' || model AS model,
  COUNT(*) AS calls,
  ROUND(100.0 * COUNT(*) FILTER (WHERE status = 'error') / COUNT(*), 2)
    AS error_pct
FROM ai_call
WHERE created_at >= now() - interval '7 days'
GROUP BY model
HAVING COUNT(*) >= 10
ORDER BY error_pct DESC;

各模型 p95 延迟

PostgreSQL 自带 percentile_cont
SELECT
  provider || '/' || model AS model,
  percentile_cont(0.5) WITHIN GROUP (ORDER BY total_ms) AS p50_ms,
  percentile_cont(0.95) WITHIN GROUP (ORDER BY total_ms) AS p95_ms
FROM ai_call
WHERE created_at >= now() - interval '7 days'
  AND total_ms IS NOT NULL
GROUP BY model
ORDER BY p95_ms DESC;

调用与积分的对应关系

每次成功的 chat 调用都走 manager 的 token 预扣 + 结算。同一行 ai_call 记录的 inputTokens / outputTokens 就是 tokensToCredits()(在 src/credits/index.ts)的输入。改了 siteConfig.credits.perKToken历史行不会重新换算 —— 只对新调用生效。 图像调用走 siteConfig.credits.perImage flat rate(不区分 provider)。如果你想按计算时长收费,自己在业务代码里覆盖。