Hadrian is experimental alpha software. Do not use in production.
Hadrian
Features

Budget Enforcement

Prevent cost overruns with atomic budget reservation and real-time enforcement

Budget enforcement blocks requests once an API key's spend limit is reached, using an atomic reservation pattern that stays accurate under concurrent load.

Overview

The budget system provides:

  • Per-key limits - Each API key can carry a budget_limit_cents and a budget_period (daily or monthly)
  • Atomic reservation - Reserve an estimated cost before the request, adjust to actual cost after completion
  • Warning thresholds - Response headers, audit entries, and WebSocket events before the hard limit
  • Time-series forecasting - Predict when budgets will be exhausted

Budgets are set per API key at creation time. There are no separate budget entities at the organization, team, project, or user level; scope a budget by choosing which owner the key belongs to.

How It Works

The atomic reservation pattern prevents overspend even with concurrent requests:

1. Request arrives
   └─ Reserve estimated cost ($0.10 default) against the spend counter
   └─ Check: current_spend + reservation > limit?
      ├─ Yes → Reject request (402 Payment Required)
      └─ No  → Continue

2. Forward to LLM provider
   └─ Stream response

3. Request completes
   └─ Calculate actual cost from token usage
   └─ Adjust: replace estimate with actual cost
      (adjustment = actual - estimated)

4. On failure/cancellation
   └─ Refund: remove reservation entirely

The check and the reservation happen in a single atomic cache operation, so 100 concurrent requests cannot slip past the limit together.

The reservation is capped at the key's own limit. A key whose budget is smaller than the configured estimate (for example a 5 cent budget with a 10 cent estimate) reserves at most its limit, so small budgets stay usable instead of rejecting every request.

Non-streaming requests settle immediately after the handler returns, using the cost computed for the response. Streaming requests settle from the streaming usage logger once the stream ends, including the partial-usage path when a client disconnects mid-stream. Exactly one side performs the adjustment.

Quick Start

Set a budget on an API key via the Admin API:

# Create an API key with a $100/month budget
curl -X POST http://localhost:8080/admin/v1/api-keys \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "production-api-key",
    "owner": {
      "type": "project",
      "project_id": "550e8400-e29b-41d4-a716-446655440000"
    },
    "budget_limit_cents": 10000,
    "budget_period": "monthly"
  }'
Budget limits are specified in cents. So 10000 = $100.00.

Users can also set budgets on their own keys through the self-service endpoint POST /admin/v1/me/api-keys, which accepts the same budget_limit_cents and budget_period fields (the owner is set to the current user automatically).

Budget Configuration

API Key Level

Budgets are configured per API key at creation:

FieldTypeDescription
budget_limit_centsintegerBudget limit in cents (e.g., 10000 = $100.00)
budget_periodstring"daily" or "monthly"

The owner field is a tagged object that determines whose usage the key represents:

typeID fieldScope
organizationorg_idShared organization-wide key
teamteam_idTeam workloads
projectproject_idProject isolation
useruser_idIndividual user
service_accountservice_account_idMachine identity

All IDs are UUIDs.

API keys are immutable: there is no PATCH or PUT endpoint. To change a budget, rotate the key (POST /admin/v1/api-keys/{key_id}/rotate) or revoke and recreate it with new values.

Gateway Settings

The [limits.budgets] section in hadrian.toml accepts exactly two keys:

[limits.budgets]
# Warning threshold as a fraction of the limit (default 0.8 = 80%)
warning_threshold = 0.8

# Estimated cost per request in cents, reserved before the request runs (default 10)
estimated_cost_cents = 10
KeyTypeDefaultDescription
warning_thresholdfloat0.8Fraction of the limit at which warnings trigger
estimated_cost_centsinteger10Pre-request reservation, adjusted after completion

Unknown keys in [limits.budgets] fail startup with a config error. There are no default budget amounts, exceeded actions, or overage settings; the only enforcement behavior is block-at-limit, and budget amounts live on each API key.

Warning Thresholds

When the spend counter reaches warning_threshold of the limit, the gateway:

  1. Publishes a budget_threshold_reached WebSocket event for real-time dashboards
  2. Logs a budget.warning audit entry (deduplicated: once per API key per period)
  3. Adds warning headers to the response

Warning Response Headers

