> ## 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.

# Observability

> Every AI call lands in `ai_call` — tokens (when reported), latency, and status. The dashboard rolls it up.

You can't fix what you can't see. The manager writes one row to `ai_call` per
attempt — success, error, or cancelled — including tokens (if the provider
reports them), latency, and any error message. That single table powers the
admin dashboard at `/admin/usage` and is all you need to spot a runaway model
or a broken provider.

## Prerequisites

* Migrated DB so the `ai_call` table exists (`pnpm db:push`).
* A few real AI calls behind you — the table is empty until then.
* Read [`src/ai/manager.ts`](https://github.com/larryworld/vibestrap/blob/main/src/ai/manager.ts) `recordCall()` — that's where every row is born.

## The columns that matter

```ts theme={null}
// src/db/ai.schema.ts (excerpt)
ai_call {
  id              text PRIMARY KEY
  userId          text NULL                            // FK to 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                         // present for text APIs only
  outputTokens    integer NULL                         // present for text APIs only
  ttftMs          integer NULL                         // streaming only
  totalMs         integer NULL
  errorMessage    text NULL
  metadata        jsonb DEFAULT '{}'
  createdAt       timestamp NOT NULL DEFAULT now()
}
```

Indexes on `userId`, `(provider, model)`, `status`, `createdAt` cover the
queries the dashboard runs.

## Why no cost column

We deliberately don't compute or store dollar cost. Provider pricing is
moving target — caching tiers, fine-tune surcharges, agreement discounts —
and any local copy drifts within weeks. Vibestrap's job is to record what
happened (calls, tokens, latency, errors); your provider's billing
dashboard is the source of truth for what it cost.

If your product does need a cost view (e.g., you bill end-users by
provider call), add a `cost_micro_cents` column back with your own
estimation logic. The schema is intentionally trivial to extend.

## Why tokens can be NULL

Token-returning APIs (OpenAI / Anthropic / OpenRouter chat) populate
`inputTokens` and `outputTokens`. Image / video / audio APIs (Replicate /
fal.ai) typically do not — those columns stay NULL for those rows. Don't
sum them as "0" silently; treat NULL as "not applicable" in your queries.

## Step-by-step: add a usage report

Three useful queries straight against `ai_call`.

### Daily call volume, last 30 days

```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;
```

### Error rate per model

```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;
```

### Latency p95 per model

Postgres has `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;
```

## Tying calls to credits

Every successful chat call passes through the manager's token-based
credit reservation. The same `ai_call` row that records `inputTokens` /
`outputTokens` is the basis for `tokensToCredits()` (in
`src/credits/index.ts`). If you change `siteConfig.credits.perKToken`,
historical rows don't get re-priced — only future calls.

For images, the manager charges `siteConfig.credits.perImage` per
generated image, regardless of provider. Override in your own code
if you want compute-time-based pricing.
