Skip to main content
The goal in GoModel is to make config.yaml optional as much as possible. Prefer environment variables for normal deployments, CI, containers, and secret injection. Use config.yaml when you need structure that env vars cannot express cleanly, especially:
  • per-provider resilience overrides
  • custom provider instance names that do not fit the generated <provider-type>-<suffix> env naming
  • richer reviewable provider model lists, especially when using allowlist mode
  • reviewable budget limits by user_path
  • larger nested config that is easier to review in one file
For multiple provider instances, env vars support <PROVIDER>_<SUFFIX>_API_KEY, <PROVIDER>_<SUFFIX>_BASE_URL, and <PROVIDER>_<SUFFIX>_MODELS. The suffix becomes a hyphenated provider name: OPENAI_EAST_API_KEY registers openai-east, and OLLAMA_A_BASE_URL registers ollama-a. Azure also supports <PROVIDER>_<SUFFIX>_API_VERSION. Google Vertex AI follows the same suffix-to-name rule (VERTEX_US_PROJECT registers vertex-us). Vertex project, location, and other Vertex settings must use the same suffix for each instance: for example, pair VERTEX_US_PROJECT with VERTEX_US_LOCATION, not the generic VERTEX_PROJECT or VERTEX_LOCATION. VERTEX_AUTH_TYPE defaults to gcp_adc. Configured provider model lists can stay in env via <PROVIDER>_MODELS, for example OPENROUTER_MODELS, ORACLE_MODELS, AZURE_MODELS, or VLLM_MODELS. Set CONFIGURED_PROVIDER_MODELS_MODE=fallback (default) to use those lists only when upstream /models fails or is empty, or allowlist to expose only the configured models for providers that define a list and skip their upstream /models calls.

Priority Order

Effective precedence is:
  1. environment variables
  2. optional config/config.yaml or config.yaml
  3. built-in defaults from code
.env is not a separate priority layer. It is just a convenient way to load environment variables before startup. Each layer is applied on top of the previous one, so a provider declared in config.yaml and overlaid by env vars still counts as coming from the file.

Startup Validation

Declarative config is parsed strictly: an unknown key is a startup error, not a silently ignored one. This catches typos (prot: instead of port:) and, more importantly, misindented sections — see the gotcha below. This applies to config.yaml and to the env vars that declare the same structures as JSON: VIRTUAL_MODELS, SET_RATE_LIMIT_*, and SET_BUDGET_*. Because env entries override YAML entries, a typo in one of them would otherwise silently win over a correct YAML entry. The default is strict because a dropped providers, rate_limits, budgets, or guardrails entry silently changes routing, cost, or security — a cost cap that never applies looks exactly like a cost cap that does. Set CONFIG_STRICT=false to downgrade unknown keys to warnings and boot without them. This is for rolling a binary back under a newer config file, not for everyday use:
CONFIG_STRICT relaxes which keys are accepted, never whether a value makes sense. A malformed value (port: [9999, 8080]) aborts startup in either mode. Entity-level validation also still applies: a virtual model whose target names a provider that does not exist fails startup even when the key that would have declared that provider was ignored with a warning. To confirm which layer supplied your providers, GoModel logs one line at boot:
from_config_file: 0 while a file is loaded means the file contributed no providers. GoModel also logs config file loaded with the resolved path, or no config file found with the paths it searched.

Current Schema

The current source of truth lives in the main codebase: For deployments mounted below a domain root, set server.base_path or BASE_PATH. For example, BASE_PATH=/g serves the gateway at /g/v1/..., /g/admin/..., and /g/health. To change the inbound user path header, set server.user_path_header or USER_PATH_HEADER. The default remains X-GoModel-User-Path.

Docker

GoModel reads config/config.yaml first, then config.yaml. With docker run, you can keep a host file named config.yml and mount it to the in-container path GoModel expects:
With Docker Compose:
If env vars and YAML both define the same setting, the environment variable wins.
If the mounted path exists but cannot be read — most often a directory bind-mounted where a file was expected — startup fails rather than silently falling back to defaults.

Gotchas

Unresolved ${VAR} placeholders drop the provider

If ${OPENAI_API_KEY} appears in YAML but the env var is not actually set, the literal string ${OPENAI_API_KEY} ends up as the API key value. The credential filter detects the ${ substring and removes the provider entirely. This also applies if ${VAR} appears in the middle of the value, such as prefix-${OPENAI_API_KEY}. Always verify your env vars are exported before starting the process, or supply a default: ${OPENAI_API_KEY:-}.

A misindented providers: section is rejected

YAML reads this as an empty providers: section plus two unrelated top-level keys, not as two providers:
Strict parsing rejects it at startup, naming every offending line:
Indent provider entries two spaces under providers:. Before strict parsing, this booted with zero YAML providers and no error. With CONFIG_STRICT=false it boots with zero YAML providers and one warning per unindented key.

Per-provider resilience can only come from YAML

The env-var override walk skips map fields. RETRY_MAX_RETRIES changes the global default for all providers but cannot target a single provider. Per-provider tuning requires a providers.<name>.resilience: block in config.yaml.

Partial YAML leaves the rest at defaults

YAML is unmarshalled onto a struct that has already been populated with built-in defaults. Only fields that appear in the file are written. Omitting max_backoff from resilience.retry leaves it at its built-in default; you do not need to repeat defaults you are happy with.