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
| Tool | Purpose | Config location |
|---|---|---|
| ruff | Linting + formatting (replaces flake8, isort, Black) | pyproject.toml [tool.ruff] |
| mypy | Static type checking | pyproject.toml [tool.mypy] |
| pre-commit | Runs checks before each commit | .pre-commit-config.yaml |
Ruff
Ruff handles both linting and formatting. It is configured in pyproject.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
# 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:
- Standard library (
import os,from typing import ...) - Third-party (
import pytest,from fastapi import ...) - 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:
[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
ignore_missing_imports = trueRun mypy:
mypyType annotation guidelines
- All public functions must have type annotations
- Use
from __future__ import annotationsat the top of every module (enables PEP 604X | Ysyntax) - Use
dict,list,tuple(lowercase) instead ofDict,List,Tuplefromtyping - Use
X | Noneinstead ofOptional[X] - Use
TYPE_CHECKINGguards for import-only types to avoid circular imports:
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from voicegateway.providers.base import BaseProviderDocstrings
Use Google-style docstrings for all public classes, methods, and functions:
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/Raisessections Args,Returns,Raisessections as needed- Private methods (
_foo) may use shorter docstrings
Conventional Commits {#conventional-commits}
All commit messages must follow Conventional Commits:
<type>(<scope>): <description>
[optional body]
[optional footer]Types
| Type | When to use |
|---|---|
feat | New feature |
fix | Bug fix |
docs | Documentation only |
test | Adding or modifying tests |
refactor | Code change that neither fixes a bug nor adds a feature |
perf | Performance improvement |
chore | Build process, dependency updates, tooling |
ci | CI/CD changes |
Scopes
Use the module or area affected:
core,providers,middleware,storage,pricingdashboard,mcp,cli,serverconfig,docker- Provider names:
openai,deepgram, etc.
Examples
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.0Multi-scope commits
If a change spans multiple scopes, list the primary scope and mention others in the body:
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, notproviders.py). - Group related functions in a module (
middleware/cost_tracker.py). - Keep
__init__.pyfiles minimal -- a docstring, re-exports of the subpackage's public API, and an__all__declaration. Nothing else. - Use
from __future__ import annotationsin 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 isvoicegateway.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:
# 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:
# Use this:
from voicegateway.core.gateway import Gateway
# Not this (would fail because voicegateway.core has __all__ = []):
from voicegateway.core import GatewayNames 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/forsrc/voicegateway/middleware/tests, etc.). pytest+pytest-asynciowithasyncio_mode = "auto"(configured inpyproject.toml). No@pytest.mark.asynciois needed; async tests are detected automatically.- Shared fixtures live in
src/voicegateway/tests/conftest.py. Per-subpackage fixtures live in that subpackage'sconftest.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]inpyproject.toml).
Naming conventions
| Item | Convention | Example |
|---|---|---|
| Modules | snake_case | cost_tracker.py |
| Classes | PascalCase | CostTracker |
| Functions | snake_case | create_provider |
| Constants | UPPER_SNAKE_CASE | DEFAULT_DB_PATH |
| Private | _leading_underscore | _PROVIDER_REGISTRY |
| Type vars | PascalCase or single letter | T |
Related pages
Adding a Provider
VoiceGateway uses a provider registry pattern that makes adding new providers straightforward. Each provider is a single Python file that extends `BaseProvider`. This guide walks through the full proc
Development Setup
This guide walks you through setting up a local development environment for VoiceGateway.