Skip to content

Configuration reference

Busbar reads two YAML files at startup:

FileDefault pathEnv overridePurpose
Provider catalog/etc/busbar/providers.yamlBUSBAR_PROVIDERSShipped map of provider names → protocol, base URL, error map. Operators rarely edit this.
Deployment config/etc/busbar/config.yamlBUSBAR_CONFIGYour site’s providers (with API key env vars), models, pools, auth, observability, and governance.

Both files support ${VAR} environment interpolation before YAML is parsed. A missing or malformed env var reference is a fatal startup error, Busbar refuses to boot rather than run with an incomplete config.

All defaults below are sourced from src/config/mod.rs, src/breaker.rs, src/health.rs, and src/proto/mod.rs. Where a serde field default differs from a runtime constant, both are noted.



These are the only environment variables read by Busbar (excluding test-only BUSBAR_T_* / BUSBAR_SENTINEL_* names):

VariableWhere readPurpose / default
BUSBAR_PROVIDERSmain.rsPath to providers.yaml. Default: /etc/busbar/providers.yaml.
BUSBAR_CONFIGmain.rsPath to config.yaml. Default: /etc/busbar/config.yaml.
RUST_LOGobservability.rsLog level: error, warn, info, debug, or trace. Default: info.
(each provider’s api_key_env value)main.rsThe env var named by api_key_env holds that provider’s upstream credential. Read once at boot per provider.
(any ${VAR} in config.yaml)config.rsExpanded before YAML is parsed. Unset → fatal boot error.

BUSBAR_CLIENT_TOKEN and BUSBAR_ADMIN_TOKEN are not special-cased in the code. They appear in the shipped config.yaml only because the file references ${BUSBAR_CLIENT_TOKEN} and ${BUSBAR_ADMIN_TOKEN}. Any variable names work.


Only the brace form ${NAME} is expanded. Bare $NAME is passed through unchanged.

auth:
client_tokens:
- "${BUSBAR_CLIENT_TOKEN}" # expanded: the env var's value is substituted
- "$BUSBAR_OTHER_TOKEN" # NOT expanded, passed verbatim as a literal string
SituationBehavior
${NAME} where NAME is unsetFatal boot error: unset environment variable: NAME
${NAME with no closing }Fatal boot error: unclosed variable reference...
${} (empty name)Fatal boot error: empty variable name in ${}
Value contains a control character (\n, \r, \t, NUL, DEL, U+0085, U+2028, U+2029)Fatal boot error, prevents YAML-structure injection via env vars

