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
<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, SGLANG_MODELS,
VLLM_MODELS, or LLMD_MODELS. llm-d instances also accept suffixed
LLMD_<SUFFIX>_INFERENCE_OBJECTIVE and
LLMD_<SUFFIX>_FAIRNESS_FROM_USER_PATH controls.
Set CONFIGURED_PROVIDER_MODELS_MODE=fallback (default) to use those lists only
when upstream /models fails or is empty, allowlist to expose only the
configured models for providers that define a list and skip their upstream
/models calls, or merge to add configured models on top of the discovered
inventory — useful for models a provider serves but does not include in its
/models listing.
Filtering a provider’s models
model_filter narrows a provider’s inventory to the models you actually want
routable. Use it when a provider’s catalog is far larger than what you want to
expose — for example running OpenRouter’s free tier for experiments while a
separate provider serves production traffic.
Patterns are globs matched case-insensitively against the raw provider model ID.
* matches any run of characters including /, so *:free matches
deepseek/deepseek-r1:free; ? matches exactly one character.
A model with no known pricing is dropped while max_price_per_mtok is set: a
price cap that lets models of unknown price through is not a cap. Prices come
from the model registry and from what the provider itself reports — OpenRouter
publishes its own rates, so its catalog (:free variants included) is priced
without extra configuration. Local providers usually report no pricing at all,
so filter those by pattern rather than by price.
max_price_per_mtok must be a finite number of 0 or more. A negative, NaN, or
infinite cap fails startup rather than silently rejecting every model or
disabling the cap, and so does an env value that is not a number at all — a cost
control the operator believes is in force must never be quietly absent.
Filtering applies to the final inventory, after
configured_provider_models_mode has resolved, so it also narrows models added
by models. Filtered models leave the routable catalog entirely: they disappear
from GET /v1/models and requests for them return “model not found”.
Filtering is a view over what a provider serves, not a deletion. The gateway
keeps the unfiltered inventory in memory and in its model cache, so loosening a
filter — or a model becoming cheap enough to pass the cap — restores it at the
next metadata refresh, without waiting for a successful upstream /models call.
The same filters are available per provider in env:
OPENROUTER_EU_MODEL_FILTER_INCLUDE configures
openrouter-eu.
Priority Order
Effective precedence is:- environment variables
- optional
config/config.yamlorconfig.yaml - 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, setserver.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.
Edits to this file are applied to a running gateway with gomodel --reload,
which re-reads the file and the environment and rebuilds the gateway in place —
see CLI Operations. Nothing changes if the
new file fails to load. server.port and server.pid_file are the exceptions:
both are fixed for the life of the process, so a reload logs a warning and keeps
the running values until the gateway is restarted.
Docker
GoModel readsconfig/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:
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:
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 skipsmap 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. Omittingmax_backoff from resilience.retry leaves it at its built-in default; you do
not need to repeat defaults you are happy with.