Cost Attribution¶
SynthOrg records every LLM call with a CostRecord (src/synthorg/budget/cost_record.py) that carries enough dimensions to slice spend four ways: provider, model, agent, and project. This guide walks through reading the rollup, choosing the right query, and wiring alerts at each granularity.
Dimensions¶
| Dimension | Source | Cardinality |
|---|---|---|
| Provider | Provider driver name | ~10 |
| Model | Provider model identifier | ~50 |
| Agent | agent_id from the executing context |
Registry-bound (~100s) |
| Project | project_id from the task context |
Hundreds to thousands |
All dimensions are bounded label values when surfaced as Prometheus metrics; see docs/guides/monitoring.md for the registry-bound enforcement rule.
Querying the rollup¶
The cost API lives at /api/v1/costs. Three core endpoints:
GET /api/v1/costs/summaryreturns the current period totals across dimensions.GET /api/v1/costs/by/{dim}returns rollups for a single dimension (e.g.by/agent).GET /api/v1/costs/recordsreturns the raw record stream (paginated).
# Summary for the current billing month.
curl -s http://localhost:8000/api/v1/costs/summary \
-H "Authorization: Bearer $TOKEN" | jq
# Per-agent breakdown.
curl -s "http://localhost:8000/api/v1/costs/by/agent?since=2026-05-01" \
-H "Authorization: Bearer $TOKEN" | jq
# Per-project breakdown filtered to one project.
curl -s "http://localhost:8000/api/v1/costs/by/project?project_id=proj-acme" \
-H "Authorization: Bearer $TOKEN" | jq
Worked example: route a Slack alert at 80% project budget¶
Set the project budget in the company template:
budget:
projects:
proj-acme:
monthly: 250.00
currency: GBP
alerts:
warning_at: 50
critical_at: 80
hard_stop_at: 95
Configure the notification dispatcher to route critical alerts to Slack:
notifications:
channels:
slack:
enabled: true
webhook_url: https://hooks.slack.com/services/.../...
routing:
- event: budget.project.warning
channels: [slack]
severity: warning
- event: budget.project.critical
channels: [slack]
severity: critical
The enforcer fires BUDGET_PROJECT_BUDGET_EXCEEDED and the dispatcher routes the notification through the configured channels. On hard-stop (95% in the example), the project's tasks are auto-cancelled and a notifications.budget_exhausted.send event lands on the notification feed.
Aggregation under concurrency¶
CostTracker.record(...) is async and lock-guarded; concurrent writes from many agents collapse to a single durable append. The per-currency invariant (assert_currencies_match) protects against accidental cross-currency rollups; mixed-currency calls raise at record time rather than silently producing a wrong total.
Limitations¶
- The summary endpoint reports the billing period total. Daily totals come from
/api/v1/costs/by/agent?period=daywith the appropriate filter. - Per-tool cost is NOT a first-class dimension. Tools are observed via
synthorg_tool_invocations_total; cost attribution stops at the model + provider level. - Project assignment relies on
task.project_idbeing set; unassigned tasks aggregate under the implicitunassignedproject bucket.
Observability¶
synthorg_cost_total(gauge): total accumulated spend.synthorg_agent_cost_total(gauge,agent_idregistry-bound): per-agent cumulative spend.synthorg_budget_used_percent(gauge): monthly utilisation.synthorg_budget_daily_used_percent(gauge): daily utilisation (pro-rated).
Events emitted on every record:
budget.cost.recorded: at successful persistence.budget.cost.record_rejected: at currency mismatch.budget.enforcement.check: pre-flight budget check (allow / downgrade / deny).
See docs/design/budget.md for the full design.