Billing — Per-Tenant Usage API
dgx billing-data ist token-scoped und kommt aus dem append-only audit-log. Endpoint liefert stündlich oder täglich aggregierte cost-buckets für deinen tenant.
Vollständige interne Architektur:
docs/BILLING.md(Cost-Pipeline ADR 0010, Tenant-Markup, OpenRouter-Reconciliation).
GET /v1/billing/usage
Query-Params
| Param | Type | Default | Beispiel |
|---|---|---|---|
from | ISO 8601 UTC (required) | — | 2026-05-16T00:00:00Z |
to | ISO 8601 UTC (required) | — | 2026-05-16T23:59:59Z |
granularity | hour | day | hour | hour |
Range maximal 31 Tage. to >= from. Granularity-Buckets sind UTC.
Response
{
"tenant_id": "godelmann-gocreate-test",
"from": "2026-05-16T00:00:00+00:00",
"to": "2026-05-16T23:59:59+00:00",
"granularity": "hour",
"bucket_count": 13,
"buckets": [
{
"bucket_start": "2026-05-16T15:00:00+00:00",
"bucket_end": "2026-05-16T16:00:00+00:00",
"total_requests": 42,
"total_cost_eur": 1.37,
"total_cost_usd": 1.62,
"total_upstream_cost_usd": 0.981052,
"by_model": {
"godelmann-gocreate-private-llama-text": {
"requests": 30, "cost_eur": 0.00, "cost_usd": 0.00, "upstream_usd": 0.0
},
"godelmann-gocreate-premium-gemini-image-standard": {
"requests": 12, "cost_eur": 1.20, "cost_usd": 1.40, "upstream_usd": 0.815
}
},
"by_cost_source": { "upstream": 12, "zero": 30 }
}
],
"total": {
"requests": 317,
"cost_eur": 1.37,
"cost_usd": 1.62,
"upstream_cost_usd": 0.981052
}
}
Felder
| Feld | Bedeutung |
|---|---|
bucket_start / bucket_end | UTC-Boundaries des Buckets (start=inclusive, end=exclusive) |
total_cost_eur | EUR-Total inkl. tenant_markup + FX_MARKUP (5% ECB-Aufschlag), ceil-zu-cent pro request |
total_cost_usd | USD-Total back-converted aus EUR via ECB-rate (pure, ohne markups) — was du in EUR zahlst, in USD ausgedrückt |
total_upstream_cost_usd | RAW upstream-USD VOR markups (für diff-checks vs OpenRouter / Anthropic) |
by_model | Per-model-Aggregation. "(unknown)" für Events ohne joined http-audit-record |
by_cost_source | Counter pro cost_source: upstream (cost vom Provider), zero (local-backend), free (free-tier), unknown |
Beispiele
Heutiger Tag stundengenau
TODAY=$(date -u +%Y-%m-%d)
curl -H "Authorization: Bearer $BEARER" -H "SPASS-User-Id: $USR" \
"https://dgx.spass.fun/v1/billing/usage?from=${TODAY}T00:00:00Z&to=${TODAY}T23:59:59Z&granularity=hour" \
| jq '.total'
Letzte 7 Tage Tagesübersicht
TO=$(date -u +%Y-%m-%d)
FROM=$(date -u -d "7 days ago" +%Y-%m-%d)
curl -H "Authorization: Bearer $BEARER" -H "SPASS-User-Id: $USR" \
"https://dgx.spass.fun/v1/billing/usage?from=${FROM}T00:00:00Z&to=${TO}T23:59:59Z&granularity=day" \
| jq '.buckets[] | {date: .bucket_start[0:10], requests: .total_requests, cost_eur: .total_cost_eur}'
Filter auf einzelnes Model
curl ... | jq '.buckets[] | {bucket: .bucket_start, image_cost: .by_model["godelmann-gocreate-premium-gemini-image-standard"].cost_eur}'
Cost-Pipeline (zusammengefasst)
eur = original_upstream_usd × rate_eur_per_usd × 1.05 × tenant_markup → ceil zu cent
usd = eur / rate_eur_per_usd
rate_eur_per_usd: ECB-mirror (frankfurter), refresh ≤25h, fallback ≤30d1.05(FX_MARKUP): security-margin gegen FX-Volatilitättenant_markup: yaml-config intokens.yaml::tenants[].defaults.cost_markup_factor(z.B.1.1= 10%)
Auth + Scope
- Jeder authentifizierte Bearer-Token sieht NUR seinen eigenen tenant (=
identity.tenant_id) - Role-agnostisch — sowohl
tenant_userals auchtenant_adminsehen aggregierte buckets ihres tenants
Was im Audit-Log enthalten ist
- Alle chat-completion-requests inkl. status-code, latency, model, cost-event
- Persist auch nach chat-delete —
DELETE /c1/conversations/<id>betrifft nur conversation-store, NICHT audit-log - Append-only — Files heißen
audit.jsonl.YYYY-MM-DDper UTC-day, nie überschrieben
Was (noch) NICHT enthalten ist
- Orphan-requests (Client-Disconnect ohne erfolgreiche response) — werden bis Cut 2.40a manuell oder via
dgx-admin reconcile-openrouteraufgeholt - Per-user-Aggregation — heute nur per-tenant; user_id ist im audit, aber response-schema gruppiert per-bucket statt per-user (ggf. follow-up)
Errors
| HTTP | Code | Wann |
|---|---|---|
| 400 | invalid_field (from/to) | malformed ISO 8601 |
| 400 | invalid_field (granularity) | nicht hour/day |
| 400 | invalid_field (to) | range > 31 Tage |
| 401 | unauthorized | bearer fehlt/invalid |
| 500 | storage_error | audit-log nicht lesbar (operational-bug, sollte nicht passieren) |
Siehe /docs/errors für volle Error-Tabelle.