VoiceGateway // DOCS

Code Style

VoiceGateway enforces consistent code style through automated tooling. This page documents the rules and conventions.

Code Style

VoiceGateway enforces consistent code style through automated tooling. This page documents the rules and conventions.

Tooling overview

ToolPurposeConfig location
ruffLinting + formatting (replaces flake8, isort, Black)pyproject.toml [tool.ruff]
mypyStatic type checkingpyproject.toml [tool.mypy]
pre-commitRuns checks before each commit.pre-commit-config.yaml

Ruff

Ruff handles both linting and formatting. It is configured in pyproject.toml:

TOML
[tool.ruff]
target-version = "py311"
line-length = 88

[tool.ruff.lint]
select = [
    "E",   # pycodestyle errors
    "W",   # pycodestyle warnings
    "F",   # pyflakes
    "I",   # isort (import sorting)
    "B",   # flake8-bugbear
    "C4",  # flake8-comprehensions
    "UP",  # pyupgrade (modernize syntax)
]
ignore = [
    "E501",  # line too long (formatter handles wrapping)
    "B008",  # function calls in argument defaults
    "C901",  # too complex
    "W191",  # indentation contains tabs
]

Running ruff

Shell
# Check for lint errors
ruff check .

# Auto-fix what can be fixed
ruff check --fix .

# Format code (Black-compatible)
ruff format .

# Check formatting without modifying files
ruff format --check .

Import sorting

Ruff's I rule handles import sorting (replacing isort). Imports are grouped in this order:

  1. Standard library (import os, from typing import ...)
  2. Third-party (import pytest, from fastapi import ...)
  3. Local (from voicegateway.core import ...)

Each group is separated by a blank line. Within a group, import statements come before from ... import.

mypy

Static type checking catches bugs before runtime. VoiceGateway's mypy config:

