Skip to content

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:

  1. Configuration File (YAML) - Recommended for production
  2. Environment Variables - Ideal for containerized deployments
  3. Command Line Arguments - Useful for testing and overrides
  4. 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:

  1. Path specified by --config flag
  2. ./config.yaml or ./config.toml (current directory)
  3. ~/.config/continuum-router/config.yaml or config.toml (user config directory)
  4. /etc/continuum-router/config.yaml or config.toml (system config directory)

Configuration Priority

Configuration is applied in the following priority order (highest to lowest):

  1. Command-line arguments (highest priority)
  2. Environment variables
  3. Configuration file
  4. 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:

  1. Direct overrides: a small set of CONTINUUM_* variables that override their config-file counterparts at startup.
  2. 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 of VAR.
    • ${VAR:-default} expands to VAR if set, otherwise the literal default (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 validate instead 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

continuum-router --help
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:

  1. --output <FILE>
  2. the global --model-metadata <FILE>, tilde-expanded
  3. model_metadata_file from the loaded config, tilde-expanded
  4. model-metadata.yaml next to the discovered config file
  5. ~/.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>.bak unless --no-backup is given.
  • Content is compared by SHA-256, so an unchanged file is skipped without a write unless --force is 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:

  1. model_metadata_file (the base file, tilde-expanded)
  2. /etc/continuum-router/model-metadata.d/
  3. ~/.config/continuum-router/model-metadata.d/
  4. ./model-metadata.d/
  5. 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 .yaml or .yml are 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 *.swp are 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
  • models entries are keyed by id: an entry whose id already exists merges into the existing entry, and a new id is appended
  • response_defaults is 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:

models:
  - id: gpt-5
    metadata:
      pricing: { input_tokens: 0.90, output_tokens: 7.20 }

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