HeaderExampleDescription
X-Budget-WarningtrueFlag indicating warning state
X-Budget-Spend-Percentage0.85Fraction of the limit consumed
X-Budget-Current-Spend-Cents850Current spend counter in cents
X-Budget-Limit-Cents1000Budget limit in cents
X-Budget-PeriodmonthlyBudget period

X-Budget-Current-Spend-Cents reports the cache counter, which includes in-flight reservations, including the current request's own reservation, not just settled spend. A fresh key's first request can report the estimate (10 cents by default) as current spend before any cost has settled.

Error Responses

When a budget is exceeded, the gateway returns 402 Payment Required (no Retry-After header):

{
  "error": {
    "type": "budget_error",
    "message": "Budget limit exceeded for monthly period: current spend plus in-flight reservations (9995 cents) leave no room for this request's estimated cost within the 10000 cent limit",
    "param": null,
    "code": "budget_exceeded",
    "request_id": "req_abc123"
  }
}

The current spend and limit are folded into the message text. The response body deliberately keeps the OpenAI error shape with no structured details object; the structured values are recorded in the budget.exceeded audit log entry instead.

Cache Requirements

Budget enforcement requires a cache backend ([cache] with type = "memory" or type = "redis"). Without a cache, budgets are silently not enforced: the limits check is skipped entirely, requests succeed, and no error is returned. If you set budgets on API keys, verify that a cache is configured.

Single-Node Deployment

In-memory cache is sufficient for a single node:

[cache]
type = "memory"

Spend counters live only in the cache. With type = "memory", a restart zeroes all counters and enforcement never reconciles against the usage_records table, so spend accumulated before the restart is forgotten. Use Redis if budget durability matters.

Multi-Node Deployment

Redis is required for shared budget state across nodes:

[cache]
type = "redis"
url = "redis://localhost:6379"

The atomic reservation pattern uses:

  • In-memory: Compare-And-Swap (CAS) loops on AtomicI64 counters
  • Redis: Lua scripts for atomic check-and-reserve operations

Cache Key Format

Budget spend is tracked with keys like:

gw:spend:{<api_key_id>}:<period>:<date>

Example: gw:spend:{550e8400-e29b-41d4-a716-446655440000}:daily:2026-07-25

The braces around the API key ID are literal: they are a Redis Cluster hash tag that keeps all keys for one API key on the same cluster slot. Monthly keys use a YYYY-MM date suffix.

Entries use a fixed full-period TTL from first write (24 hours for daily, 31 days for monthly) rather than expiring exactly at the period boundary. The date suffix in the key isolates periods, so a new period always starts at zero with a new key, and the fixed TTL prevents a long-running request from outliving its counter.

Audit Logging

Budget events are logged to the audit log:

ActionTrigger
budget.warningSpend crosses the warning threshold (once per key per period)
budget.exceededRequest blocked at the budget limit (every blocked request)

Both entries record the API key as the actor plus org_id and project_id where known, and a details object with limit_cents, current_spend_cents, period, request_path, and request_id (warnings also include spend_percentage).

Query them via the Admin API:

curl "http://localhost:8080/admin/v1/audit-logs?action=budget.exceeded" \
  -H "Authorization: Bearer $ADMIN_TOKEN"

WebSocket Events

Real-time budget events are published for dashboards. The same event type fires for warnings (with the actual percentage) and for exceeded budgets (with threshold_percent: 100):

interface BudgetThresholdReached {
  event_type: "budget_threshold_reached";
  timestamp: string;
  budget_type: "daily" | "monthly";
  threshold_percent: number; // 100 = exceeded
  current_amount_microcents: number;
  limit_microcents: number;
  user_id: string | null;
  org_id: string | null;
  project_id: string | null;
}

Time-Series Forecasting

The gateway includes time-series forecasting powered by augurs to predict budget exhaustion. Forecasts are available at GET /admin/v1/api-keys/{key_id}/usage/forecast, with equivalent endpoints for organizations, projects, users, and providers.

Algorithm

Data AvailableMethodDescription
14+ daysMSTL + AutoETSSeasonal decomposition with weekly patterns
7-13 daysAutoETSExponential smoothing without seasonality
< 7 daysNoneInsufficient data for forecasting

MSTL (Multiple Seasonal-Trend decomposition using Loess) captures weekly patterns like higher weekday usage vs. weekends.

Forecast Response

