This commit is contained in:
2026-08-23 22:52:13 +02:00
parent 8983cfc8a1
commit 296dc3c841
32 changed files with 2025 additions and 52 deletions
+62
View File
@@ -38,6 +38,68 @@ when the call failed.
log line, and the insert uses `context.WithoutCancel` so a cancelled job still
records the call that was in flight.
## Cost (Admin → AI costs)
`/admin/ai-costs` answers "what does each user cost me": spend by user, by company
and by model, with a grand total across all tenants.
It reads `ai_usage_daily`, a rollup written next to every captured call — one row
per day / company / user / model / role. That is why it can be kept for
`aiaudit.UsageRetentionDays` (**3 years**) while the prompt bodies expire in a week:
a busy tenant adds tens of rows a day, not one per product.
Money is **micro-USD integers** end to end (`cost_micros`). Summing millions of
fractional-cent calls as floats drifts; integers add up exactly. `cost_usd` in the
API is a display convenience — never re-sum it.
Prices live in `ai_model_prices`, seeded with the OpenAI GPT-5.6 list prices
effective 2026-07-30 (per 1M tokens):
| model | input | cached input | output |
|---|---|---|---|
| gpt-5.6-luna | $0.20 | $0.02 | $1.20 |
| gpt-5.6-terra | $2.00 | $0.20 | $12.00 |
| gpt-5.6-sol | $5.00 | $0.50 | $30.00 |
Update the table when list prices change (`UPDATE ai_model_prices …`). Recorded
cost is never recalculated, so history keeps the rate that applied at the time.
`aiaudit.defaultModelPrices` mirrors these as a code fallback for a model with no
row; an unpriced model records usage at zero cost rather than guessing.
Cached prompt tokens are read from `usage.prompt_tokens_details.cached_tokens` and
billed at the cached rate — they are a *subset* of prompt tokens, so they are
discounted, never added on top.
```
GET /api/admin/ai-costs?days=30&from=&to=&company_id=
```
### History from before capture existed
`cmd/backfill-ai-usage` recovers cost for products processed before this shipped,
reading the provider usage blocks still stored in `processed_products.gpt_response`.
```
go run ./cmd/backfill-ai-usage # dry run
go run ./cmd/backfill-ai-usage -apply
```
It is best effort, and the limits are real:
- **Only rows that still carry a usage block.** `gpt_response` is overwritten on
every reprocess and absent on older or failed rows.
- **No per-user attribution** — `processed_products.user_id` is null on those rows,
so recovered spend lands under "Unattributed" in the by-user view. By-company and
the grand total are complete.
- **The day is `updated_at`**, the last write to the row, not necessarily when the
call was made.
- **Cached-token discounts cannot be recovered**, so recovered input is priced as
fully fresh — an over-estimate, never an under-estimate.
Rows are written with `source='backfill'`, so a re-run replaces its own rows and can
never double-count live capture. By default the scan stops at the first day live
capture recorded; `-before YYYY-MM-DD` overrides that.
## Retention
Rows are large and high volume — one per product per language, a few KB each. They