Skip to content
View as .md

LLM models and keys

Users pick models, never providers. A model ref is provider/model-id (pi format, e.g. openrouter/anthropic/claude-haiku-4.5). The provider prefix is only the join to a key — it decides which secret to look up.

OpenRouter is the default and recommended provider: one key covers every model namespace. Direct-provider refs (anthropic/<id>, google/<id>) work only if a key for that provider resolves.

Model resolution

For every LLM turn (in-server reply, ambient gate, sandbox pi turn):

thread config `model` → agent `model` → `defaultModelRef` (cheap OpenRouter preset)

Thread = “try this model for this conversation”. Agent = durable preference. The thread override is an optional lever, set rarely — it wins because it’s the most deliberate, most local choice, and in a multi-agent thread it pins every participant to one model.

Key resolution

One precedence, both the reply path and the sandbox path:

env-binding secret → house secret → worker env → fail loudly
  • App supplies (worker env): OPENROUTER_API_KEY only. This is the base key that makes arbe work without BYOK.
  • House supplies (house secrets, same names): OPENROUTER_API_KEY to replace the app key, or direct-provider keys (ANTHROPIC_API_KEY, GEMINI_API_KEY, …) to unlock direct refs.
  • Env binding (sandbox environments): secrets bound to the environment win over house secrets.

A failed secret resolve logs and falls back to the next tier; failures are never cached. Resolved overlays are cached ~60s per isolate (keyed house + agent + env), so key rotation lands within a minute.

Code: resolveTurnSecrets in packages/core/dispatch/turn-secrets.ts — the one resolver behind in-server replies, the gate, run_command, and delegate_task. It returns {values, sources}: per key, which tier supplied it. The LLM read on top is replyKeysFromSecrets (dispatch.ts).

Spend and budget

Two separate concerns: whose key (resolution above) and whether arbe subsidises (house budget).

Attribution (key_source)

Every paid seam calls recordUsage() (packages/core/usage.ts) after the spend, stamping key_source from the resolver:

key_sourceWho paysCounts toward spend_cap_usd?
workerarbe (app env keys)yes
househouse (BYOK secret)no
envhouse (env-bound secret)no

The ledger is per house, per event — no second table. Split totals with where key_source = 'worker' (see budget_check). Details: analytics → usage.

House budget (arbe subsidy only)

Each house has houses.spend_cap_usd — a lifetime cap on arbe-funded spend (key_source = 'worker'). New houses default to $5 (column default). NULL = uncapped (legacy only).

  • arbe sets the cap — service role / SQL during alpha. The column is not in authenticated houses update grants; house owners cannot raise it.
  • BYOK is the owner escape hatch — add provider keys under house secrets; LLM spend on those keys is the house’s bill and ignores the cap.
  • BYOK ≠ zero arbe spend — some seams always use worker keys (file indexing, parts of sandbox). Those still count against the cap.

Enforcement

Cap checks run only before a worker-key spend — gate, reply, sandbox, file index — not as a blanket “pause all dispatch” when over cap. A house over budget but on BYOK for LLM keeps getting LLM turns; worker-only seams refuse when the cap is exhausted. Code: packages/core/worker-budget.ts, wired from dispatch.ts and worker-key tools.

Over cap on a worker seam → signal.dispatch.skipped { reason: 'budget_exceeded' } for that path. Design detail: house budget.

Exceptions

  • ragthis file indexing is single-tenant by design: worker env key only, not house-overridable (see docs/thinking/files.md).
  • GEMINI_API_KEY is not a worker-env key. Google models require a house or env-binding secret.