{
  "current_spend_microcents": 5000000,
  "budget_limit_microcents": 10000000,
  "budget_period": "monthly",
  "avg_daily_spend_microcents": 250000,
  "std_dev_daily_spend_microcents": 50000,
  "sample_days": 21,
  "days_until_exhaustion": 20.0,
  "projected_exhaustion_date": "2026-08-14",
  "days_until_exhaustion_lower": 16.7,
  "days_until_exhaustion_upper": 25.0,
  "budget_utilization_percent": 50.0,
  "projected_period_spend_microcents": 7750000,
  "time_series_forecast": {
    "dates": ["2026-07-26", "2026-07-27", "2026-07-28"],
    "point_forecasts": [260000, 255000, 180000],
    "lower_bounds": [200000, 195000, 120000],
    "upper_bounds": [320000, 315000, 240000],
    "confidence_level": 0.95,
    "used_seasonal_decomposition": true
  }
}

Costs are stored in microcents (1/10,000 of a cent, so 1/1,000,000 of a dollar) for precision. 1,000,000 microcents = $1.00.

Forecast Fields

FieldDescription
days_until_exhaustionEstimated days until budget runs out
projected_exhaustion_dateCalendar date of projected exhaustion
days_until_exhaustion_lower95% confidence lower bound (faster exhaustion)
days_until_exhaustion_upper95% confidence upper bound (slower exhaustion)
budget_utilization_percentCurrent period utilization
projected_period_spend_microcentsProjected total spend by period end
time_series_forecastMulti-day point forecasts with intervals

Pricing Configuration

Budget enforcement relies on accurate cost calculation. Pricing values are integer microcents per 1M tokens:

[pricing]
# Where the recorded cost comes from:
#   "prefer_provider"  - use provider-reported cost when present, else calculated (default)
#   "calculated_only"  - always calculate from token counts and configured pricing
#   "provider_only"    - only use provider-reported cost, record zero otherwise
cost_source = "prefer_provider"

[pricing.pricing.openai."gpt-4o-mini"]
input_per_1m_tokens = 150000    # $0.15 per 1M input tokens
output_per_1m_tokens = 600000   # $0.60 per 1M output tokens

Pricing can also be attached to a model directly in the provider config, which takes precedence:

[providers.openai.models."gpt-4o-mini"]
input_per_1m_tokens = 150000
output_per_1m_tokens = 600000

Resolution order, highest priority first:

  1. [providers.<name>.models."<model>"] per-provider model pricing
  2. [pricing.pricing.<provider>."<model>"] explicit pricing config
  3. The bundled models.dev catalog, used automatically for known models

Models with no pricing from any source record zero cost, so their requests only consume budget transiently (the reservation is refunded at settlement).

Usage Analytics

The admin panel provides usage dashboards for monitoring spend and token consumption across the multi-tenancy hierarchy.

Admin Usage Page

The dedicated Usage page (/admin/usage) supports multi-dimensional filtering:

FilterDescription
OrganizationRequired; scopes all data to a single org
TeamNarrow to a specific team within the org
ProjectNarrow to a specific project
UserNarrow to a specific user
API KeyNarrow to a single API key

Filters follow a priority order: API Key > User > Project > Team > Organization. The most specific filter wins.

The dashboard displays:

  • Summary cards - Total cost, total requests, input/output tokens
  • Cost over time - Daily spend line chart
  • Cost by model - Breakdown pie chart
  • Model details table - Per-model token and cost breakdown

Self-Service Usage

Non-admin users can view their own usage at /usage in the web UI. This page calls the self-service API endpoints (/admin/v1/me/usage/*) which require only standard authentication, not admin privileges.

Usage records and dashboards are the durable source of truth for reporting. The budget spend counter in the cache is a separate, enforcement-only value and the two are never reconciled.

Best Practices

  1. Set warning thresholds - Use 0.7-0.8 to get alerts before hitting limits
  2. Use Redis for production - Required for multi-node deployments and for counters that survive restarts
  3. Verify a cache is configured - Without one, budgets are silently not enforced
  4. Monitor forecasts - Review days_until_exhaustion to proactively adjust budgets
  5. Scope appropriately - Use project-owned keys for isolation, organization-owned keys for shared budgets
  6. Configure pricing for custom models - Models missing from the catalog record zero cost and never consume budget

On this page