Skip to content

Virtual keys

Outcome: Issue scoped keys with budgets, RPM, and TPM limits.

Steps

daari keys team-create eng --daily-budget 5 --rpm 120 --tpm 80000 --rpd 5000
daari keys create ci --daily-budget 2 --rpm 60 --tpm 40000 --rpd 2000 --team eng --window 7d=10
daari keys create shared-agent --user-daily-cap 2
daari keys list
daari keys list --status active
daari keys list --idle-days 90
daari report --by-team
daari usage --by-user

--rpd is requests per UTC day on the key or team. 0, or omitting the flag, means unlimited. daari keys list --status filters active / expired / revoked. --idle-days N returns keys whose last_used_at is older than N days (or never used). Successful auth updates last_used_at (throttled to once per 60s per key).

Enable in config:

server:
  virtual_keys:
    enabled: true

Use the plaintext secret once as Authorization: Bearer ….

Model allowlists

Restrict a key or a team to specific models. Patterns are exact names or globs (claude-*). Named groups are defined once in settings and referenced by keys and teams. A key allowlist intersects the team allowlist: the key can only narrow what the team already allows. Leave the field unset for today's unrestricted behavior.

model_groups:
  anthropic:
    - claude-*
  local:
    - llama3.2:3b
daari keys team-create eng --model-group anthropic --allowed-model 'llama*'
daari keys create ci --team eng --allowed-model llama3.2:3b
daari keys update <key_id> --allowed-model 'claude-*'
daari keys team-update <team_id> --model-group local

A request whose model is outside the allowlist is HTTP 403 model_not_allowed (the body names the model, never the key) and writes an auth.model_denied audit row. Router fallback will not send that key to a frontier model outside the allowlist.

GET /v1/models (OpenAI and Anthropic facades) returns only cards that match the key ∪ team allowlist when either side is set — same patterns as inference. Master key and unrestricted virtual keys still see the full catalog. GET /v1/models/{id} returns 404 for ids outside the allowlist. Lifecycle query filters compose with the allowlist (retired models stay omitted by default).

Admission priority

The global in-flight gate admits waiters by priority class, then FIFO: high before normal before low. Mark interactive IDE keys high and overnight batch / eval keys low so one GPU favors live turns. Key priority wins over team priority; the default is normal. Batch drain acquires at low.

daari keys team-create batch --priority low
daari keys create ide --priority high --team eng
daari keys list   # prio column

Concurrency caps

--max-in-flight N on keys create / keys team-create sets a per-key or team in-flight gate (0 = unlimited). keys list shows a mif column; keys export --json round-trips the cap in metadata. Admin GET /v1/daari/keys|teams and the web-ui inventory expose the same field.

daari keys team-create fleet --max-in-flight 4
daari keys create agent --max-in-flight 2 --team fleet
daari keys list   # mif column

Cache scope

The org-shared cache is the default: identical prompts (L0) and similar prompts (L1) are reused across every key. That is the hit-rate win. A team handling confidential material can opt into a boundary instead.

cache_scope is global (default), team, or key, on both a virtual key and a team. The stricter setting wins (key over team over global), so a team set to team isolates every member key even when the key itself stays global. team folds team_id into the L0 exact key, the Redis L0 key, and the L1 context key. key folds key_id the same way. global leaves those hashes unchanged, so entries written before the flag stay reachable.

Two keys on the same team with cache_scope: team share entries. A key on another team does not, including an L1 hit on a merely similar prompt. Requests with no auth (master key off, no virtual keys) stay global. X-Daari-No-Cache still skips the cache entirely.

daari keys team-create legal --cache-scope team
daari keys create counsel --team legal
daari keys create solo --cache-scope key
daari keys list

daari keys list prints the effective scope. POST /introspect includes cache_scope for the token (never the secret).

Key expiry (doctor / Prometheus / webhook)

Enforcement-time expiry is a 401 key_expired. For 90-day rotation, also enable the proactive plane next to budget alerts:

alerts:
  key_expiry_webhook_url: https://hooks.example/daari-key-expiry
  key_expiry_webhook_secret: secret://env-file/~/.daari/secrets.env#KEY_EXPIRY_WEBHOOK
  key_expiry_warn_days: 14

daari doctor warns inside the warn window; scrape exposes daari_virtual_keys{status=...}; the webhook POSTs event: key_expiry once per key entering expiring_soon (signed with X-Daari-Signature when the secret is set). Full recipe: budgets and frontier.

Backup export / import

daari keys export --json / daari keys import round-trip key hashes (never plaintext) plus teams. The document also carries lifecycle and admission fields so host migration keeps idle hygiene and gate behavior: keys keep last_used_at, priority, and cache_scope; teams carry cache_scope and priority too (team-scoped L0/L1 isolation and admission class). Older export files without those keys import with today's defaults (last_used_at null, priority=normal, cache_scope=global). Prefer export/import over copying the SQLite file when moving hosts — see upgrade.

Verify

Call /v1/chat/completions with the key; exceed budget and confirm rejection.

Next

→ Auth and keys