> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vibestrap.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# 可观测性

> 每次 AI 调用都落 `ai_call` —— Token（若 provider 返回）、延迟、状态。仪表盘做聚合。

看不见就修不了。Manager 每次调用都往 `ai_call` 写一行 —— 成功、失败、取消都写 —— 包括 Token 数（provider 返回则记录）、延迟、错误信息。这一张表撑起了 `/admin/usage` 后台，也是你定位失控模型或挂掉 provider 唯一需要看的地方。

## 前置条件

* 数据库已迁移（`pnpm db:push`），`ai_call` 表存在。
* 实际跑过几次 AI 调用 —— 不然表是空的。
* 看一遍 [`src/ai/manager.ts`](https://github.com/larryworld/vibestrap/blob/main/src/ai/manager.ts) 的 `recordCall()`，每行数据都是从这里诞生的。

## 关键字段

```ts theme={null}
// 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)`、`status`、`createdAt` 上的索引覆盖了后台要跑的所有查询。

## 为什么没有成本字段

我们故意不计算或存储美元成本。Provider 定价是个移动靶 —— 缓存折扣、微调加价、协议价档位 —— 任何本地副本几周就会和实际账单对不上。Vibestrap 的职责是记录"发生了什么"（调用、Token、延迟、错误）；**Provider 后台是"花了多少钱"的唯一权威来源**。

如果你的产品确实需要在 vibestrap 里看 \$（比如你按 provider 调用次数对终端用户收费），自己加一列 `cost_micro_cents` + 自己写算法。schema 故意保留扩展空间。

## 为什么 Token 可以为 NULL

返回 Token 的 API（OpenAI / Anthropic / OpenRouter chat）会填 `inputTokens` 和 `outputTokens`。图像 / 视频 / 音频类 API（Replicate / fal.ai）通常不返回，那些行就保持 NULL。**别把 NULL 当 0 加**，要把它当成"不适用"。

## 步骤：写一份用量报告

三条直接打 `ai_call` 的查询。

### 近 30 天日调用量

```sql theme={null}
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;
```

### 各模型错误率

```sql theme={null}
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`：

```sql theme={null}
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）。如果你想按计算时长收费，自己在业务代码里覆盖。