Ordinary punctuation (: / @ . - # ") in env var values is allowed. Interpolation scans the entire raw file, including commented-out lines, so a ${VAR} in a comment must still resolve.


A map of provider name → ProviderDef. The shipped catalog is a curated set of verified providers across the six supported protocols. You can add an entry for any OpenAI-compatible endpoint not already in the catalog.

FieldTypeRequiredDefaultNotes
protocolstringnoanthropicOne of the six supported wire protocols: anthropic, openai, gemini, bedrock, responses, cohere. An unknown protocol is a startup error.
base_urlstringyesn/aScheme + host (+ optional path prefix). Must start with https:// for external endpoints. An http:// URL in the catalog is not blocked at parse time but will be rejected by the SSRF guard on deployment use. Trailing slash is trimmed.
error_mapmap<string, string>no{}Maps a provider-specific error code string (from the JSON error body) to a canonical disposition class. Valid values: rate_limit, overloaded, server_error, timeout, network, auth, billing, client_error, context_length. An unrecognized class value is a startup error. HTTP-status classification (401→auth, 429→rate_limit, 5xx→server_error, etc.) applies automatically without an error_map; this field is only for provider-specific JSON codes.
pathstringnoProtocol’s standard pathOverrides the upstream request path appended to base_url. Must begin with /. Use when the API version is in base_url and the endpoint path differs from the protocol default (e.g. /chat/completions without /v1).
authstringnoProtocol’s native authbearer (sends Authorization: Bearer <key>) or api-key (sends api-key: <key>, for Azure OpenAI). When unset, each protocol uses its native scheme: bearer for anthropic/openai/responses/cohere, x-goog-api-key for gemini, AWS SigV4 for bedrock. Setting auth: api-key forces the api-key: header regardless of protocol (rarely useful outside Azure OpenAI).
healthobjectnononeActive health-probe config. See Health probing.

Example entries:

anthropic:
protocol: anthropic
base_url: https://api.anthropic.com
azure-openai:
protocol: openai
base_url: https://myaccount.openai.azure.com/openai/deployments/gpt-4o
path: /chat/completions?api-version=2024-02-01
auth: api-key # sends api-key: <key> instead of Authorization: Bearer
zai-api:
protocol: openai
base_url: https://api.z.ai/api/paas/v4
path: /chat/completions
error_map:
"1113": billing
"1302": rate_limit

In config.yaml, a provider entry may selectively override the catalog’s protocol, base_url, error_map (merged: deployment entries win per code), path, auth, and health. The only always-required field in the deployment entry is api_key_env.

Health probing sends one minimal token request per interval per lane. It runs on a background task; probe outcomes run through the same disposition pipeline as organic traffic (2xx recovers the lane, transient failures increment the breaker, hard errors set the lane dead for 30 min).

FieldTypeDefaultNotes
modestringnonenone (passive only, breaker updates on organic traffic), dead (re-probe only tripped lanes), active (probe all lanes at every interval). active sends one billable request per lane per interval.
interval_secsinteger30Seconds between probes. Floored at 1.
timeout_secsinteger5Per-probe request timeout. Floored at 1.
anthropic:
protocol: anthropic
base_url: https://api.anthropic.com
health:
mode: dead
interval_secs: 30
timeout_secs: 5

A provider with no API key configured (api_key_env unset or its value empty) will not be probed regardless of the health block.


listen: "0.0.0.0:8080"
FieldTypeDefault
listenstring (host:port)0.0.0.0:8080

The value is passed directly to tokio::net::TcpListener::bind. An invalid or already-bound address is a fatal startup error.


Optional. When present, Busbar terminates inbound TLS natively (and, with client_ca_file, requires mutual TLS). When absent, Busbar serves plain HTTP, the historical default, unchanged.

tls:
cert_file: /etc/busbar/tls/fullchain.pem # PEM cert chain, leaf first
key_file: /etc/busbar/tls/privkey.pem # PEM private key (PKCS#8 / PKCS#1 / SEC1)
client_ca_file: /etc/busbar/tls/ca.pem # optional: present ⇒ mTLS required
FieldTypeDefault
cert_filestring (path), (required when tls is set)
key_filestring (path), (required when tls is set)
client_ca_filestring (path)unset (no client-cert requirement)

Certs/keys are loaded once at startup; any missing or unparseable file is a fatal startup error naming the file. ALPN advertises http/1.1. Rotate certs by replacing the files and restarting. Full operational guide: operations.md.


Front-door authentication for clients. When governance is enabled, governance virtual keys supersede static auth entirely, every request must carry a valid virtual key.

auth:
chain: [tokens]
upstream_credentials: own
client_tokens:
- "${BUSBAR_CLIENT_TOKEN}"
FieldTypeRequiredDefaultNotes
chainlistno[]The ordered authentication chain — each name is a compiled-in auth module (the built-in is tokens). [] (default) is the open front door: no client authentication, development only, loud startup warning. An unknown module name is a startup error.
upstream_credentialsstringnoownWhose key hits the provider: own (Busbar’s configured lane credential) or passthrough (forward the caller’s own token upstream — Busbar holds no keys).
client_tokenslistno[]The tokens module’s allowlist (env-interpolated). Required to be non-empty when tokens is in the chain. All comparisons are constant-time (no timing oracle). Inert when tokens is not in the chain.
modulesmapno{}Per-module trust-boundary caps, keyed by module name — see below.
modestringnon/aRemoved in 1.3 (was token/passthrough/none). The auth block rejects unknown keys, so a stale mode: is a hard parse error. Mapping: mode: tokenchain: [tokens]; mode: nonechain: []; mode: passthroughchain: [] + upstream_credentials: passthrough.

Token extraction order: Authorization: Bearer, then x-api-key, then x-goog-api-key. Blank values are treated as absent.

Semantics:

  • chain: [tokens]: the client must send a token matching an entry in client_tokens. Every route except /healthz requires a valid token (including /stats and /metrics, which are information-disclosure surfaces).
  • upstream_credentials: passthrough: the caller’s own token is forwarded to the upstream provider. An upstream 401/403 response is attributed to the caller; the breaker’s auth/billing disposition fires, which hard-downs the lane for 30 minutes: so callers with bad keys will suppress that lane for everyone for 30 minutes. Use with care.
  • chain: []: open relay, no client authentication. /metrics and /stats are admitted unconditionally. Development only; Busbar logs a loud warning at startup.

Startup validation:

  • tokens in the chain + empty effective client_tokens → startup error (every request would be rejected).
  • chain: [] + non-empty client_tokens → startup warning (the list has no effect).
  • upstream_credentials: passthrough + a provider whose api_key_env resolves to a non-empty value → startup warning (credential-leak risk: an unauthenticated caller’s request could carry Busbar’s own key to the upstream).

Bedrock ingress. Native Bedrock SDK clients authenticate with AWS SigV4 (Authorization: AWS4-HMAC-SHA256 …). There are two tracks:

  • Without governance (chain: [], with or without upstream_credentials: passthrough): Busbar does not verify the inbound SigV4 signature. The header is forwarded upstream (passthrough) or ignored entirely. Use this for transparent Bedrock proxying without per-key controls.
  • With governance (chain: [tokens] + governance.enabled: true): Busbar verifies the inbound SigV4 signature natively (src/auth/mod.rs verify_bedrock_sigv4, including body-hash integrity). Mint a key with "issue_aws_credential": true; the response includes aws_access_key_id + aws_secret_access_key (shown once). The Bedrock SDK authenticates with that pair; Busbar verifies the signature, then applies the key’s budget / RPM / TPM / allowed-pools. No passthrough required.

All other five ingress protocols use bearer-style auth and work with every chain configuration.

auth.modules — per-module trust-boundary caps

Section titled “auth.modules — per-module trust-boundary caps”

An auth module is a fully trusted endpoint: a module asserting groups: ["busbar-admins"] IS asserting an admin. Two operator-owned caps bound any module’s blast radius, applying wherever the module appears (the data-plane chain or admin_auth):

auth:
modules:
corp-ad:
allowed_groups: [llm-users, busbar-viewers] # groups this module may assert
max_admin_scope: read-only # ceiling regardless of group_map
FieldDefaultNotes
allowed_groupsabsent (no cap)Busbar intersects the module’s returned groups with this allowlist BEFORE group_map resolution — a module cannot claim a group you did not pre-authorize for it.
max_admin_scoperead-onlyCeiling on the admin scope obtainable through this module, regardless of what group_map grants: read-only | hooks-register | full. full is an explicit opt-in, warned at startup. The built-in admin-tokens operator credential is exempt (it is the root credential).

The admin API (/api/v1/admin/*) authenticates through its own chain — admin_auth: (default [admin-tokens], the single operator token) — and group_map: maps identity-provider GROUPS to authority, both admin and data-plane:

admin_auth: [admin-tokens]
group_map:
busbar-admins: { admin_scope: full }
busbar-viewers: { admin_scope: read-only }
llm-users: { allowed_pools: [my-pool], rpm_limit: 600 }
FieldNotes
admin_scopeThe admin authority this group grants: read-only | hooks-register | full. Absent = none. The most permissive of a principal’s mapped groups wins (then the module ceiling applies).
allowed_poolsDATA-PLANE grant: pools this group may target. Setting it (even [] = every pool) is what grants inference access at all; a group with only admin_scope confers none. Pool lists union across a principal’s groups.
rpm_limit / tpm_limit / max_budget_centsRate and spend caps for principals granted through this group — enforced by exactly the machinery a virtual key uses, keyed by the principal. Most-permissive union: a granting group without a cap lifts that axis; otherwise the max wins.

Unmapped groups grant nothing (fail closed): with governance enabled, an identified principal whose groups earn no allowed_pools grant is rejected outright.

The admin chain is live-mutable over the API (PUT /api/v1/admin/auth) with an anti-lockout guard — see the Admin API guide.


Declares which catalog providers this deployment uses and supplies the env var holding each one’s credential.

FieldTypeRequiredDefaultNotes
api_key_envstringyesn/aName of the env var that holds the upstream API key or credential. Read once at boot. An unset or empty env var logs a startup warning; the lane starts but will fail upstream auth.
protocolstringnoCatalog valueOverride the catalog protocol. Rarely needed.
base_urlstringnoCatalog valueOverride the upstream base URL. Must use https:// for public/external hosts. Plain http:// is permitted only for private or loopback hosts (e.g. a local Ollama or vLLM instance). Cloud-metadata hosts are blocked regardless of scheme (see SSRF guard).
error_mapmap<string, string>no{} merged onto catalogMerged with the catalog’s error_map; deployment entries win per code.
pathstringnoCatalog valueOverride the upstream path. Must begin with /.
authstringnoCatalog valuebearer or api-key.
healthobjectnoCatalog valueOverride the catalog’s health probe config.
allow_metadata_hostslistno[]Per-provider surgical exception: hosts/IPs to unblock from the cloud-metadata SSRF denylist for this provider only. See Security: Provider upstreams & SSRF.

Credential format by protocol:

Protocolapi_key_env value formatHow it’s sent
anthropicAPI key (sk-ant-api…) or OAuth token (sk-ant-oat…)x-api-key: <key> for API keys; Authorization: Bearer <key> for OAuth tokens. Mode is inferred from the key prefix; both headers are sent if the prefix is unrecognized. anthropic-version header is always added.
openai / responses / cohereAPI keyAuthorization: Bearer <key>
openai + auth: api-key (Azure)API keyapi-key: <key>
geminiAPI keyx-goog-api-key: <key>
bedrockACCESS_KEY_ID:SECRET_ACCESS_KEY or ACCESS_KEY_ID:SECRET_ACCESS_KEY:SESSION_TOKENAWS SigV4: signed per request. Region is parsed from the host in base_url (e.g. bedrock-runtime.us-east-1.amazonaws.com).
providers:
anthropic:
api_key_env: ANTHROPIC_KEY
openai:
api_key_env: OPENAI_KEY
gemini:
api_key_env: GEMINI_KEY
health:
mode: dead
interval_secs: 60
bedrock-us-east-1:
api_key_env: AWS_BEDROCK_CREDS # ACCESS:SECRET or ACCESS:SECRET:SESSION

Reserved name: a provider named admin (or any name beginning with admin/) is a startup error.


A model is a lane: one model at one provider, with its own concurrency semaphore, lifetime budget, and breaker cell. Models must be defined here before they can be used as pool members or targeted directly.

FieldTypeRequiredDefaultNotes
providerstringyesn/aMust name a key in this file’s providers map.
max_concurrentintegeryesn/aMaximum simultaneous in-flight requests for this lane (semaphore size). Must be ≥ 1.
max_requestsintegerno-1Lifetime request budget. -1 = unlimited. When the counter reaches 0 the lane is unusable. Must not be 0 (zero budget = permanently unusable = startup error).
default_max_tokensintegerno4096Injected only on a cross-protocol hop to a backend that requires max_tokens (Anthropic protocol) when the caller omitted it. Has no effect on same-protocol passthrough. Must be > 0 when set.
upstream_modelstringnothe config keyThe model id sent to the provider on the wire (request body for body-model protocols; URL path for path-model protocols like Bedrock/Gemini; and health probes). Defaults to the config key. Set it when the key can’t be the wire id: most commonly to run the same model behind two providers (the keys must differ, but each needs its own provider-specific model string). Must be non-empty when set. Metrics, breaker cells, and logs still key off the config key, not this.
attempt_timeout_msintegernounset (no cap)Per-attempt cap, in milliseconds, on time to response headers (the hang detector). If the provider has not started answering within the cap, the attempt is treated exactly like a transport timeout: the breaker records a transient failure and the request fails over to the next pool member within the same request. Because the cap covers only connect + headers, a healthy long stream body is never cut off by it. A pool member’s own attempt_timeout_ms overrides this per pool. Must be ≥ 1 when set (0 is a startup error); always floored by the request’s remaining failover.timeout_secs budget.
reasoningboolnofalseOperator declaration that this model accepts reasoning/thinking request parameters (Anthropic thinking, Gemini thinkingConfig, OpenAI reasoning_effort). Gates the cross-protocol reasoning carry: without the flag, a translated reasoning ask is dropped at the seam (warned) and never sent, so a non-reasoning model can never 400 from translation. Capability is per-model, not per-provider (Sonnet takes thinking; Haiku rejects it) — you declare what you deployed, like context_max. A pool member’s reasoning overrides this per pool. Same-protocol passthrough ignores it.
prompt_cachingboolnofalseOperator declaration that this model accepts prompt-cache markers on dialects where the marker is model-gated — Bedrock Converse’s cachePoint, which Claude accepts but Amazon Nova hard-rejects with a 400 (“extraneous key”). The cache twin of reasoning: without the flag, cross-protocol cache_control breakpoints headed to such a dialect are dropped at the seam (warned) and the request proceeds uncached — fail-safe, never a translation-induced 400. Set it on Claude-on-Bedrock models to keep their prompt caching across the Anthropic→Bedrock translation. Dialects whose cache form is universally accepted (the Anthropic API’s cache_control) ignore the flag, as does same-protocol passthrough (byte-exact).
models:
claude-sonnet-4-5:
provider: anthropic
max_concurrent: 20
max_requests: -1
default_max_tokens: 8192
gpt-4o:
provider: openai
max_concurrent: 20
gemini-1.5-pro:
provider: gemini
max_concurrent: 15
nova-pro:
provider: bedrock-us-east-1
max_concurrent: 10

Direct routing: a model named my-model is reachable at POST /my-model/v1/messages (Anthropic ingress). The ad-hoc route POST /{provider}/{model}/v1/messages bypasses the model map entirely: it routes to the named provider with the named model string, using no pool.

Reserved name: a model named admin is a startup error.

Same model, two providers (upstream_model)

Section titled “Same model, two providers (upstream_model)”

To run one real model: say Claude 3.5 Sonnet, behind both Anthropic and Bedrock in a single failover pool, the two model keys must differ (keys are unique), but each provider expects its own model string. upstream_model carries the provider-specific wire id while the key stays a stable operator alias:

models:
sonnet-anthropic:
provider: anthropic
max_concurrent: 20
upstream_model: claude-3-5-sonnet-20241022 # what Anthropic expects on the wire
sonnet-bedrock:
provider: bedrock-us-east-1
max_concurrent: 10
upstream_model: anthropic.claude-3-5-sonnet-20241022-v2:0 # Bedrock's modelId
pools:
sonnet: # clients call ONE name: POST /sonnet/v1/messages
members:
- target: sonnet-anthropic
weight: 3 # primary
- target: sonnet-bedrock
weight: 1 # cross-provider failover lane

Clients always address sonnet; when Anthropic rate-limits or trips its breaker, busbar fails over in-flight to the same model on Bedrock. Health probes use upstream_model too, so a lane can’t report healthy on the alias while real traffic fails on the wrong upstream id. Models without a collision (e.g. gpt-4o) need no upstream_model: the key already is the wire id.


A pool is a named, weighted group of model lanes with shared failover, breaker, and affinity config. Pools are optional, a deployment can route directly to models without any pools.

Target a pool with POST /smart/v1/messages (Anthropic ingress), or by setting "model": "smart" in POST /v1/chat/completions (OpenAI ingress), POST /v2/chat (Cohere), etc.

Reserved name: a pool named admin is a startup error. A pool name must not collide with any provider or model name.

pools:
smart:
members:
- target: claude-sonnet-4-5
weight: 8
- target: gpt-4o
weight: 2
- target: gemini-1.5-pro
weight: 1
FieldTypeRequiredDefaultNotes
targetstringyesn/aName of a model in models. Must be a configured model; a missing model is a startup error.
weightintegerno1Relative selection share under smooth weighted round-robin (SWRR), computed over the currently healthy/usable members. Must be ≥ 1. 0 is a startup error.
context_maxintegernononeThis member’s maximum context window (tokens). Used for context-length failover.
attempt_timeout_msintegernothe model’s valuePer-attempt time-to-response-headers cap for this member in this pool, overriding the model-level attempt_timeout_ms. Lets the same model carry different hang tolerances per pool (e.g. 10000 in a batch pool, 50 in a latency-critical one). Must be ≥ 1 when set (0 is a startup error). See Per-attempt timeouts.
reasoningboolnothe model’s valuePer-pool override of the model-level reasoning capability flag (member wins), so the same lane can allow thinking in a research pool and refuse it in a latency-critical one. See Cross-protocol reasoning.
tierstringnononeOperator-declared routing tier label (e.g. "primary", "overflow", "large", "small"). Inert for plain weighted pools (no hooks). Exposed to gate hooks as the tier field on each candidate. See Pool hooks.
cost_per_mtokfloatnononeOperator-declared cost in currency units per million tokens. Drives the cheapest ordering strategy and is exposed to gate hooks. Inert when unset or for plain weighted pools.
tagslistno[]Free-form string labels (e.g. ["opus", "large-context"]). The restrict gate verb intersects the candidate set against these tags (compliance pinning). Exposed to gate hooks for tag-based candidate selection. Inert for plain weighted pools.

Selection uses Nginx-style smooth weighted round-robin (SWRR) across the healthy subset. A tripped, dead, or capacity-exhausted member is skipped and its share redistributes to the remaining members automatically. Selection state is isolated per-pool (separate SWRR shard), so unrelated pools that share a lane select independently.

Empty members list is a startup error.

A pool spanning members that use different underlying protocols produces a startup warning (not an error). Cross-protocol requests are translated via the IR (intermediate representation), which is lossless for all standard fields. Source-only fields (e.g. OpenAI logprobs, n) are dropped before reaching a foreign backend.


Some providers fail by hanging: the connection opens, then nothing comes back for minutes. The ordinary transport timeout is sized for a full response and is far too long to catch this. attempt_timeout_ms caps how long a single attempt may wait for response headers; when it expires, the attempt is recorded as a transient failure on that member’s breaker cell and the request fails over to the next member, all within the same request.

Two layers, member wins over model:

models:
gemini-pro:
provider: gemini
max_concurrent: 20
attempt_timeout_ms: 10000 # model-level default: give it 10s anywhere
pools:
batch:
members:
- target: gemini-pro # inherits the model's 10000ms
- target: gpt-4o
realtime:
members:
- target: gemini-pro
attempt_timeout_ms: 50 # THIS pool can't wait: hop after 50ms
- target: gpt-4o

Details:

  • The cap covers connect + time to response headers only. A healthy stream that has started answering is never cut off mid-body by it.
  • Expiry is classified like a network timeout: it counts toward the breaker’s transient streak (repeated hangs trip the lane) and shows up in metrics as disposition="attempt_timeout" on busbar_upstream_failures_total and reason="attempt_timeout" on busbar_failovers_total.
  • The cap is always floored by the request’s remaining failover.timeout_secs budget; it can never extend a request past that.
  • Unset means no per-attempt cap (the transport timeout still applies). 0 is a startup error; disable by omitting the field.

The reasoning/thinking ask translates between the three protocols that model it: OpenAI reasoning_effort and Responses reasoning.effort (words), Anthropic thinking.budget_tokens and Gemini thinkingConfig.thinkingBudget (token budgets). Number to number is a straight copy; words and numbers convert through the effort table below. The response-side thinking content (thinking blocks, thought parts) already translates losslessly and needs no configuration.

The ask is gated per lane because thinking support is per-model, not per-protocol, and Busbar keeps no model database. reasoning: true on a model (or a pool member, which wins) declares “this backend accepts thinking params”:

models:
claude-sonnet:
provider: anthropic
max_concurrent: 20
reasoning: true # this model accepts thinking params
claude-haiku:
provider: anthropic
max_concurrent: 40 # no flag: a translated reasoning ask is dropped (warned), never sent

With the flag set, an OpenAI client’s reasoning_effort: "high" reaches this Claude lane as thinking: {type: enabled, budget_tokens: 16384}; a Gemini client’s thinkingBudget: 6000 reaches it as budget_tokens: 6000. Without the flag the request still succeeds, thinking at the backend’s default level.

The effort table (word ↔ number conversion, both directions) is operator-tunable:

limits:
reasoning_effort_budgets: # defaults shown; must be ascending, all > 0
minimal: 1024
low: 4096
medium: 8192
high: 16384

Guard rails, applied automatically: the budget is clamped to leave at least 1024 answer tokens under max_tokens (Anthropic requires budget_tokens < max_tokens), and when max_tokens is too small to fit any thinking the ask is dropped with a warn. Anthropic rejects temperature/top_k alongside thinking, so those knobs are omitted (warned) when a thinking ask is emitted to an Anthropic backend. Gemini’s dynamic -1 round-trips to Gemini verbatim and projects elsewhere as medium.


A pool names the hooks it wants — an ordering strategy and/or gates — in one hooks: [...] list. The ordering strategy decides the order in which healthy members are tried; gates can reject, restrict, or order the request before dispatch. The default (no list, or no strategy named) is weighted (SWRR) — zero-cost and byte-identical to the pre-hooks baseline. The full hook model — the registry, taps vs gates, grants, reply arms, and guarantees — lives in Hooks.

hooks: # top-level registry: define each hook once
smart-router:
kind: gate
socket: /run/busbar/router.sock
pools:
smart:
hooks: [cheapest, smart-router] # base ordering strategy + a gate, one list
members:
- target: claude-sonnet-4-5
weight: 2
context_max: 200000
tier: primary
cost_per_mtok: 3.0
tags: ["sonnet", "fast"]
- target: gpt-4o
weight: 1
context_max: 128000
tier: primary
cost_per_mtok: 5.0
tags: ["gpt4"]
- target: gpt-4o-mini
weight: 1
tier: overflow
cost_per_mtok: 0.15
tags: ["cheap"]

The pool hooks: list:

  • At most one ordering strategyweighted, cheapest, fastest, least_busy, or usage — sets the pool’s base ranking. Naming none leaves the base defaulted: the registry’s default: true hook (if one exists) becomes the base, else the compiled-in weighted.
  • Any other name is a gate reference into the top-level hooks: registry (must exist and be kind: gate — a dangling name or a tap is a startup error).
  • Several gates may share the list. All decision gates — the pool’s and any global_hooks — fire concurrently per request and reconcile deterministically: any reject wins (the lowest-priority gate’s status/message surfaces), restricts intersect (an empty intersection applies that gate’s on_empty — fail-closed by default), and with multiple orders the last in the chain wins. A restriction persists across every failover hop.

Top-level hooks: registry fields (per named hook; full reference in Hooks):

FieldTypeDefaultDescription
kindtap | gaterequiredgate = fire-and-wait (may rank/reject/restrict/rewrite); tap = fire-and-forget observation.
socketstringnoneAbsolute Unix-domain-socket path of the operator-run hook binary (lazy connect; Unix only). Exactly one of socket/webhook.
webhookstringnoneSidecar URL. SSRF-guarded (loopback allowed; RFC-1918/CGNAT/link-local/metadata blocked; remote must be https://).
timeout_msinteger1Hard wall-clock deadline for a gate decision. Co-located socket ≈ 8 µs, webhook ≈ 34 µs — raise it when the hook does I/O. On timeout the decision is coerced to on_error.
on_errorstringnothingFallback when a gate times out / errors / saturates: nothing (default — do not participate — a failing non-routing gate can never displace another gate’s verdict), weighted (the ordering floor; same behavior, the name for ordering gates), reject (fail closed — security gates set this), first, or the NAME of a fallback hook (a chain, proven terminating at boot). A gate’s deliberate reject reply is a decision, not a failure — on_error never applies to it.
promptno | ro | rwnoPrompt-content grant: ro sends the prompt read-only; rw additionally allows a rewrite reply. rw on a tap is a startup error. Immutable after registration; enforced both directions.
userno | ronoCaller-identity grant: governance key id/name (never the secret) + the body’s end-user field.
priorityinteger0Chain ordering key: orders the rewrite transform chain and tie-breaks the phase-2 reconcile (which reject surfaces; which order is “last”). Ties keep globals first, then config order.
on_emptystringrejectA restrict gate’s empty-intersection behavior: reject (fail closed, 503) or weighted (advisory escape — that gate’s restriction is skipped).
globalbooleanfalseFire on every request (overlay on top of each pool’s own hooks) — inline sugar for listing the name in global_hooks:.
defaultbooleanfalseMake this hook THE base ordering for pools that named no strategy (replacement, not overlay). At most one hook may set it — a second is a startup error.
settingsmap{}Opaque settings pushed to the hook via the configure wire message: as the first message on every socket (re)connection, and live via PATCH /api/v1/admin/hooks/{name}/settings (commit-on-ack). Busbar never interprets the contents.

The per-member tier, cost_per_mtok, and tags fields documented in Members and weights above feed the ordering strategies and gate candidates. Gate observability: the x-busbar-route-policy / x-busbar-route-target response headers name the deciding hook and chosen lane.


Per-(pool, lane) circuit-breaker tuning. The breaker state is independent per pool: a lane open in pool A can be closed in pool B. Lane-global state (hard-down, lifetime budget, concurrency semaphore) is shared across all pools.

pools:
primary:
members:
- target: claude-sonnet-4-5
- target: gpt-4o
breaker:
trip:
mode: error_rate
window_secs: 30
threshold: 0.5
min_requests: 5
base_cooldown_secs: 15
max_cooldown_secs: 120
FieldTypeDefaultValidationNotes
trip.modestringerror_rateMust be error_rate or consecutiveerror_rate: trips when errors/total ≥ threshold over window_secs seconds, with at least min_requests outcomes in the window. consecutive: trips after consecutive_n consecutive failures regardless of window.
trip.window_secsinteger30Must be ≥ 1Sliding outcome window for error_rate mode. Outcomes older than window_secs are evicted. (Renamed from window_s in 1.0.0; the old key still loads via a serde alias.)
trip.thresholdfloat0.5Must be in (0.0, 1.0]Error fraction threshold for error_rate mode. 0.5 means more than half of outcomes in the window must be errors to trip.
trip.min_requestsinteger5Must be ≥ 1error_rate mode: minimum outcomes required in the window before the threshold is evaluated. Prevents tripping on a single failure with no baseline.
trip.consecutive_ninteger3Must be ≥ 1consecutive mode: number of consecutive failures that trip the breaker. (Renamed from n in 1.0.0; the old key still loads via a serde alias.)
base_cooldown_secsinteger15Must be ≥ 1Initial cooldown duration after a trip. Subsequent trips without a successful recovery double the cooldown (exponential backoff).
max_cooldown_secsinteger120Must be ≥ base_cooldown_secsMaximum cooldown regardless of backoff.

Cooldown details. Cooldown is exponential: base * 2^streak, clamped to max_cooldown_secs, with ±10% random jitter (seeded from time, cell address, and streak) to decorrelate simultaneous failures. A provider Retry-After header is always honored as a floor on the computed cooldown (no config knob; always enabled), hard-capped at 24 hours to prevent overflow.

Recovery. When a cooldown expires the breaker transitions to HalfOpen. Exactly one request becomes the recovery probe (via a single CAS); /healthz and SWRR selection reads never steal the probe. If the probe succeeds, the breaker closes; if it fails, the cooldown doubles and the cycle repeats.

Disposition by error class:

ClassBreaker effectLane penalty
rate_limit, overloaded, server_error, timeout, networkTransient: increments error counter / streak, may tripYes
auth, billingHard-down, 30-minute sticky cooldown (HARD_DOWN_COOLDOWN_SECS = 1800); recovers only via successful health probeYes (hard)
client_errorClient fault, relayed verbatimNone
context_lengthContext failover, fails over to larger-context memberNone

A context_length classification is suppressed on any 5xx response, it cannot mask an upstream outage.

Omitting the breaker block uses all defaults above. The defaults match ADR-0002.


Bounds how long Busbar will retry across members for a single request.

pools:
resilient:
members:
- target: claude-sonnet-4-5
weight: 3
- target: gpt-4o
weight: 2
- target: gemini-1.5-pro
weight: 1
failover:
timeout_secs: 30
max_hops: 3
exclusions:
- gemini-1.5-pro # never used as a failover destination; still receives primary traffic
FieldTypeDefaultValidationNotes
timeout_secsinteger120Must be ≥ 1Wall-clock budget for the entire request across all hops. Exceeded → 503 immediately. (Renamed from deadline_secs in 1.0.0; the old key still loads via a serde alias.)
max_hopsinteger3n/aMaximum number of failover hops for one request. A hop is one upstream attempt that fails before the first response byte. (Renamed from cap in 1.0.0; the old key still loads via a serde alias.)
exclusionslistnoneEach entry must name a member of this poolModel names that are never selected as a failover destination, primary or otherwise. Use to reserve a member for affinity-only use or to permanently exclude a degraded lane.

Failover boundary: the first upstream byte. Failover is only possible before the first byte of the upstream response reaches the client. Once streaming has begun (any SSE or event-stream byte sent to the client), an upstream failure cannot fail over. Busbar instead records the breaker penalty and emits an in-band SSE error event. The client is responsible for retrying at the application level.

Budget refund. The lifetime max_requests counter is decremented optimistically when a 2xx header is received. If the response body then fails to deliver (transport error after headers), the decrement is reversed, so a partial-body transport failure does not permanently consume a budget slot.


What to do when every member of the pool is tripped, dead, or concurrency-exhausted.

pools:
primary:
members:
- target: claude-sonnet-4-5
- target: gpt-4o
on_exhausted:
action: fallback_pool:overflow
overflow:
members:
- target: claude-sonnet-4-5
- target: gpt-4o-mini
on_exhausted:
action: least_bad
Action valueBehavior
reject (also 503, status_503, status503)Return 503 Service Unavailable with a Retry-After header set to the soonest member cooldown expiry. This is the default when on_exhausted is omitted.
least_bad (also least-bad, leastbad)Route to the member whose cooldown expires soonest, even though it is Open. The request is likely to fail, but degraded service is preferred over a hard 503. This is logged as a degraded dispatch.
fallback_pool:<name>Route the request to another named pool and run its full selection logic. Cycles (primary → overflow → primary) and self-references are detected at startup and are errors.

Unknown or malformed action values are a fatal startup error (not a runtime 503).


Pin a session to one pool member while that member remains healthy. Useful to keep provider-side prompt caches warm or to maintain conversational state.

pools:
smart:
members:
- target: claude-sonnet-4-5
- target: gpt-4o
affinity:
mode: session
header_name: x-session-id
FieldTypeDefaultNotes
modestringsessionsession is the only supported value. Any other value is a startup error.
header_namestringx-session-idRequest header whose value identifies the session.

Affinity is a preference, not a hard pin. If the sticky member is tripped, dead, or at capacity, Busbar falls back to normal SWRR selection without failing the request.


Declare each member’s context_max so an oversized request fails over to a larger-context member instead of returning an error: and without penalizing the smaller lane, since a context-length overflow is not an upstream fault.

pools:
long-context:
members:
- target: claude-sonnet-4-5
context_max: 200000
- target: gemini-1.5-pro
context_max: 1000000

When a member returns a context-length error, busbar:

  1. Excludes from the current request any candidate whose known context_max is ≤ the failed lane’s.
  2. Fails over to a member with a larger (or unknown) context_max.
  3. Records no breaker penalty against the smaller lane.

Members without context_max set are always eligible for context-length failover (their capacity is unknown; Busbar treats unknown as potentially unlimited).


Optional. Exposes nine operational limits: eight previously hardcoded, plus the new max_inbound_concurrent, so operators can tune them without rebuilding. All fields default to their historical values, so omitting this block is a no-op.

limits:
max_inbound_concurrent: 0 # 0 = unlimited; > 0 adds a global concurrency cap
request_body_max_bytes: 33554432 # 32 MiB
upstream_request_timeout_secs: 300
tls_handshake_timeout_secs: 10
pool_max_idle_per_host: 64
hard_down_cooldown_secs: 1800 # 30 min
upstream_error_body_max_bytes: 262144 # 256 KiB
max_honored_retry_after_secs: 86400 # 24 h
default_max_tokens: 4096
FieldTypeDefaultNotes
max_inbound_concurrentinteger0Global inbound concurrency cap. 0 = unlimited (no cap layer installed). Any positive value installs an outermost concurrency-limit middleware before routing.
request_body_max_bytesinteger33554432Maximum inbound request body size (bytes). Exceeding this returns a protocol-native 413.
upstream_request_timeout_secsinteger300Per-upstream-request wall-clock timeout. Applies to both the connect and the full response.
tls_handshake_timeout_secsinteger10Wall-clock cap on each inbound TLS handshake; prevents slowloris / handshake-flood. Ignored when tls: is absent.
pool_max_idle_per_hostinteger64HTTP connection pool idle connection limit per upstream host.
hard_down_cooldown_secsinteger1800Sticky cooldown for auth/billing breaker dispositions (hard-down). Recovering these lanes requires a successful health probe.
upstream_error_body_max_bytesinteger262144Maximum bytes buffered from a non-2xx upstream response body for error classification.
max_honored_retry_after_secsinteger86400Maximum value honored from an upstream Retry-After header (to prevent overflow).
default_max_tokensinteger4096Gateway-wide default injected on cross-protocol hops to Anthropic when the caller omitted max_tokens. Overridden by a per-model default_max_tokens when set.

All sinks are opt-in. Prometheus /metrics is always on and needs no config entry. It is auth-gated (same rules as /stats) and is not an unauthenticated endpoint.

observability:
otlp_endpoint: "http://localhost:4318/v1/traces"
request_log_webhook_url: "https://logs.example.com/busbar"
emit_server_timing: true
FieldTypeDefaultNotes
otlp_endpointstringnoneWhen set, installs an OTLP/HTTP trace exporter. Loopback http:// is allowed (standard collector default). Remote endpoints must use https://. SSRF-guarded: rejects RFC-1918, link-local, CGNAT, metadata hosts. Traces are flushed on graceful shutdown.
request_log_webhook_urlstringnoneWhen set, fires a fire-and-forget JSON POST per completed request: {ts, ingress_protocol, pool, outcome, latency_ms}. Must be https://. SSRF-guarded (same classes as otlp_endpoint plus broadcast). At most 64 deliveries in flight; drops rather than queues. 2-second delivery timeout.
emit_server_timingboolfalseControls whether the Server-Timing: busbar;dur=<ms> response header is emitted on every response. Defaults to false, the header is an in-band busbar fingerprint, so it is suppressed by default for backend indistinguishability. Set to true to enable it as a latency probe.

OTLP credential hygiene. If your OTLP endpoint requires auth, supply credentials in the URL userinfo (https://user:pass@collector.example.com/…): Busbar moves them to an Authorization: Basic header and strips them from the URL before logging, so they do not appear in logs or spans.


Optional virtual-key governance layer. When enabled, static auth tokens are superseded, every request must carry a busbar-issued virtual key. Per-key controls: allowed pools (ACL), budget (cents), budget period, and rate limits (RPM/TPM). State is durable in embedded SQLite.

governance:
enabled: true
db_path: /var/lib/busbar/governance.db
admin_token: "${BUSBAR_ADMIN_TOKEN}"
price_per_request_cents: 1
price_per_1k_tokens_cents: 50
FieldTypeRequiredDefaultValidationNotes
enabledboolnofalsen/aMaster switch.
db_pathstringnobusbar-governance.dbn/aPath to the SQLite file. The directory must exist and be writable.
admin_tokenstringnonone (admin API disabled)Must be non-empty (non-whitespace) when enabled: trueGuards the /api/v1/admin/keys API. If absent when enabled: true, Busbar refuses to start (the admin API would be silently inaccessible).
price_per_request_centsintegerno1Negative values clamped to 0Flat per-request charge against each virtual key’s budget (in cents).
price_per_1k_tokens_centsintegerno0Negative values clamped to 0Per-1,000-token charge (input + output tokens from response usage metadata).
budget_on_store_errorstringnoallowallow or denyBehavior when the budget store errors during the atomic admission check-and-charge. allow (default) fails open, the request proceeds, preserving availability on a store hiccup. deny fails closed, the request is rejected, providing a hard budget guarantee for security/regulated deployments. A definitive over-budget result always rejects regardless of this setting.
sqlite_busy_timeout_msintegerno5000n/aSQLite busy_timeout (milliseconds) for the governance store under write contention.
rate_sweep_intervalintegerno256Must be ≥ 1How often (every N admissions) the in-memory rate-limit map evicts idle entries. Correctness does not depend on it (per-key windows reset on lookup); it only bounds memory. 0 is rejected at startup.

Budget spend per request: price_per_request_cents + (total_tokens / 1000) * price_per_1k_tokens_cents.

Enforcement semantics (important for operators):

  • RPM is precise. The per-minute counter is incremented synchronously on admission.
  • TPM is best-effort. Token counts are fed post-response; concurrent in-flight requests are not pre-charged. The first request of each rate window is always admitted.
  • Budget admission is a hard, atomic cap. The budget check and the flat per-request charge are one atomic conditional UPSERT (charge_within_budget): a request whose fee would push the window’s spend past max_budget_cents is rejected before it is forwarded, and a concurrent burst cannot race past the limit. The token-priced component (price_per_1k_tokens_cents) is accrued post-response, so spend from requests already in flight when the cap is neared can land after admission; that overshoot is bounded by in-flight parallelism. A request admitted and then failing upstream (non-2xx) has its flat fee refunded. On store errors, behavior is controlled by budget_on_store_error (default allow = fail open; set deny for fail-closed).

Incompatible combination: enabled: true + auth.mode: passthrough is a startup error. Governance supersedes passthrough; the combination is unsupported.

Virtual key format: sk-bb-<32 hex characters> (128-bit CSPRNG). Shown in plaintext exactly once at mint; stored as SHA-256 hash only. Key IDs have the form vk_<16 hex characters>.

Admin API routes (guarded by the admin token, not a virtual key):

RouteMethodDescription
/api/v1/admin/keysPOSTMint a new virtual key. Returns plaintext bearer secret once. Pass "issue_aws_credential": true to also receive aws_access_key_id + aws_secret_access_key for Bedrock-SDK clients (both shown once).
/api/v1/admin/keysGETList all keys (metadata only; no secrets).
/api/v1/admin/keys/{id}PATCHUpdate key fields. Three-state semantics: absent = unchanged, null = clear to unlimited, value = set.
/api/v1/admin/keys/{id}/usageGETCurrent-window spend, tokens, and request count.
/api/v1/admin/keys/{id}DELETERevoke a key. Returns 404 if not found (not idempotent).

See operations.md for the full admin API payload schemas and virtual key fields, including the issue_aws_credential Bedrock SigV4 option.


Optional. Extends or overrides the hardcoded cloud-metadata SSRF denylist. When absent, only the built-in denylist applies. See Security: Provider upstreams & SSRF for the full threat model, the complete denylist, and worked examples.

security:
blocked_metadata_hosts:
- "169.254.100.1"
allow_metadata_hosts:
- "metadata.google.internal"
allow_all_metadata: false # default; set true only for dev, logs a startup WARNING
FieldTypeDefaultNotes
blocked_metadata_hostslist[]Additional hosts/IPs appended to the hardcoded denylist. Entries may be IP literals or DNS hostnames. Matched with the same obfuscation-aware canonicalization as the built-in list.
allow_metadata_hostslist[]Hosts/IPs to unblock globally: removed from the effective denylist for all providers. Use per-provider allow_metadata_hosts for a narrower exception.
allow_all_metadataboolfalseDisables the SSRF guard entirely. Every cloud-metadata endpoint becomes reachable by every provider. Logs a startup WARNING. Development use only.

Precedence: a host is blocked iff it is in the denylist (hardcoded union blocked_metadata_hosts) and not in any allow-override (security.allow_metadata_hosts union that provider’s allow_metadata_hosts) and not allow_all_metadata. Allow always wins.


The smallest config that parses and resolves. providers and models are the only required top-level sections.

config.yaml:

providers:
anthropic:
api_key_env: ANTHROPIC_KEY
models:
claude:
provider: anthropic
max_concurrent: 10

Required environment variable: ANTHROPIC_KEY must be set.

Routes available:

  • POST /claude/v1/messages: Anthropic ingress, directly to the claude model.
  • GET /healthz, readiness check.
  • GET /metrics, Prometheus (admitted unconditionally under mode: none).

listen defaults to 0.0.0.0:8080. No auth gate. No pools.


This example requires: BUSBAR_CLIENT_TOKEN, BUSBAR_ADMIN_TOKEN, ANTHROPIC_KEY, OPENAI_KEY, GEMINI_KEY.

listen: "0.0.0.0:8080"
# ---------------------------------------------------------------------------
# Auth: clients send Authorization: Bearer <BUSBAR_CLIENT_TOKEN>
# Governance is enabled below, so this becomes vestigial, governance keys
# supersede static tokens once governance is active.
# ---------------------------------------------------------------------------
auth:
mode: token
client_tokens:
- "${BUSBAR_CLIENT_TOKEN}"
# ---------------------------------------------------------------------------
# Providers: declare which catalog providers this deployment uses.
# api_key_env names the env var holding each provider's credential.
# ---------------------------------------------------------------------------
providers:
anthropic:
api_key_env: ANTHROPIC_KEY
health:
mode: dead # re-probe only tripped lanes, every 30s
interval_secs: 30
timeout_secs: 5
openai:
api_key_env: OPENAI_KEY
gemini:
api_key_env: GEMINI_KEY
# ---------------------------------------------------------------------------
# Models: one lane per model. Each lane has its own semaphore and breaker.
# ---------------------------------------------------------------------------
models:
claude-sonnet:
provider: anthropic
max_concurrent: 20
max_requests: -1 # unlimited lifetime budget
default_max_tokens: 4096 # injected on cross-protocol hops to Anthropic only
gpt-4o:
provider: openai
max_concurrent: 20
gemini-1.5-pro:
provider: gemini
max_concurrent: 15
gpt-4o-mini:
provider: openai
max_concurrent: 30 # high capacity overflow lane
# ---------------------------------------------------------------------------
# Pools: named groups of weighted lanes with failover and breaker config.
# context_max is a per-member field (here used for context-length failover).
# ---------------------------------------------------------------------------
pools:
# Primary pool, weighted SWRR with session affinity and a tight breaker.
smart:
members:
- target: claude-sonnet
weight: 2
context_max: 200000
- target: gpt-4o
weight: 2
context_max: 128000
- target: gemini-1.5-pro
weight: 1
context_max: 1000000
affinity:
mode: session
header_name: x-session-id
breaker:
trip:
mode: consecutive # trip fast on a short streak
consecutive_n: 2
base_cooldown_secs: 5
max_cooldown_secs: 60
failover:
timeout_secs: 30 # total wall-clock budget across all hops
max_hops: 3 # at most 3 failover attempts
on_exhausted:
action: fallback_pool:overflow
# Overflow pool, used when every smart member is tripped.
overflow:
members:
- target: claude-sonnet
weight: 3
- target: gpt-4o-mini
weight: 1
on_exhausted:
action: least_bad # serve degraded rather than hard 503
# Cost-optimized pool, cheapest available member first.
# cost_per_mtok on each member drives the cheapest ordering strategy.
batch:
hooks: [cheapest]
members:
- target: gpt-4o-mini
weight: 1
cost_per_mtok: 0.15
tags: ["cheap"]
- target: claude-sonnet
weight: 1
cost_per_mtok: 3.0
failover:
timeout_secs: 120
max_hops: 3
on_exhausted:
action: reject
# ---------------------------------------------------------------------------
# Observability: traces and per-request webhook logging.
# /metrics is always on (no config needed).
# ---------------------------------------------------------------------------
observability:
otlp_endpoint: "http://localhost:4318/v1/traces"
request_log_webhook_url: "https://logs.example.com/busbar"
emit_server_timing: true
# ---------------------------------------------------------------------------
# Governance: virtual keys, budgets, rate limits.
# Note: mode: passthrough is incompatible with governance.enabled: true.
# ---------------------------------------------------------------------------
governance:
enabled: true
db_path: /var/lib/busbar/governance.db
admin_token: "${BUSBAR_ADMIN_TOKEN}"
price_per_request_cents: 1
price_per_1k_tokens_cents: 50

Busbar validates the merged config before accepting any traffic. Fatal errors abort startup; warnings are logged and startup continues.

Errors (fatal):

RuleCondition
Provider name reservedAny provider named admin or beginning with admin/
Protocol unknownprotocol not in {anthropic, openai, gemini, bedrock, responses, cohere}
base_url SSRFbase_url resolves to a cloud-metadata/IMDS host (e.g. 169.254.169.254, 100.100.100.200, metadata.google.internal) or uses an alternate IP encoding (decimal-int, hex, octal, IPv4-mapped IPv6) that decodes to a metadata address
base_url plaintextbase_url uses http:// with a public (non-private, non-loopback) host: plain HTTP to a public host would expose the API key on the wire
error_map value unknownA value in error_map is not one of the nine canonical disposition classes
auth value unknownauth field value not bearer or api-key
auth.mode value unknownauth.mode not one of token, passthrough, none (case-insensitive)
affinity.mode value unknownaffinity.mode not session (the only supported value)
Removed token field setThe 1.0.0-removed auth.token field is present, rejected at parse as an unknown field (unknown field \token`); move its value into client_tokens`
path malformedpath does not begin with /
Model name reservedModel named admin
provider reference missingmodels.<name>.provider does not name a configured provider
max_concurrent: 0A concurrency semaphore of 0 never grants a permit
max_requests: 0Zero lifetime budget = permanently unusable lane
default_max_tokens: 0Would be injected upstream and rejected
Pool name reservedPool named admin
Pool name collisionPool name matches a provider or model name
Empty membersA pool with no members is un-routable
weight: 0Pool member weight of 0 is invalid
target reference missingPool member target does not name a configured model
failover.timeout_secs: 0Zero failover deadline
failover.exclusions danglingAn exclusion names a model not in the pool
Fallback pool cycleon_exhausted: fallback_pool:<X> where following the chain creates a cycle
Fallback pool self-referenceon_exhausted: fallback_pool:<self>
Fallback pool unknownon_exhausted: fallback_pool:<name> where name is not a configured pool
on_exhausted malformedUnrecognized action string
affinity.mode unknownAny value other than session
Pool hooks: names more than one ordering strategyA pool has one base ordering
Pool hooks: gate name not in the registryEvery non-strategy name must reference a top-level hooks: entry
Pool hooks: names a tapOnly a gate (fire-and-wait) can influence routing
Hook with neither/both of socket and webhookExactly one transport per hook
Hook webhook SSRF-blockedRFC-1918, CGNAT, link-local, and metadata hosts are blocked (loopback allowed)
prompt: rw on a kind: tap hookA tap observes; it can never rewrite
More than one hook with default: trueAt most one default base ordering (error names both hooks)
Hook named after a built-inRegistry names must not shadow the compiled-in plugin names
route: / policy: / hook: pool keysRemoved/retired keys; each parse error names the hooks: [...] fix
Breaker max_cooldown < base_cooldownCooldown ceiling below the base
tokens in auth.chain + empty client_tokensEvery request would be rejected
auth.chain names an unknown moduleEvery chain entry must be a compiled-in auth module
auth.mode presentRemoved in 1.3 — write chain: + upstream_credentials:
governance.enabled: true + no admin_tokenAdmin API silently inaccessible
governance.enabled: true + auth.mode: passthroughUnsupported combination
${VAR} unset in configUnresolvable interpolation reference
${} or unclosed ${Malformed interpolation syntax

Warnings (non-fatal):

Condition
auth.mode: none with non-empty client_tokens (allowlist has no effect)
auth.mode: passthrough with a provider whose API key env var is non-empty (credential-leak risk)
Heterogeneous pool (members span more than one backend protocol, cross-protocol translation applies)
api_key_env names an env var that is unset or empty at boot (lane will fail auth)
allowed_pools on a virtual key (admin API) names a pool not currently configured
auth.mode: token or auth.mode: none with governance enabled (static auth is superseded; effective mode is governance virtual keys)