Configuration Guide¶
This guide documents how to configure Continuum Router. The router supports multiple configuration methods with a clear priority system for different deployment scenarios.
Configuration sections:
- Server & Backends — Server settings, backend providers, and connection options
- Health & Caching — Health checks, request settings, retry, caching, and logging
- Security & Admin — API keys, authentication, WebUI, admin endpoints, and ACP
- Advanced — Global prompts, model metadata, hot reload, tracing, load balancing, rate limiting
- Examples — Configuration examples and Rust Builder API
Configuration Methods¶
Continuum Router supports four configuration methods:
- Configuration File (YAML) - Recommended for production
- Environment Variables - Ideal for containerized deployments
- Command Line Arguments - Useful for testing and overrides
- Rust Builder API - Type-safe programmatic configuration for library usage
Configuration Discovery¶
The router automatically searches for configuration files in these locations (in order). At each location, the .yaml extension is tried first, then .toml:
- Path specified by
--configflag ./config.yamlor./config.toml(current directory)~/.config/continuum-router/config.yamlorconfig.toml(user config directory)/etc/continuum-router/config.yamlorconfig.toml(system config directory)
Configuration Priority¶
Configuration is applied in the following priority order (highest to lowest):
- Command-line arguments (highest priority)
- Environment variables
- Configuration file
- Default values (lowest priority)
This allows you to: - Set base configuration in a file - Override specific settings via environment variables in containers - Make temporary adjustments using command-line arguments
Configuration File Format¶
Complete Configuration Example¶
# Continuum Router Configuration
# Generate the full annotated sample with: continuum-router --generate-config
# Server configuration
server:
# bind_address accepts a single string or an array of addresses
# TCP format: "host:port", Unix socket format: "unix:/path/to/socket"
bind_address: "0.0.0.0:8080" # Single address
# bind_address: # Or multiple addresses:
# - "0.0.0.0:8080" # TCP on all interfaces
# - "unix:/var/run/router.sock" # Unix socket (Linux/macOS, Windows 10 1809+)
# socket_mode: 0o660 # Optional: Unix socket file permissions
workers: 4 # Number of worker threads (0 = auto-detect)
connection_pool_size: 100 # Max idle connections per backend
# Model metadata configuration (optional)
model_metadata_file: "model-metadata.yaml" # Base (layer zero) metadata file
model_metadata_dirs: # Extra model-metadata.d drop-in directories
- "/etc/router/metadata" # Applied after the conventional ones, in order
# Load balancing strategy: RoundRobin (default), WeightedRoundRobin,
# LeastLatency, Random, ConsistentHash, PrefixAwareHash
selection_strategy: RoundRobin
# Backend configuration
backends:
# Native OpenAI API with built-in configuration
- name: "openai"
type: openai # Use native OpenAI backend
api_key: "${CONTINUUM_OPENAI_API_KEY}" # Loaded from environment
org_id: "${CONTINUUM_OPENAI_ORG_ID}" # Optional organization ID
weight: 3
models: # Specify which models to use
- gpt-4o
- gpt-4o-mini
- o3-mini
- text-embedding-3-large
retry_override: # Backend-specific retry settings (optional)
max_attempts: 5
initial_delay: "200ms"
max_delay: "30s"
backoff_multiplier: 2.0
jitter: true
retryable_status_codes: [429, 502, 503, 504]
retryable_errors: [ConnectionError, TimeoutError]
timeout: "60s"
# Generic OpenAI-compatible backend with custom metadata
- name: "openai-compatible"
url: "https://custom-llm.example.com"
weight: 1
models:
- "gpt-4"
- "gpt-3.5-turbo"
model_configs: # Enhanced model configuration with metadata
- id: "gpt-4"
aliases: # Alternative IDs that share this metadata (optional)
- "gpt-4-0125-preview"
- "gpt-4-turbo-preview"
metadata:
display_name: "GPT-4"
summary: "Most capable GPT-4 model for complex tasks"
capabilities: ["text", "image", "function_calling"]
knowledge_cutoff: "2024-04"
pricing:
input_tokens: 0.03
output_tokens: 0.06
limits:
context_window: 128000
max_output: 4096
# Ollama local server with automatic URL detection
- name: "local-ollama"
type: ollama # Defaults to http://localhost:11434
weight: 2
models:
- "llama2"
- "mistral"
- "codellama"
# vLLM server
- name: "vllm-server"
type: vllm
url: "http://localhost:8000"
weight: 1
# Models will be discovered automatically if not specified
# Models with namespace prefixes (e.g., "custom/gpt-4") will automatically
# match metadata for base names (e.g., "gpt-4")
# Google Gemini API (native backend)
- name: "gemini"
type: gemini # Use native Gemini backend
api_key: "${CONTINUUM_GEMINI_API_KEY}" # Loaded from environment
weight: 2
models:
- gemini-3.1-pro-preview
- gemini-3-flash-preview
- gemini-2.5-pro
- gemini-2.5-flash
# Health monitoring configuration
health_checks:
interval: "30s" # How often to check backend health
timeout: "5s" # Timeout for health check requests
unhealthy_threshold: 3 # Failures before marking unhealthy
healthy_threshold: 2 # Successes before marking healthy
endpoint: "/health" # Endpoint used for health checks
warmup_check_interval: "1s" # Accelerated interval while a backend warms up (HTTP 503)
max_warmup_duration: "300s" # Max time in accelerated warmup mode
block_startup: true # Gate listener binding on pre-warm and the initial health check
# Request handling and timeout configuration
timeouts:
connection: "10s" # TCP connection establishment timeout
request:
standard: # Non-streaming requests
first_byte: "30s" # Time to receive first byte
total: "180s" # Total request timeout (3 minutes)
streaming: # Streaming (SSE) requests
first_byte: "60s" # Time to first SSE chunk
chunk_interval: "30s" # Max time between chunks
total: "600s" # Total streaming timeout (10 minutes)
image_generation: # Image generation requests (DALL-E, etc.)
first_byte: "60s" # Time to receive first byte
total: "180s" # Total timeout (3 minutes default)
model_overrides: # Model-specific timeout overrides
gpt-5-latest:
streaming:
total: "1200s" # 20 minutes for GPT-5
gpt-4o:
streaming:
total: "900s" # 15 minutes for GPT-4o
health_check:
timeout: "5s" # Health check timeout
interval: "30s" # Health check interval
# Global retry and resilience configuration
retry:
max_attempts: 3 # Maximum retry attempts
initial_delay: "100ms" # Initial delay between retries
max_delay: "10s" # Maximum delay between retries
backoff_multiplier: 2.0 # Exponential backoff multiplier
jitter: true # Add random jitter to delays
retryable_status_codes: [429, 502, 503, 504]
retryable_errors: [ConnectionError, TimeoutError]
timeout: "30s" # Total retry budget
# Logging configuration
logging:
level: "info" # Log level: trace, debug, info, warn, error
format: "json" # Log format: json, pretty
# Files API configuration
files:
enabled: true # Enable/disable Files API endpoints
max_file_size: 536870912 # Maximum file size in bytes (default: 512MB)
storage_path: "./data/files" # Storage path for uploaded files (supports ~)
retention_days: 0 # Startup retention sweep in days (0 = keep forever)
metadata_storage: persistent # Metadata backend: "memory" or "persistent" (default)
cleanup_orphans_on_startup: false # Auto-cleanup orphaned files on startup
# Authentication and authorization
auth:
method: api_key # "none" or "api_key" (default)
required_scope: files # API key scope required for access
enforce_ownership: true # Users can only access their own files
admin_can_access_all: true # Admin scope grants access to all files
# Distributed tracing configuration
tracing:
enabled: true # Enable/disable distributed tracing
w3c_trace_context: true # Support W3C Trace Context (traceparent header)
headers:
trace_id: "X-Trace-ID" # Header name for trace ID
request_id: "X-Request-ID" # Header name for request ID
correlation_id: "X-Correlation-ID" # Header name for correlation ID
# Circuit breaker configuration
circuit_breaker:
enabled: true # Protect ordinary proxy dispatch and expose Admin controls
failure_threshold: 5 # Consecutive failures to open the circuit
failure_rate_threshold: 0.5 # Failure rate (0.0-1.0) to open the circuit
minimum_requests: 10 # Minimum requests before rate evaluation
timeout: "60s" # Time before attempting recovery (half-open)
half_open_max_requests: 3 # Trial requests allowed in half-open state
half_open_success_threshold: 2 # Successes required to close the circuit
# Ordinary proxy traffic records outcomes and routes around open circuits.
# Health checks, retry, and fallback remain complementary layers.
# Rate limiting configuration
rate_limiting:
enabled: true # Enable rate limiting
storage: memory # "memory" or "redis"
limits:
per_client:
requests_per_second: 10
burst_capacity: 20
global:
requests_per_second: 1000
burst_capacity: 2000
# Admin API configuration
admin:
auth:
method: bearer # Auth method: none, bearer, basic, ip_whitelist, api_key
bearer_token: "${ADMIN_TOKEN}" # Admin authentication token
stats:
enabled: true # Enable/disable stats collection
retention_window: 24h # Ring-buffer retention for windowed queries
token_tracking: true # Parse response bodies for token usage
persistence:
enabled: true # Enable stats persistence across restarts
path: ./data/stats.json # File path for the snapshot
snapshot_interval: 5m # How often to write periodic snapshots
max_age: 7d # Discard snapshots older than this on startup
# Metrics and monitoring configuration
metrics:
enabled: true # Enable Prometheus metrics collection
path: "/metrics" # Metrics endpoint path
Minimal Configuration¶
# Minimal configuration - other settings will use defaults
server:
bind_address: "0.0.0.0:8080"
backends:
- name: "ollama"
url: "http://localhost:11434"
- name: "lm-studio"
url: "http://localhost:1234"
health_checks:
interval: "30s"
timeout: "5s"
unhealthy_threshold: 3
healthy_threshold: 2
endpoint: "/health"
logging:
level: "info"
format: "json"
Environment Variables¶
Environment variables enter the configuration through two mechanisms:
- Direct overrides: a small set of
CONTINUUM_*variables that override their config-file counterparts at startup. -
General
${VAR}interpolation: every string-valued YAML/TOML field is interpolated when the file is loaded, not just secrets.logging.level: "${LOG_LEVEL}",files.storage_path: "/data/${TENANT}", timeout strings, URLs, and paths all work.${VAR}expands to the value ofVAR.${VAR:-default}expands toVARif set, otherwise the literaldefault(which may be empty).$$is an escape producing a literal$, so$${VAR}yields the literal text${VAR}.- Variable names must match
[A-Za-z_][A-Za-z0-9_]*; a${...}with an invalid name is left literal. Only string scalars are interpolated, so a${VAR}where a number or boolean is expected fails typed parsing.
At startup and hot-reload, an unset
${VAR}with no default aborts loading with an error naming the config path.continuum-router config validateinstead reports the unset reference as a warning and keeps the placeholder, so a template can be validated without production secrets present.
Direct Overrides¶
| Variable | Type | Description |
|---|---|---|
CONTINUUM_BIND_ADDRESS |
string | Overrides server.bind_address |
CONTINUUM_BACKEND_URLS |
string | Comma-separated backend URLs; overrides the backends list |
CONTINUUM_LOG_LEVEL |
string | Overrides logging.level (trace, debug, info, warn, error) |
CONTINUUM_SELECTION_STRATEGY |
string | Overrides selection_strategy; accepts RoundRobin, WeightedRoundRobin, LeastLatency, Random, ConsistentHash, PrefixAwareHash. An unrecognized value fails startup rather than falling back silently. Precedence: --selection-strategy > this variable > the file value > RoundRobin |
CONTINUUM_FILES_AUTH_METHOD |
string | Overrides files.auth.method; accepts none or api_key. Applies even when the file has no files: section. An unrecognized value fails startup rather than falling back silently |
CONTINUUM_FILES_AUTH_SCOPE |
string | Overrides files.auth.required_scope |
CONTINUUM_FILES_ENFORCE_OWNERSHIP |
boolean | Overrides files.auth.enforce_ownership. Accepts exactly true or false (lowercase); any other value fails startup |
CONTINUUM_FILES_ADMIN_ACCESS_ALL |
boolean | Overrides files.auth.admin_can_access_all. Same strict true/false syntax as CONTINUUM_FILES_ENFORCE_OWNERSHIP |
RUST_LOG |
string | Rust-specific logging filter configuration |
Native Backend API Keys¶
Native backends fall back to these variables when no api_key is set in the config:
| Variable | Used by |
|---|---|
CONTINUUM_OPENAI_API_KEY |
type: openai backends |
CONTINUUM_OPENAI_ORG_ID |
type: openai backends (optional organization ID) |
CONTINUUM_ANTHROPIC_API_KEY |
type: anthropic backends |
CONTINUUM_GEMINI_API_KEY |
type: gemini backends |
API Key Management¶
| Variable | Type | Default | Description |
|---|---|---|---|
CONTINUUM_API_KEY |
string | - | Single API key for simple deployments |
CONTINUUM_API_KEY_SCOPES |
string | "read,write" |
Comma-separated scopes for the API key |
CONTINUUM_API_KEY_USER_ID |
string | "admin" |
User ID associated with the API key |
CONTINUUM_API_KEY_ORG_ID |
string | "default" |
Organization ID associated with the API key |
CONTINUUM_DEV_MODE |
boolean | false |
Enable development API keys (DO NOT use in production) |
Model Metadata Download Token¶
continuum-router metadata download reads a GitHub token from these variables, in order, and uses the first non-empty one. See Model metadata download for the full rules.
| Variable | Type | Description |
|---|---|---|
CONTINUUM_GITHUB_TOKEN |
string | Preferred. GitHub token with read access to the continuum-router repository |
GITHUB_TOKEN |
string | Fallback; the name CI runners already export |
GH_TOKEN |
string | Fallback; the name the GitHub CLI already exports |
The token is required while the repository is private, is sent only to raw.githubusercontent.com and api.github.com, and is never logged or printed.
Example Environment Configuration¶
# Direct overrides
export CONTINUUM_BIND_ADDRESS="0.0.0.0:9000"
export CONTINUUM_BACKEND_URLS="http://localhost:11434,http://localhost:1234"
export CONTINUUM_LOG_LEVEL="debug"
# Referenced from config.yaml via ${...} substitution
export CONTINUUM_OPENAI_API_KEY="sk-..."
export ADMIN_TOKEN="my-admin-token"
# Start the router
continuum-router
All other settings (health checks, timeouts, retry, caching, and the rest of the Files API configuration beyond files.auth) are configured through the configuration file.
Command Line Arguments¶
Command-line arguments provide the highest priority configuration method and are useful for testing and temporary overrides.
Core Options¶
| Argument | Type | Description |
|---|---|---|
--mode <MODE> |
enum | Server mode: http (default) or stdio (ACP JSON-RPC 2.0 transport) |
-c, --config <FILE> |
path | Configuration file path |
--generate-config |
flag | Generate sample YAML config and exit |
--generate-example-config |
flag | Generate example configuration documentation (env vars, routing, validation rules, precedence) and exit |
--generate-toml-config |
flag | Generate TOML-format sample config and exit |
--model-metadata <FILE> |
path | Path to model metadata YAML file (overrides config) |
Backend Configuration¶
| Argument | Type | Description |
|---|---|---|
--backends <URLs> |
string | Comma-separated backend URLs |
--backend-url <URL> |
string | Single backend URL (deprecated) |
Server Configuration¶
| Argument | Type | Description |
|---|---|---|
--bind <ADDRESS> |
string | Server bind address |
--connection-pool-size <SIZE> |
integer | HTTP connection pool size |
Load Balancing¶
| Argument | Type | Description |
|---|---|---|
--selection-strategy <STRATEGY> |
string | Load balancing strategy: RoundRobin (default), WeightedRoundRobin, LeastLatency, Random, ConsistentHash, or PrefixAwareHash |
Health Check Configuration¶
| Argument | Type | Description |
|---|---|---|
--disable-health-checks |
flag | Disable health monitoring |
--health-check-interval <SECONDS> |
integer | Health check interval |
--health-check-timeout <SECONDS> |
integer | Health check timeout |
--unhealthy-threshold <COUNT> |
integer | Failures before unhealthy |
--healthy-threshold <COUNT> |
integer | Successes before healthy |
Configuration Utilities¶
| Argument | Type | Description |
|---|---|---|
--migrate-config-file <FILE> |
path | Migrate and fix configuration file issues without starting the router; creates a backup and reports all changes (YAML and TOML) |
--dry-run |
flag | With --migrate-config-file, preview migration changes without applying them |
Container Health Check¶
| Argument | Type | Description |
|---|---|---|
--health-check |
flag | Check whether a running server is healthy and exit (0 = healthy, 1 = unhealthy); intended for Docker HEALTHCHECK |
--health-check-url <URL> |
string | Health endpoint to probe (default: http://localhost:8080/health) |
Subcommands¶
| Command | Description |
|---|---|
auth login --backend <NAME> |
Run the OAuth device authorization flow for the named backend (its auth.type must be oauth) and persist tokens to auth.oauth.token_store. See Server & Backends for the OAuth backend configuration. |
config validate <FILE> |
Validate YAML/TOML and print a structured JSON report. Exits non-zero when invalid. |
config generate --template <NAME> [--output <FILE>] |
Generate one of the nine embedded templates: minimal, development, multi-provider, production-ha, api-gateway, kv-cache-optimized, smart-routing, disaggregated, or cost-optimized. |
config diff <A> <B> |
Compare effective configuration values. Secret-looking values are redacted. |
config show [--resolved] <FILE> |
Print normalized configuration. --resolved applies provider defaults and supported field-specific environment expansion; plaintext credentials may be printed. |
metadata download (alias metadata update) |
Download model-metadata.yaml from the continuum-router repository and install it at the resolved metadata path. See Model metadata download below. |
metadata show [--resolved] [--json] |
Print the metadata assembled from the base file and every model-metadata.d/ drop-in, with the applied layers and the skipped files. --resolved prints the effective typed document instead of the raw merge. See Layered model metadata below. |
mcp-serve |
Run the configuration-assistant MCP server over stdio. Requires the mcp Cargo feature, which is included in default and official release builds. |
Model metadata download¶
model-metadata.yaml carries the router-wide pricing, context windows, capabilities, thinking patterns, and /v1/models response defaults. It changes far more often than the router binary, and a release archive, Debian package, or container ships without it, so metadata download fetches the canonical copy and installs it where the router reads it.
export CONTINUUM_GITHUB_TOKEN="$(gh auth token)" # required while the repository is private
continuum-router metadata download # fetch main, install at the resolved path
continuum-router metadata download --check # report staleness only, write nothing
continuum-router metadata download --ref v1.15.5 # pin to a release tag
continuum-router metadata download --output ./model-metadata.yaml
Authentication¶
The continuum-router repository is private, so an unauthenticated request to the default source returns 404 for every ref, including refs that certainly exist. Supply a GitHub token with read access through one of these variables, checked in order with the first non-empty value winning:
| Variable | Notes |
|---|---|
CONTINUUM_GITHUB_TOKEN |
Preferred. Matches the router's own CONTINUUM_* environment surface, so the token can be scoped to the router rather than shared with every tool on the host |
GITHUB_TOKEN |
The name CI runners already export |
GH_TOKEN |
The name the GitHub CLI already exports |
Surrounding whitespace is trimmed and an all-whitespace value counts as unset, so a stray export GITHUB_TOKEN= does not shadow a real GH_TOKEN.
There is no --token flag on purpose: a secret passed as a command-line argument lands in shell history and in ps output for every user on the machine.
The token is attached only when the request host is exactly raw.githubusercontent.com or api.github.com. A --url pointing at a mirror, or at a lookalike host such as raw.githubusercontent.com.evil.test, never receives it. The token is never logged or printed; the report names the variable it came from, not its value.
A 404 tells you which case you are in. Without a token, the error names the three variables and says the repository may be private. With a token, it says the ref or path may not exist or the token may lack access.
A --url may carry basic-auth credentials for a private mirror (https://ci:token@mirror.internal/model-metadata.yaml). They are sent as an Authorization: Basic header, and every place the command prints the source (the human report, the --json source_url field, and the error messages) strips the userinfo first, so the password does not reach a CI log or an archived report.
Flags¶
| Flag | Default | Behavior |
|---|---|---|
--ref <GIT_REF> |
main |
Branch or tag to fetch from. Conflicts with --url. |
--url <URL> |
derived from --ref |
Full HTTPS source override for mirrors and air-gapped installs. |
--output <FILE> |
resolved (below) | Explicit destination path. |
--check |
off | Fetch and compare only; write nothing. |
--force |
off | Rewrite even when the content hash is unchanged. |
--no-backup |
off | Skip the <file>.bak copy of the file being replaced. |
--timeout <SECONDS> |
30 |
Whole-request timeout. |
--json |
off | Machine-readable report instead of the human report. |
The global -c/--config and --model-metadata are reused for destination resolution.
Destination resolution¶
The destination is resolved in this order and always printed, on failure as well as on success:
--output <FILE>- the global
--model-metadata <FILE>, tilde-expanded model_metadata_filefrom the loaded config, tilde-expandedmodel-metadata.yamlnext to the discovered config file~/.config/continuum-router/model-metadata.yaml
--model-metadata sits above the config field because that is what it does for the router itself: it overrides model_metadata_file. So continuum-router --model-metadata /etc/cr/meta.yaml metadata download installs the file exactly where that router instance will read it.
Missing parent directories are created.
Safety behavior¶
- The download is parsed and validated against the metadata schema before anything on disk is touched. A malformed or schema-invalid response exits non-zero and leaves the existing file byte-for-byte unchanged.
- Replacement is atomic through a temporary file in the destination directory, and an existing file's mode is preserved (a new file gets
0644). - The previous file is kept as
<file>.bakunless--no-backupis given. - Content is compared by SHA-256, so an unchanged file is skipped without a write unless
--forceis given. - Only HTTPS sources are accepted, and redirects that leave HTTPS are refused.
- Responses larger than 8 MiB are rejected.
Exit codes¶
| Code | Meaning |
|---|---|
0 |
Written successfully, or already up to date |
1 |
Fetch, validation, or write failure |
2 |
--check only: an update is available |
The 2 case lets cron and CI branch on staleness without treating it as an error.
Effect on a running router¶
Updating the file does not hot-reload a running router. The config watcher watches only the config file path, and both model_metadata_file and model_metadata_dirs are marked requires_restart in the admin config schema, so restart the router or trigger a config reload for the new metadata to take effect. The command prints this reminder on every run.
Layered model metadata¶
model_metadata_file is layer zero, not the whole story. Metadata is assembled from a layered search path so the vendor baseline this project ships (and metadata download overwrites) stays separate from the files an operator maintains.
Search path¶
Lowest precedence first, so the more specific location wins by being applied last:
model_metadata_file(the base file, tilde-expanded)/etc/continuum-router/model-metadata.d/~/.config/continuum-router/model-metadata.d/./model-metadata.d/- every directory listed in
model_metadata_dirs, in the order given
model_metadata_dirs supplements the conventional locations rather than replacing them, and its entries are used verbatim: the model-metadata.d name is not appended, so a Kubernetes ConfigMap mounted at /etc/router/metadata/ is listed as exactly that path. The global --model-metadata flag keeps its meaning: it sets the base file, it does not disable drop-ins.
A typical layout:
/etc/continuum-router/model-metadata.yaml # vendor baseline, safe to overwrite
/etc/continuum-router/model-metadata.d/
10-private-models.yaml # models we host ourselves
50-negotiated-pricing.yaml # our contract rates for gpt-5
90-local-overrides.yaml
File selection¶
Within a directory:
- only regular files ending in
.yamlor.ymlare read, matched case-insensitively - files apply in lexicographic filename order, which is what makes the
10-/50-/90-numeric prefix convention work - subdirectories are never recursed into
- dotfiles,
*~, and*.swpare skipped, so editor backups and vim swap files are ignored - a symlink is resolved but must still land on a regular file
- a missing directory is not an error
Merge rules¶
Layers merge on the parsed YAML, before deserialization:
- mapping into mapping: recursive key-wise merge, so keys the higher layer omits inherit from the lower one
- sequence: replaced wholesale, never unioned
- scalar (including an explicit
null): replaced modelsentries are keyed byid: an entry whoseidalready exists merges into the existing entry, and a newidis appendedresponse_defaultsis a mapping, so it merges per field
Merging before deserialization is what makes a narrow override possible. An override that never mentions responses_only cannot clear an inherited true, and a field added to the metadata schema later merges correctly with no new code.
Sequences replace rather than union deliberately. aliases and capabilities are sequences, and union semantics would make removing an inherited entry impossible.
Given a vendor baseline:
models:
- id: gpt-5
aliases: [gpt-5-latest]
metadata:
display_name: GPT-5
capabilities: [chat, reasoning, vision]
pricing: { input_tokens: 1.25, output_tokens: 10.0 }
and model-metadata.d/50-negotiated-pricing.yaml:
the effective result keeps display_name, capabilities, and aliases tracking upstream while pricing uses the negotiated rates. The operator maintains four lines instead of a full copy that rots.
Precedence outside the metadata layer¶
Unchanged: backend model_configs still win over the merged metadata, which still wins over the built-in OpenAI registry. Layering changes only how the metadata layer itself is assembled.
Failure handling and limits¶
A drop-in that fails to read, parse, or validate is skipped with a warning and the rest of the search path still loads, so one malformed operator file cannot take down a running router. A malformed base model_metadata_file stays fatal, exactly as before. Blame is assigned once. A layer is only named when the merge satisfied the metadata schema immediately before that layer was applied, so a base file that already breaks the schema is reported as the cause instead of every drop-in that happens to sit on top of it. metadata show prints that attribution under Schema failure introduced by.
At most 64 files totalling at most 8 MiB are assembled. Exceeding either cap is an error, not a truncation: applying a partial override set silently would be worse than refusing to assemble.
A fatal assembly failure during a hot reload aborts the whole reload, not just the metadata layer. The router logs an error naming the assembly as the cause and keeps serving the previously loaded configuration together with the model metadata it already had, exactly as it already does for a config file that fails validation. A reload is all-or-nothing. The alternative, publishing a configuration whose metadata caches are empty, would silently revert pricing, context windows, capabilities, and /v1/models response defaults to backend-reported values with no restart to explain it. This matters most for the drop-in directories: adding a 65th file, or one large enough to push the total past 8 MiB, is a fatal assembly failure that anyone with write access to a model-metadata.d/ directory can cause without touching an existing file.
Inspecting the result¶
Skipping is invisible to clients, so metadata show is the supported way to see it:
continuum-router metadata show # the raw merge, with the layer report
continuum-router metadata show --resolved # the effective typed document the router loads
continuum-router metadata show --json # the same report as a machine-readable object
Both modes print the search path, the applied layers in application order (with every model id a later layer took over and from which file), and every skipped file with its reason. The header is written as YAML comments, so metadata show > merged.yaml still produces a file the router can read. Without --resolved the raw merge is printed verbatim, including keys the schema ignores, which is what makes a typo visible; with --resolved the merge is deserialized, response_defaults is validated and sanitized, and the typed value is re-serialized.
metadata show exits 0 whenever the search path could be assembled, including when files were skipped, and 1 on a fatal failure such as an unreadable base file or an exceeded cap.
If the discovered or explicitly named config file could not be found or could not be loaded, metadata show says so instead of silently describing a search path that omits that config's model_metadata_file and model_metadata_dirs: a # WARNING: header line in the human report, and a config_warning field alongside --json.
Relationship to metadata download¶
metadata download writes only the base model_metadata_file. It never reads from or writes into a drop-in directory. That separation is what makes the download safe to run repeatedly: the vendor layer is disposable and reproducible, the operator layer is untouched. The documented workflow on upgrade is "run metadata download, keep your changes in model-metadata.d/".
Drop-in changes do not hot-reload either. model_metadata_dirs is requires_restart in the admin config schema, same as model_metadata_file.
Example CLI Usage¶
# Use config file with overrides
continuum-router --config config.yaml --bind "0.0.0.0:9000"
# Override backends temporarily
continuum-router --config config.yaml --backends "http://localhost:11434"
# Use custom model metadata file
continuum-router --config config.yaml --model-metadata /path/to/custom-metadata.yaml
# Use model metadata with tilde expansion
continuum-router --model-metadata ~/configs/model-metadata.yaml
# Adjust health check settings for testing
continuum-router --config config.yaml --health-check-interval 10
# Generate sample configuration
continuum-router --generate-config > my-config.yaml
# Validate it and generate a focused template
continuum-router config validate my-config.yaml
continuum-router config generate --template production-ha --output production.yaml
# Inspect or compare configurations (`--resolved` output may contain secrets)
continuum-router config show my-config.yaml
continuum-router config diff my-config.yaml production.yaml
# Migrate an outdated config file (preview first, then apply)
continuum-router --migrate-config-file config.yaml --dry-run
continuum-router --migrate-config-file config.yaml
# Docker HEALTHCHECK probe
continuum-router --health-check --health-check-url http://localhost:8080/health
# OAuth device-flow login for a ChatGPT/Codex backend
continuum-router --config config.yaml auth login --backend chatgpt
# Register the configuration assistant with an MCP client
claude mcp add continuum-router-config -- continuum-router mcp-serve
# Refresh model metadata, or check whether the local copy is stale (exit 2 = update available)
export CONTINUUM_GITHUB_TOKEN="$(gh auth token)"
continuum-router metadata download
continuum-router metadata download --check --json
# Install at the path this router instance reads
continuum-router --model-metadata /etc/continuum-router/model-metadata.yaml metadata download
# Inspect the metadata actually in effect after model-metadata.d layering
continuum-router metadata show --resolved
continuum-router metadata show --json