TOML
[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
ignore_missing_imports = true

Run mypy:

Shell
mypy

Type annotation guidelines

  • All public functions must have type annotations
  • Use from __future__ import annotations at the top of every module (enables PEP 604 X | Y syntax)
  • Use dict, list, tuple (lowercase) instead of Dict, List, Tuple from typing
  • Use X | None instead of Optional[X]
  • Use TYPE_CHECKING guards for import-only types to avoid circular imports:
Python
from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from voicegateway.providers.base import BaseProvider

Docstrings

Use Google-style docstrings for all public classes, methods, and functions:

Python
def create_provider(provider_name: str, config: dict[str, Any]) -> BaseProvider:
    """Create a provider instance by name.

    Args:
        provider_name: Name of the provider (e.g., "openai", "deepgram").
        config: Provider configuration dict from voicegw.yaml.

    Returns:
        Initialized provider instance.

    Raises:
        ValueError: If provider name is unknown.
    """

Rules:

  • First line is a concise imperative summary (no period for one-liners)
  • Blank line between summary and Args/Returns/Raises sections
  • Args, Returns, Raises sections as needed
  • Private methods (_foo) may use shorter docstrings

Conventional Commits {#conventional-commits}

All commit messages must follow Conventional Commits:

Plain text
<type>(<scope>): <description>

[optional body]

[optional footer]

Types

TypeWhen to use
featNew feature
fixBug fix
docsDocumentation only
testAdding or modifying tests
refactorCode change that neither fixes a bug nor adds a feature
perfPerformance improvement
choreBuild process, dependency updates, tooling
ciCI/CD changes

Scopes

Use the module or area affected:

  • core, providers, middleware, storage, pricing
  • dashboard, mcp, cli, server
  • config, docker
  • Provider names: openai, deepgram, etc.

Examples

Plain text
feat(providers): add ElevenLabs TTS support
fix(middleware): prevent double cost tracking on fallback
docs(mcp): add authentication examples
test(storage): cover edge case in daily aggregation query
refactor(core): extract model resolution from gateway to router
chore(deps): bump livekit-agents to 1.6.0

Multi-scope commits

If a change spans multiple scopes, list the primary scope and mention others in the body:

Plain text
feat(mcp): implement project tools (list/get/create/delete)

Also updates storage layer to support project deletion
and adds conftest fixtures for MCP testing.

File organization

  • One class per file for providers (openai_provider.py, not providers.py).
  • Group related functions in a module (middleware/cost_tracker.py).
  • Keep __init__.py files minimal -- a docstring, re-exports of the subpackage's public API, and an __all__ declaration. Nothing else.
  • Use from __future__ import annotations in every module.

Internal modules

Files whose names start with a leading underscore are internal implementation details and not part of the public import surface:

  • src/voicegateway/_version.py -- hatch-vcs generated, do not edit.
  • src/voicegateway/tests/fixtures/streaming/_loader.py -- private test helper.
  • src/voicegateway/inference/_llm.py, _stt.py, _tts.py -- private factories; the public surface is voicegateway.inference.{LLM,STT,TTS}.

A future ruff rule could enforce that nothing under src/voicegateway/ imports a leading-underscore module from a different subpackage; for now the convention is documentation-only.

Public API contract

Every package and subpackage __init__.py declares an explicit __all__ list. This is the public surface:

Python
# voicegateway/server/__init__.py
from voicegateway.server.main import build_app

__all__ = ["build_app"]

When __all__ is the empty list (__all__: list[str] = []), the subpackage exposes nothing at its top level and callers reach into submodules directly:

Python
# Use this:
from voicegateway.core.gateway import Gateway

# Not this (would fail because voicegateway.core has __all__ = []):
from voicegateway.core import Gateway

Names not in __all__, and any leading-underscore module, are internal. They may be renamed or removed in any minor release without a deprecation cycle.

Module-level patterns

The codebase converged on a small set of patterns. New code should follow them unless there is a concrete reason not to.

typing.Protocol vs ABC

Prefer typing.Protocol for structural typing where multiple implementations need to satisfy an interface without sharing helper code (see src/voicegateway/cli/tui/data for a real example -- the DataClient Protocol is satisfied by both HttpClient and LocalClient without inheritance). Use an abstract base class only when the base genuinely supplies shared behaviour (src/voicegateway/providers/base.py's BaseProvider is the canonical example: every concrete provider inherits real helper methods).

Pydantic for config

Anything parsed from YAML or environment variables is a Pydantic model. See src/voicegateway/core/config.py and src/voicegateway/core/schema.py for the project-wide config shape; the validators there are the single source of truth for what voicegw.yaml accepts.

Async throughout

Every I/O path uses async / await. Storage reads, provider calls, HTTP handlers, MCP tools, the dashboard backend -- all async. Synchronous helpers exist only for pure data transformation (parsing, formatting). When in doubt, make it async; mixing sync and async boundaries is the most common source of subtle bugs in this codebase.

Exception handling

Catch specific exception types where possible. except Exception is acceptable at top-level boundaries (provider call sites, MCP tool dispatch, middleware fallback) where the catch is paired with structured logging and a controlled fallback. Avoid broad excepts in narrow code paths -- they hide real bugs and bypass the type system.

Test patterns

See Testing for the full guide. Quick reference:

  • Tests live under src/voicegateway/tests/ mirroring the package layout (src/voicegateway/tests/middleware/ for src/voicegateway/middleware/ tests, etc.).
  • pytest + pytest-asyncio with asyncio_mode = "auto" (configured in pyproject.toml). No @pytest.mark.asyncio is needed; async tests are detected automatically.
  • Shared fixtures live in src/voicegateway/tests/conftest.py. Per-subpackage fixtures live in that subpackage's conftest.py.
  • File-name pattern: test_<thing-under-test>.py. Function-name pattern: test_<behaviour>.
  • Subprocess CLI tests use the patterns in src/voicegateway/tests/cli/test_record_streaming_fixtures_cli.py.
  • Coverage stays at or above the project gate (see [tool.coverage.run] in pyproject.toml).

Naming conventions

ItemConventionExample
Modulessnake_casecost_tracker.py
ClassesPascalCaseCostTracker
Functionssnake_casecreate_provider
ConstantsUPPER_SNAKE_CASEDEFAULT_DB_PATH
Private_leading_underscore_PROVIDER_REGISTRY
Type varsPascalCase or single letterT

On this page