Budgets and frontier (L6)¶
Outcome: Cap spend and configure ordered L6 providers.
Budgets¶
frontier:
enabled: true
daily_budget_usd: 5
monthly_budget_usd: 50
soft_budget_ratio: 0.8
Soft warnings then hard stop. The same frontier.soft_budget_ratio (default
0.8) drives USD soft bands: when spend/limit crosses the soft line but not
the hard cap, responses still succeed and add x-daari-budget-warning: soft
plus daari_meta.warning=budget_warning (OpenAI chat, Anthropic Messages, and
Responses when X-Daari-Meta is set). Prometheus increments
daari_soft_warnings_total{kind="budget"} on those 2xx responses; hard 402
bumps daari_rejects_total{kind="budget"} only. See
Response headers and
metrics.
Per-key budgets¶
A virtual key can carry its own caps, charged only against that key's own spend:
daari keys team-create eng --daily-budget 5
daari keys create ci-bot --daily-budget 2 --monthly-budget 20 --team eng --window 7d=10
daari keys create weekly-bot --window week=10
daari keys create contractor --window lifetime=50
Each window is checked independently. A key can carry several
{duration, max_usd} windows at once (day/24h/daily, week/weekly/rpw/1w
for ISO calendar weeks, month/30d/monthly, 7d, lifetime/total, …);
team caps apply to every key on that team and the tighter of key vs team wins.
The org frontier.* caps above remain an outer ceiling. A key over budget gets
402 naming the window that tripped and when it resets:
{
"error": {
"type": "budget_exceeded",
"message": "Virtual key daily frontier budget ($2.0000) exceeded — $2.4310 spent. Resets at 2026-08-28T00:00:00+00:00.",
"client_id": "ci-bot",
"window": "daily",
"budget_usd": 2.0,
"spend_usd": 2.431,
"reset_at": "2026-08-28T00:00:00+00:00",
"scope": "key"
}
}
Lifetime caps¶
lifetime (aliases: total, all) sums all-time frontier spend for the key
(or team) from the usage ledger — no calendar reset, no rollover. The 402
payload names window: "lifetime" and omits a reset time. Same shape works for
--window-requests lifetime=1000. Per-end-user lifetime caps use key metadata
user_lifetime_usd_cap (charged via the user ledger, scope: "user").
Temporary budget boosts¶
When a team hits its monthly cap mid-incident, grant a bounded, auto-expiring increase. Expiry is evaluated at enforcement time — no cron:
daari keys budget-boost <key_id> --usd 25 --until 2026-09-23T00:00:00Z
daari keys team-budget-boost eng --usd 100 --until 2026-09-23T12:00:00Z
daari keys show <key_id> # lists active boosts
Grant and expiry emit audit actions budget.boost.grant /
budget.boost.expire. Active boosts add to the effective USD (and optional
request) limit on every matching window for that scope.
Per-end-user caps on a shared key¶
When many humans share one virtual key (a team agent), pass the OpenAI user
field so spend is attributed per person. Optional --user-daily-cap denies only
the user who crossed it (scope: "user"); others on the same key keep working.
Requests without user are attributed to unknown and are never user-capped.
daari keys create shared-agent --user-daily-cap 2
daari usage --by-user
# or: daari report --by-user
GET /v1/daari/report includes a users array (client_id, user_id, counts).
Window rollover (opt-in)¶
By default unused headroom disappears at reset. Set rollover: true on a
window so leftover USD carries into the next period's effective limit:
{ "duration": "month", "max_usd": 100, "rollover": true, "rollover_cap_multiple": 2.0 }
CLI: daari keys create bot --window month=100:rollover.
At each period boundary, unused headroom (prev_effective − spent, floor 0)
becomes carry. Effective limit is min(base + carry, base × rollover_cap_multiple)
(default cap 2× the base) so idle keys cannot accumulate forever. Carry is
persisted on the usage ledger (budget_window_state) so SQLite and Postgres
replicas agree. Headers, 402 bodies, and budget alert webhooks all report the
effective limit and remaining.
Clients do not have to wait for the 402. Every successful response to a
budgeted key carries x-daari-budget-remaining / -limit / -window /
-reset / -scope for the window it will hit first (least USD left across
key and team), and the 402 repeats them with remaining 0 plus
Retry-After. In the soft band, successful responses also carry
x-daari-budget-warning: soft (omitted on hard 402). See
Response headers.
Operator alerts¶
A webhook fires when a request pushes a key or team window across a
threshold (default 80% and 100%) — USD or request-count quotas. Empty
URL disables. The POST is a background task: a down hook is logged
(budget.alert_failed) and never delays or fails the chat response.
alerts:
budget_webhook_url: https://hooks.example/daari-budget # Slack / ntfy / PagerDuty
budget_webhook_secret: secret://env-file/~/.daari/secrets.env#BUDGET_WEBHOOK # optional
budget_thresholds: [0.8, 1.0]
member_budget_thresholds: [] # optional; e.g. [0.8, 1.0]
member_budget_thresholds is empty by default, so key and team alerts are
unchanged. When a team key sets user_daily_usd_cap and this list is
non-empty, a named user crossing one of those ratios of the daily cap
POSTs the same webhook with scope: "member", team_id, user_id,
window, spent, and limit. That crossing fires once per member, window,
and threshold (Redis SET NX EX when configured; a Redis error still
delivers and logs budget.alert_dedupe_degraded). Signing uses the same
X-Daari-Signature secret.
Payload (never includes key material):
{
"scope": "key",
"id": "a1b2c3d4",
"name": "ci-bot",
"window": "daily",
"limit_usd": 5.0,
"spent_usd": 4.12,
"remaining_usd": 0.88,
"threshold": 0.8,
"reset_epoch": 1756944000
}
Request-quota crossings use the same envelope with quota: "requests" and
limit_requests / spent_requests / remaining_requests instead of the
USD fields.
When budget_webhook_secret is set (plaintext or a secret:// ref), every
POST carries X-Daari-Timestamp (unix seconds) and X-Daari-Signature
(HMAC-SHA256 hex of timestamp + "." + body over the exact request bytes).
Unset keeps the previous unsigned POST (no new headers). Reject timestamps
older than ~5 minutes to block replays.
Rotate without a hard cutover via daari alerts rotate-secret budget --secret NEW
(or key-expiry). That moves the current secret to
budget_webhook_secret_previous (and stamps
budget_webhook_secret_rotated_at). Signing always uses the new current;
during grace the header lists both digests comma-separated
(current,previous) so receivers can verify against either secret. Clear
*_webhook_secret_previous after all receivers accept the new secret —
daari doctor tips when a previous secret has been in place for ~7 days.
import hashlib, hmac, time
TOLERANCE = 300 # seconds
def verify(body: bytes, signature: str, secret: str, timestamp: str,
previous: str = "") -> bool:
if abs(time.time() - int(timestamp)) > TOLERANCE:
return False
secrets = [s for s in (secret, previous) if s]
digests = [p.strip().lower().removeprefix("v1=") for p in signature.split(",") if p.strip()]
for sec in secrets:
expected = hmac.new(
sec.encode(), f"{timestamp}.".encode() + body, hashlib.sha256
).hexdigest()
if any(hmac.compare_digest(expected, d) for d in digests):
return True
return False
Each (scope, id, window, threshold, reset_epoch) fires at most once until
that window resets. With cache.backend: redis (the same Redis as L0/L1 and
rate limits), the fleet claims that tuple with SET NX EX before delivery;
the TTL runs until the window reset so keys expire on their own. Without
Redis, dedupe stays in-memory on the process that handled the request — two
replicas can notify twice. A Redis error still delivers the alert and logs
budget.alert_dedupe_degraded (never drop a page because the claim store
is down). Each fire writes a budget.alert audit row and increments
daari_budget_alerts_total{scope,threshold}.
Key expiry alerts¶
Budget pages catch spend cliffs; key-expiry alerts catch credential rotation
before a 90-day key goes dark. The same alerts block opts in a proactive
plane (doctor + Prometheus + webhook) that does not wait for a 401
key_expired at request time:
alerts:
key_expiry_webhook_url: https://hooks.example/daari-key-expiry # empty disables
key_expiry_webhook_secret: secret://env-file/~/.daari/secrets.env#KEY_EXPIRY_WEBHOOK # optional
key_expiry_warn_days: 14 # doctor / gauge / webhook window (env: DAARI_ALERTS__KEY_EXPIRY_WARN_DAYS)
daari doctorwarns on keys entering the warn window and counts expired-but-not-revoked (key_expirycheck). When a webhook URL is set without a signing secret, doctor tipskey_expiry_webhook_secret.- Scrape-time gauge
daari_virtual_keys{status}exposesactive,expired,expiring_soon, andrevoked(stock Grafana: Virtual keys by status). See Prometheus metrics. - When
key_expiry_webhook_urlis set, each scrape that sees a key enterexpiring_soonPOSTs once (RedisSET NXdedupe when available; fail-open on Redis errors). Down hooks logkey_expiry.alert_failedand never delay the scrape.
Payload (never includes key material or the secret prefix):
{
"event": "key_expiry",
"status": "expiring_soon",
"key_id": "a1b2c3d4",
"name": "ci-bot",
"expires_at": "2026-10-24T12:00:00+00:00",
"warn_days": 14
}
key_expiry_webhook_secret reuses the budget signing headers:
X-Daari-Timestamp + X-Daari-Signature (HMAC-SHA256 of
timestamp + "." + body). Verify with the same helper as budget webhooks
above (including key_expiry_webhook_secret_previous during rotation).
Unset keeps an unsigned POST. Rotate with
daari alerts rotate-secret key-expiry --secret NEW.
Existing keys that only have daily_budget_usd / monthly_budget_usd are
migrated to day / month windows on first open; behavior is unchanged.
Only frontier (L6) usage counts toward USD budgets. Local tiers and cache
hits are free and never consume a USD budget. Spend is priced per model from
pricing.models, so a key's remaining allowance reflects the models it
actually used.
Request-count quotas¶
USD caps cannot meter shared local GPU capacity: local tiers cost $0.00, so a
key can monopolize the fleet without touching a budget window. RPM only bounds
bursts. Request-count quotas close that gap.
daari keys create gpu-bot --window-requests 1d=5000 --window 1d=5
daari keys team-create eng --window-requests 1d=20000
YAML / SSO budget_windows entries accept the same shape:
budget_windows:
- duration: day
max_usd: 5
max_requests: 5000
Semantics match USD windows: multiple durations are checked independently,
team caps apply to every key on the team, and the tighter of key vs team wins
per dimension (USD and requests merge independently). Counting is
ledger-backed (requests − cache_hits on client_usage), so quotas hold
across replicas when observability.backend=postgres. Local-tier requests
count; L0/L1 cache hits do not — a cache hit costs nothing to serve.
Exceeding a request quota returns the same 402 budget_exceeded shape with
quota: "requests", budget_requests / spend_requests, plus
x-daari-quota-requests-remaining / -limit (and Retry-After). Successful
responses for a quota-bound key carry the remaining/limit headers so clients
can back off before the hard stop. daari keys list prints live
req used/cap next to USD usage for each window (appends soft when the
window is in the soft band). daari report / GET /v1/daari/report include
a request_quotas array with the same used/cap/remaining/soft fields.
Soft band (same frontier.soft_budget_ratio, default 0.8): when used/cap
crosses the soft line but not the hard cap, responses still succeed and add
x-daari-quota-requests-warning: soft plus daari_meta.warning =
"request_quota_warning" on OpenAI chat, Anthropic Messages, and Responses
(when X-Daari-Meta is set). Operator webhooks from
alerts.budget_webhook_url also fire on request-quota threshold crossings
with quota: "requests" and limit_requests / spent_requests /
remaining_requests (USD payloads unchanged).
Pricing¶
Spend is computed per model and per direction from pricing.models, in USD per
1M tokens:
pricing:
models:
gpt-4o:
input_per_1m: 2.50
output_per_1m: 10.00
cached_input_per_1m: 1.25
my-self-hosted-model:
input_per_1m: 0.0
output_per_1m: 0.0
Names match on longest prefix, so a gpt-4o entry also prices
gpt-4o-2024-08-06. A vendor prefix resolves the same way:
anthropic.claude-fable-5-1 and models/gemini-3.8-flash use the shipped
id. Anything unmatched falls back to the flat
usage.frontier_price_per_1k_tokens, which ignores direction and will misprice a
model whose output rate differs sharply from its input rate. daari doctor warns
about models being billed at that fallback, so add an entry when you adopt a new
model or your budgets will drift from your real invoice.
Shipped list prices (captured 2026-09-07)¶
The default table still includes the 2024 entries (gpt-4o, gpt-4o-mini,
claude-3-5-sonnet, claude-3-5-haiku, claude-3-opus) at their previous
rates. Current flagship and workhorse models, USD per 1M tokens:
| Model | Input | Cached input | 1h cache write | Output | Source |
|---|---|---|---|---|---|
claude-fable-5-1 |
10.00 | 0.25 | 20.00 | 50.00 | Fable 5.1 |
claude-opus-5 |
5.00 | 0.50 | 10.00 | 25.00 | Anthropic pricing |
claude-opus-5-5 |
4.00 | 0.20 | 8.00 | 20.00 | LiteLLM 1.104 / Anthropic Opus 5.5; cache read halved 2026-10-07; non-default temperature/top_p/top_k stripped |
claude-sonnet-5 |
2.00 | 0.20 | 4.00 | 10.00 | Anthropic pricing |
claude-sonnet-5-5 |
2.00 | 0.10 | 4.00 | 10.00 | Sonnet 5.5; cache read halved 2026-10-07; non-default temperature/top_p/top_k stripped |
claude-haiku-4-5 |
1.00 | 0.10 | 2.00 | 5.00 | Anthropic pricing |
claude-haiku-5-5 |
0.10 | 0.01 | 0.20 | 0.50 | Haiku 5.5 (2026-10-07); 5m write $0.125 (≥100K $0.625); 1h write $0.20 (≥100K $1.00); adaptive thinking maps budget_tokens → effort |
gpt-6-astra |
10.00 | 1.00 | — | 50.00 | OpenAI; ≥272K input → $20 / $2 / $75 |
gpt-6.1-sol |
2.00 | 0.10 | 2.50 | 10.00 | OpenAI (2026-09-29); 5× cheaper than Astra; no published above-272K surcharge |
chat-latest |
5.00 | 0.50 | — | 30.00 | Rolling ChatGPT Instant alias (updated 2026-10-07); underlying snapshot moves — prefer gpt-6-astra for production pins |
gpt-6-luna |
0.10 | 0.01 | — | 0.50 | Chat/Responses. POST /v1/decisions is input-only: $0.10 in, $0 out, $0 cache |
gpt-5.6 / gpt-5.6-sol |
4.00 | 0.40 | — | 20.00 | OpenAI promo through 2026-11-21 (Sol) |
gpt-5.6-terra |
2.00 | 0.20 | — | 12.00 | OpenAI API pricing |
gpt-5.6-luna |
0.20 | 0.02 | — | 1.20 | OpenAI API pricing |
gemini-3.8-flash |
0.75 | 0.075 | — | 3.75 | Gemini pricing intro rate through 2026-12-31 |
grok-4.7 |
2.00 | 0.50 | — | 6.00 | xAI / LiteLLM 1.105 RC lab bar (2026-10-04); OpenRouter ~$1.6/$4.8 |
gpt-5.6 is the Sol alias. Longer keys win, so gpt-5.6-luna is not priced
as Sol. Gemini 3.8 Flash's published standard rate becomes $1.50 / $7.50 on
2027-01-01; the shipped default is the intro rate in effect now. Override
pricing.models when that date passes or when a provider changes a quote.
Context-threshold pricing¶
Optional fields on a pricing.models entry switch the entire request to a
higher rate once prompt tokens reach a threshold (OpenAI's long-context
surcharge pattern):
pricing:
models:
gpt-6-astra:
input_per_1m: 10.0
output_per_1m: 50.0
cached_input_per_1m: 1.0
input_threshold_tokens: 272000
above_input_per_1m: 20.0 # 2×
above_output_per_1m: 75.0 # 1.5×
Below the threshold, rates are unchanged. At or above it, input/output (and a
proportionally scaled cached-input rate) use the above_* values. Cost
headers, the usage ledger, and budget enforcement all go through the same
cost_usd path, so a 402 can fire before a 2× request is dispatched.
Service-tier pricing¶
OpenAI / Anthropic / OpenRouter service_tier is forwarded on L6 hops and
multiplies cost_usd: flex is 0.5×, priority / fast are 2× (OpenAI
renamed Priority to Fast mode; both names bill the same), ultrafast is 6×
for gpt-6.1-sol and gpt-6-astra (Astra Ultrafast is US/global only — an
region_pin: eu key clears the tier with daari_meta.warning=
ultrafast_eu_unsupported; unsupported models get
ultrafast_unsupported_model), standard / default / auto / missing is
1×. Unknown values log service_tier_ignored and bill as standard.
OpenAI regional processing (data residency) adds a further 1.10× uplift on
models released on or after 2026-03-05 when a key/team region_pin is us or
eu; unset pin bills the standard (or Fast/Ultrafast) rate with no residency
premium (daari doctor pricing notes). FedRAMP routing is not invented here.
Anthropic cache_control TTL write rates¶
Anthropic prompt-cache markers on system/tool blocks (cache_control.ttl
5m or 1h) are preserved on the L6 Messages payload. Default auto-hint
remains {type: ephemeral} (5-minute). When the request asks for ttl: 1h,
cost_usd(..., cache_write_tokens=…, cache_ttl="1h") bills writes at
cache_write_1h_per_1m (2× base input on the shipped Claude table). Missing
TTL or 5m keeps today's rate (write tokens at input_per_1m).
Providers / fallback¶
Configure frontier.providers (ordered list) for OpenAI-compatible bases, Anthropic, OpenRouter, etc. Circuit breakers and key rotation ship with the L6 pool. Each entry may set timeout_s, retry_attempts, and retry_backoff_s; unset fields inherit upstream.frontier_timeout_seconds / upstream.retry. A provider's exhausted retries advance the chain, and one slot's timeout does not consume the next slot's budget. POST /v1/daari/route/preview and daari route preview show the resolved policy on policy / chain. A provider whose provider is anthropic/claude, or whose base_url contains anthropic.com, is sent native Messages API payloads (POST …/messages, x-api-key) rather than an OpenAI /chat/completions body.
Clients may send OpenRouter's provider object (zdr, sort, order, max_price, …). daari stores it on the request, passes it through when the L6 slot is OpenRouter, and fails closed (HTTP 400) if zdr: true and no configured slot declares zdr: true. Chosen provider, usage.cost, and cached tokens land in daari_meta.
OpenRouter slot (G3)¶
Use their catalog; do not clone it. The documented slot:
frontier:
enabled: true
providers:
- id: openrouter
base_url: https://openrouter.ai/api/v1
model: openrouter/auto # or anthropic/claude-sonnet-4.5, etc.
api_key_env: OPENROUTER_API_KEY
zdr: false # set true if the key is ZDR-only
# timeout_s: 30 # optional; else upstream.frontier_timeout_seconds
# retry_attempts: 2 # optional; else upstream.retry.attempts
# retry_backoff_s: 0.5 # optional; else upstream.retry.base_delay_ms / 1000
OPENROUTER_API_KEY (or DAARI_FRONTIER_API_KEY) is BYOK — never committed. Outbound calls send HTTP-Referer and X-Title: daari for app attribution. L6 daari_meta records cost_usd (upstream) and daari_cost_usd: 0.
Local model suffixes: daari:floor is the smallest capable local tier (L3); daari:nitro prefers a warm / low-latency local backend. These do not call OpenRouter.
API keys via environment (never commit):
export DAARI_FRONTIER_API_KEY=sk-...
# or provider-specific envs documented in config reference
PII / slim¶
frontier:
scrub_pii: true
slim_prompts: true
Verify¶
Force L6 only in a test env with a tiny budget; confirm soft warning then block in traces/daari_meta.