voicegw.yaml is the central config file for VoiceGateway. It is validated at startup using a Pydantic schema with extra="forbid", so any typo or unknown key produces a clear error before your gateway starts.
Discovery order
VoiceGateway searches for the file in this order:./voicegw.yaml(current directory)~/.config/voicegateway/voicegw.yaml/etc/voicegateway/voicegw.yaml
VOICEGW_CONFIG to an absolute path. See Environment variables.
Top-level sections
All sections are optional. Omitted sections use defaults.providers
Configure credentials for each provider. String values support ${ENV_VAR} substitution.
api_key, base_url, and enabled (bool, default true). See Providers.
models
Register named aliases per modality. Each alias maps to a provider and model.
stacks
Named bundles that map one name to an STT, LLM, and TTS model ID. Reference a stack from a project with default_stack.
projects
Define projects for cost attribution and budget enforcement.
budget_action is one of warn, throttle, or block. Project-level providers override the top-level block for that project. See Projects.
projects.<id>.metrics
Conversation-metric tuning, per project rather than global:
The dead-air poll interval is a constructor argument, not a config key.
fallbacks
Ordered model IDs per modality. The router walks the list at startup and picks the first model whose provider imports cleanly.
observability
Three boolean flags that control which middleware runs. All default to true.
cost_tracking
Configure the SQLite storage backend for cost persistence.
enabled(bool, defaultfalse): enable cost persistence. Also enabled automatically whenVOICEGW_DB_PATHis set.db_path(string): path to the SQLite database file.daily_budget_alert(float, optional): global daily budget alert threshold in USD.
rate_card
The rating layer’s price book. VoiceGateway turns each request’s recorded provider cost into a billable price and stamps that price immutably onto the request row (rated_price_usd + rate_rule). The card is a global default_markup fallback plus an ordered list of rules. For the full model see Rating.
default_markup(float, default1.0): cost-plus multiplier applied when no rule matches a request.rules(list): ordered rate rules. Each rule is scoped, and carries exactly one kind of arithmetic.
Scope fields
Every scope field is optional and defaults to “any”:modality:stt,llm, ortts.provider: a provider name such asopenaiordeepgram.model: a model name, bare (nova-3) or fully qualified (deepgram/nova-3).tenant: a tenant ID, for a per-tenant override.plan: a plan name.
Rule kind: cost-plus or fixed
A rule is either cost-plus or fixed:- cost-plus (
markup): billable price is the recorded cost timesmarkup. Because it multiplies the recorded cost, it auto-follows voice-prices base movement (change the base price, the rated price tracks it). - fixed (
fixed+unit): billable price is an advertised$/unit(thefixedvalue) times the request’s billable quantity inunit. Decoupled from base cost, so it advertises a stable price as the base moves. Valid units:minute,second,char,1k_char,token,1k_token,1m_token,request.
Resolution
The single most specific matching rule wins. Precedence istenant > plan > global, and within that model > provider > modality-only. A later rule wins a specificity tie, so a rule layered after the seed takes precedence. When no rule matches, the request falls back to the default_markup cost-plus pass-through.
Write-time and immutable
Rating happens once, at write time, on the server (voicegw serve). The rated_price_usd and rate_rule audit token (for example cost_plus:1.3, fixed:0.006/minute, or default:1) are stamped onto the row and never rewritten: editing the card later never changes historical rows. Inspect and reconcile the card with the voicegw prices commands.
latency
Configure latency monitoring thresholds.
ttfb_warning_ms(float, default500.0): time-to-first-byte warning threshold in milliseconds.percentiles(list of floats): which percentiles to track and report.
rate_limits
Per-provider rate limiting.
requests_per_minute(int): maximum requests per minute for the provider.
ingest
Rate limiting for the fleet collector ingest endpoint (POST /v1/ingest). Limiting uses a per-caller token bucket keyed by virtual key, then static API key, then client IP.
enabled(bool, defaulttrue): turn ingest rate limiting on or off.requests_per_minute(int, default120): sustained per-caller rate. Set to0to disable.burst(int, default240): token-bucket ceiling.max_batch_size(int, default1000): maximum records per POST. Larger batches are rejected with413.
429 with a Retry-After header. The remote sink honors Retry-After and retries without dropping data.
retention
Hard-delete aged rows from the collector database. A background worker prunes sessions and their dependent rows by ended_at, and requests by timestamp, in batches.
enabled(bool, defaulttrue): turn retention pruning on or off.default_days(int, default90): age in days after which rows are deleted.
workers
Cadence for background workers: latency and agent rollups, and the retention prune. Workers run in-process and are started by the server. In a multi-replica deployment, set enabled: false on every replica except one.
enabled(bool, defaulttrue): start the background workers.rollup_interval_seconds(int, default900): how often the latency and agent rollups refresh.retention_interval_seconds(int, default3600): how often retention runs.node_scrape_interval_seconds(int, default15): how often the node scrape polls, when it runs at all.node_sample_max_age_days(int, default7): how long a rawnode_samplesrow is kept. Every scrape tick deletes rows older than this, so the table stays bounded whether or not per-project retention is on.
node_sample_max_age_days before running anything you intend to report on
for longer than a week. Retention equal to the observation window prunes the
window’s first day before the run ends, and the report then cannot cover the
span the run was performed to demonstrate. It has to exceed the run, not match
it.
The cost is rows: one target at the default 15s interval writes 5,760 rows a
day, so N targets over D days is roughly N x 5760 x D. A thirteen-target fleet
held for ten days is a few hundred thousand rows and on the order of a hundred
megabytes.
The node scrape is the one worker that is off by default. It is built only when
VOICEGW_NODE_SCRAPE_TARGETS names at least one target, so an install that does
not set that variable starts no scrape task and makes no outbound requests, and
this interval has no effect. See GET /v1/metrics in the HTTP API reference for
the target grammar.
serve
Bind host and port for the daemon. The daemon serves the HTTP API (/v1/*), dashboard API (/api/*), and the React SPA (/) on this single port.
host(string, default0.0.0.0): bind address. Use127.0.0.1to restrict to localhost.port(int, default8080): port number.provider_base_url_hosts(list of strings, default empty): hosts a managed provider’sbase_urlmay be moved to byPATCH /v1/providers/{provider_id}when the request does not carry a newapi_key. Entries are bare hosts (api.example.com) or full URLs, whose host is what counts. Values support${ENV_VAR}substitution.
Why provider_base_url_hosts exists
A PATCH that only changes base_url keeps the provider’s already-stored API key. POST /v1/providers/{provider_id}/test then builds the provider from that row, so the stored key is sent to whatever host the PATCH set. Anyone who can reach the write API could point a provider at a host they control and read the key out of the request.
The endpoint therefore constrains the host only for that exact combination: a host change that reuses the stored key. Permitted without any config are the provider’s current host and the vendor’s own default host (api.openai.com for openai, localhost for ollama, and so on), so leaving this list empty changes nothing for an existing deployment. Add a host here to allow a proxy or self-hosted gateway. A PATCH that includes its own api_key is never constrained: the caller already holds a key, so there is nothing to leak.
Environment variable substitution
Any string value in the config can use${VAR_NAME} syntax. VoiceGateway substitutes these at load time from os.environ. If the variable is not set, the value resolves to an empty string. See Environment variables.
Explore each section
Providers
API keys, base URL overrides, and per-project provider blocks for all 11 providers.
Models
Model ID format, language and voice suffixes, and custom alias registration.
Stacks
Named STT + LLM + TTS bundles for quality tiers.
Projects
Cost attribution, budget enforcement, and per-project provider keys.
Environment variables
All VOICEGW_ and provider API key variables, plus substitution rules.
Observability
Latency tracking, cost recording, and request logging middleware.