VoiceGateway // DOCS

Configuration Layers

VoiceGateway merges configuration from three sources with a clear priority order. This allows base configuration in YAML, dynamic management via the dashboard/MCP, and environment-level overrides.

Configuration Layers

VoiceGateway merges configuration from three sources with a clear priority order. This allows base configuration in YAML, dynamic management via the dashboard/MCP, and environment-level overrides.

Priority Order

Plain text
ENV variables (highest)  >  SQLite managed tables  >  YAML file (lowest)
Configuration Sources ConfigManager.load_merged() Merged GatewayConfig Environment VariablesDEEPGRAM_API_KEY, VOICEGW_DB_PATH, etc. SQLite Managed Tablesmanaged_providers, managed_models, managed_projects voicegw.yamlBase configuration file 1. Deep-copy YAML config 2. Layer in managed_providers from SQLite 3. Layer in managed_models from SQLite 4. Layer in managed_projects from SQLite 5. Env vars already substituted in step 1 providers + models + projects + fallbacks + ...

ConfigManager

File: src/voicegateway/core/config_manager.py

The ConfigManager is responsible for merging YAML and SQLite sources into a single GatewayConfig.

Python
class ConfigManager:
    def __init__(self, yaml_config: GatewayConfig, storage: SQLiteStorage | None):
        self._yaml = yaml_config
        self._storage = storage

    async def load_merged(self) -> GatewayConfig:
        """Return a GatewayConfig with managed_* rows merged in."""
        merged = copy.deepcopy(self._yaml)
        # Layer in managed providers, models, projects from SQLite
        ...
        return merged

    async def refresh(self) -> GatewayConfig:
        """Reload after a write. Called by Gateway.refresh_config()."""
        return await self.load_merged()

Merge Rules

The key rule is: YAML always takes precedence. If a provider, model, or project exists in both YAML and SQLite, the YAML version wins.

Python
for row in await self._storage.list_managed_providers():
    pid = row["provider_id"]
    if pid in merged.providers:
        continue  # YAML takes precedence -- don't overwrite

This means you can "pin" critical configuration in YAML and use the dashboard/MCP for everything else, without worrying about managed resources overwriting your file-based config.

The source Field

Each ProjectConfig carries a source field indicating where it came from:

source ValueMeaning
"yaml"Defined in voicegw.yaml
"db"Created via dashboard or MCP, stored in managed_projects

For providers and models, the _source key is injected into the config dict:

Python
merged.providers[pid] = {
    "api_key": plaintext_key,
    "base_url": row.get("base_url"),
    "_source": "db",
    **(row.get("extra_config") or {}),
}

YAML Configuration

File: src/voicegateway/core/config.py

Environment Variable Substitution

YAML values containing ${ENV_VAR} are replaced with the corresponding environment variable at load time:

voicegw.yaml
providers:
  openai:
    api_key: ${OPENAI_API_KEY}
  deepgram:
    api_key: ${DEEPGRAM_API_KEY}

The substitution is recursive -- it works inside strings, dicts, and lists. Missing env vars resolve to empty strings.

When no explicit path is provided:

  1. Check VOICEGW_CONFIG env var.
  2. Search these paths in order:
    • ./voicegw.yaml
    • ~/.config/voicegateway/voicegw.yaml
    • /etc/voicegateway/voicegw.yaml

Pydantic Validation

File: src/voicegateway/core/schema.py

Before parsing, the raw YAML dict is validated against a Pydantic model (VoiceGatewayConfig). Validation errors are formatted with field paths and messages:

Plain text
Configuration validation failed:
  - providers.openai.api_key: field required
  - cost_tracking.db_path: str type expected

Check your voicegw.yaml for typos or invalid values.

GatewayConfig Dataclass

The parsed config is stored as a GatewayConfig dataclass with these fields:

FieldTypeDescription
providersdict[str, dict]Provider configs keyed by name
modelsdict[str, dict[str, dict]]Models keyed by modality, then model ID
fallbacksdict[str, list[str]]Fallback chains per modality
cost_trackingdictDB path, enabled flag
latencydictTTFB warning threshold
rate_limitsdict[str, dict]RPM limits per provider
dashboarddictDashboard config
projectsdict[str, ProjectConfig]Project configs
stacksdict[str, dict[str, str]]Named model bundles
observabilitydictFeature flags for tracking

Refresh Cycle

When the dashboard or MCP server creates/updates/deletes a managed resource:

upsert_managed_provider(...) log_audit_event(...) refresh_config() refresh() list_managed_providers() list_managed_models() list_managed_projects() merged GatewayConfig Router(merged_config) Rebuild BudgetEnforcer + FallbackChains Dashboard / MCP SQLite Gateway ConfigManager Router

This ensures that newly added providers and models are immediately available for routing, without requiring a server restart.

Example Configuration

voicegw.yaml
providers:
  openai:
    api_key: ${OPENAI_API_KEY}
  deepgram:
    api_key: ${DEEPGRAM_API_KEY}
  cartesia:
    api_key: ${CARTESIA_API_KEY}

models:
  stt:
    deepgram/nova-3:
      provider: deepgram
      model: nova-3
  llm:
    openai/gpt-4.1-mini:
      provider: openai
      model: gpt-4.1-mini
  tts:
    cartesia/sonic-3:
      provider: cartesia
      model: sonic-3
      default_voice: 794f9389-aac1-45b6-b726-9d9369183238

fallbacks:
  stt:
    - deepgram/nova-3
    - openai/whisper-1
  tts:
    - cartesia/sonic-3
    - elevenlabs/turbo-v2.5

stacks:
  premium:
    stt: deepgram/nova-3
    llm: openai/gpt-4.1-mini
    tts: cartesia/sonic-3

projects:
  prod:
    name: Production
    daily_budget: 50.00
    budget_action: throttle
    default_stack: premium
    tags: [production]

cost_tracking:
  enabled: true
  db_path: ~/.config/voicegateway/voicegw.db

rate_limits:
  openai:
    requests_per_minute: 60
  deepgram:
    requests_per_minute: 100

latency:
  ttfb_warning_ms: 500

observability:
  latency_tracking: true
  cost_tracking: true
  request_logging: true

On this page