Model and Provider Configuration (models.yml)
August 2, 2026 · View on GitHub
This document describes how the coding-agent currently loads models, applies overrides, resolves credentials, and chooses models at runtime.
What controls model behavior
Primary implementation files:
src/config/model-registry.ts— loads built-in + custom models, provider overrides, runtime discovery, auth integrationsrc/config/model-resolver.ts— parses model patterns and selects models for the default and agent rolessrc/config/settings-schema.ts— model-related settings (modelRoles, provider transport preferences)src/session/auth-storage.ts— API key + OAuth resolution orderpackages/ai/src/models.tsandpackages/ai/src/types.ts— built-in providers/models andModel/compattypes
Config file location and legacy behavior
Default config path:
~/.gjc/agent/models.yml
Legacy behavior still present:
- If
models.ymlis missing andmodels.jsonexists at the same location, it is migrated tomodels.yml. - Explicit
.json/.jsoncconfig paths are still supported when passed programmatically toModelRegistry.
models.yml shape
providers:
<provider-id>:
# provider-level config
equivalence:
overrides:
<provider-id>/<model-id>: <canonical-model-id>
exclude:
- <provider-id>/<model-id>
provider-id is the canonical provider key used across selection and auth lookup.
equivalence is optional and configures canonical model grouping on top of concrete provider models:
overridesmaps an exact concrete selector (provider/modelId) to an official upstream canonical idexcludeopts a concrete selector out of canonical grouping
Provider-level fields
providers:
my-provider:
baseUrl: https://api.example.com/v1
apiKey: MY_PROVIDER_API_KEY
api: openai-completions
headers:
X-Team: platform
authHeader: true
auth: apiKey
disableStrictTools: false # set true for Anthropic-compatible endpoints that reject the strict field
cacheRetention: short # none | short | long; model entries and modelOverrides can override this
discovery:
type: ollama
modelOverrides:
some-model-id:
name: Renamed model
cacheRetention: long
models:
- id: some-model-id
name: Some Model
api: openai-completions
reasoning: false
input: [text]
cost:
input: 0
output: 0
cacheRead: 0
cacheWrite: 0
contextWindow: 128000
maxTokens: 16384
headers:
X-Model: value
cacheRetention: none
thinking:
minLevel: low
maxLevel: xhigh
mode: effort
defaultLevel: high
levels: [low, medium, high, xhigh]
compat:
supportsStore: true
supportsDeveloperRole: true
supportsReasoningEffort: true
maxTokensField: max_completion_tokens
openRouterRouting:
only: [anthropic]
vercelGatewayRouting:
order: [anthropic, openai]
extraBody:
gateway: m1-01
controller: mlx
modelBindings:
modelRoles:
default: my-provider/some-model-id:high
agentModelOverrides:
executor: my-provider/some-model-id
Allowed provider/model api values
openai-completionsopenai-responsesopenai-codex-responsesazure-openai-responsesbedrock-converse-streamanthropic-messagesgoogle-generative-aigoogle-vertexgoogle-gemini-cliollama-chatcursor-agent
First-class DeepInfra, Azure OpenAI, and Amazon Bedrock examples
Azure OpenAI uses canonical OpenAI model IDs in GJC and resolves those IDs to Azure deployment names at request time. Set AZURE_OPENAI_DEPLOYMENT_NAME_MAP to avoid assuming model id equals deployment name:
providers:
azure-openai:
baseUrl: https://my-resource.openai.azure.com/openai/v1
apiKeyEnv: AZURE_OPENAI_API_KEY
api: azure-openai-responses
models:
- id: gpt-4.1
- id: o3
export AZURE_OPENAI_DEPLOYMENT_NAME_MAP='gpt-4.1=gpt-41-prod,o3=o3-reasoning-prod'
DeepInfra is available as the first-class deepinfra provider. It uses DeepInfra's OpenAI-compatible Chat Completions endpoint and reads DEEPINFRA_API_KEY when no explicit config key is provided. Set serviceTier: priority in GJC config or use the runtime service-tier controls to send DeepInfra's service_tier: "priority" request field for supported models:
providers:
deepinfra:
baseUrl: https://api.deepinfra.com/v1/openai
apiKeyEnv: DEEPINFRA_API_KEY
api: openai-completions
models:
- id: deepseek-ai/DeepSeek-V3.2
Amazon Bedrock uses the native bedrock-converse-stream transport and AWS credential chain auth. Do not put AWS access keys in models.yml; configure AWS_REGION / AWS_PROFILE or standard static AWS credential environment variables instead:
providers:
amazon-bedrock:
baseUrl: https://bedrock-runtime.us-east-1.amazonaws.com
api: bedrock-converse-stream
models:
- id: us.anthropic.claude-opus-4-6-v1
- id: anthropic.claude-3-5-sonnet-20241022-v2:0
MiniMax and GLM custom provider examples
For common MiniMax and GLM/zAI setup, prefer the provider presets so the OpenAI-compatible API, base URL, env var, model id, and compatibility flags are written together:
gjc setup provider --preset minimax
gjc setup provider --preset minimax-cn
gjc setup provider --preset glm
gjc setup provider --preset alibaba-token-plan
The same presets are available inside the TUI:
/provider add --preset minimax
/provider add --preset glm
/provider add zai
/provider add --preset alibaba-token-plan
Presets only write models.yml entries that reference documented environment variable names (MINIMAX_CODE_API_KEY, MINIMAX_CODE_CN_API_KEY, ZAI_API_KEY, or ALIBABA_TOKEN_PLAN_API_KEY); they do not store or validate real credentials. The GLM preset aliases (glm, zai, z-ai) write an OpenAI-compatible custom provider named glm-proxy and do not replace the first-class zai provider. The Alibaba Token Plan preset (aliases: alibaba, token-plan) writes an OpenAI-compatible custom provider named alibaba-token-plan with per-model API routing (qwen3.8-max-preview uses openai-responses; glm-5.2, deepseek-v4-pro, and deepseek-v4-flash-0731 use openai-completions).
Model profiles (--mpreset)
Model profiles are optional top-level profiles: entries in ~/.gjc/agent/models.yml. A profile can require provider credentials before activation and can map one or more model roles; omitted roles inherit from the active defaults.
See also: Cross-vendor role-based profiles — a curated multi-vendor
profiles:recipe and verified selector notes that build on the mechanism described here.
profiles:
team-standard:
required_providers: [openai, anthropic]
model_mapping:
default: openai/gpt-5.2
executor: anthropic/claude-sonnet-5:medium
architect: openai/o3:high
planner: openai/o3:high
critic: openai/o3:high
model_mapping keys are role names (default, executor, architect, planner, critic). Every role accepts either one provider/modelId[:effort] selector or a non-empty ordered array of selectors; the first entry is primary and later entries are fallback candidates. required_providers is the aggregate set of providers required across the profile's mapped roles.
Fallback chains
Preset model_mapping roles, top-level modelRoles, and task.agentModelOverrides all accept string | string[]. Keep one selector per line when a chain needs to be readable:
profiles:
reliable:
required_providers: [anthropic, openai]
model_mapping:
default: [anthropic/claude-sonnet-4-5, openai/gpt-4o-mini]
modelBindings:
modelRoles:
default: [anthropic/claude-sonnet-4-5, openai/gpt-4o-mini]
agentModelOverrides:
executor: [anthropic/claude-sonnet-4-5, openai/gpt-4o-mini]
Resolution-time skips for unavailable, unauthenticated, or unknown entries cost zero attempts and advance immediately. Only request-time retryable failures (such as 429, quota, authentication, or 5xx failures) consume an entry's fallback.maxAttempts total attempts (default: 3). The active default fallback remains sticky for the session; role-override fallback state is fresh for each subagent call. The active model is shown consistently in status and /model.
Managed fallback attempts buffer provisional streamed output until an attempt is accepted, so output can appear later than it does for a one-model stream. Current Cursor-agent transports are fail-closed unavailable in retryable fallback chains: resolution rejects them with Cursor model <selector> requires provider-side tool execution and cannot be used in a retryable fallback chain because they do not provide a client-side tool-call mode.
Cancellation discards provisional output and emits exactly one cancelled agent_end; RPC, ACP, and the TUI therefore settle once. On load, the source-aware one-shot migration reads legacy retry.fallbackChains, prepends the effective role chain, and writes the ordered, deduplicated result to the corresponding role array; the legacy key is then ignored.
Built-in profiles are grouped by provider mix and tier:
codex-{eco,medium,pro}— GPT-5.6 Sol/Terra/Luna role mixes tuned by tier and reasoning effort;lunamaxxing— OpenAI Codex Luna-only profile with maximum reasoning on delegated rolesopencodego— single OpenCode Go preset (Kimi default, DeepSeek executor/architect, Qwen planner, MiMo critic)claude-opus— Anthropic OAuth preset centered onclaude-opus-5- Single-provider tiers:
glm-{eco,medium,pro},kimi-coding-plan-{eco,medium,pro},mimo-{eco,medium,pro},grok-{eco,medium,pro},cursor-{eco,medium,pro},minimax-{eco,medium,pro} - Alibaba Token Plan:
alibaba-token-plan-balancedpreserves the established Qwen/DeepSeek V4 Pro/GLM mix;alibaba-token-plan-proraises execution and independent criticism with DeepSeek V4 Flash 0731 max and GLM xhigh;alibaba-token-plan-qwenmaxxingstays Qwen-only - Combos:
opus-codex,codex-opencodego, andfable-opus-codex
The eco, medium, and pro Codex profile mappings are current product judgments: Eco assigns Terra low/Luna low/Luna high/Terra xhigh/Terra high to default/executor/planner/critic/architect; Medium assigns Sol low/Terra low/Terra high/Sol xhigh/Sol high; Pro assigns Sol medium/Terra medium/Sol high/Sol max/Sol xhigh; and LunaMaxxing assigns Luna medium/Luna xhigh/Luna max/Luna max/Luna max. opus-codex retains the Medium Codex executor, critic, and architect roles but uses anthropic/claude-sonnet-5 for planner; codex-opencodego retains the Medium Codex default and architect roles; and fable-opus-codex uses the Pro Codex executor and architect roles with anthropic/claude-opus-5:medium for planner. The descriptive repeated local exact-edit evidence informs only selected executor-style TypeScript tasks; it does not evaluate or prove default, planner, architect, or critic performance. See GPT-5.6 Codex preset benchmark. The Alibaba Pro role evidence and its limits are recorded separately in Alibaba Token Plan Pro profile benchmark. Effort suffixes are clamped to each model's supported thinking range at preview and activation time. Single-provider tiers pin each provider's current flagship (zai/glm-5.2, kimi-code/kimi-k2.7-code, xiaomi/mimo-v2.5-pro, xai/grok-4.3, cursor/composer-1.5, minimax-code/minimax-m3). User-defined profiles override built-ins by exact profile name.
Use gjc --mpreset <name> to activate a profile for the current session only. Activation hard-blocks when any provider listed in required_providers lacks credentials. Add --default to persist the selected profile as modelProfile.default in config.yml, so it applies at startup:
gjc --mpreset codex-medium
gjc --mpreset opencodego --default
The /model command opens to a preset landing view: presets are grouped by provider with live auth marks (✓/✗), highlighting a group expands its tiers, and selecting a tier shows the full role→model preview before applying for the session or as default. Typing jumps straight to model search, and Browse all models opens the classic tabbed model selector. In /login, Add custom provider is the first option for configuring credentials needed by custom or profile-required providers; after a successful provider login, the matching preset is recommended automatically.
MiniMax's OpenAI-compatible endpoint rejects multiple system messages and emits thinking in reasoning_content, so pin the public-safe compatibility fields when hand-authoring a custom provider:
providers:
minimax-custom:
baseUrl: https://api.minimax.io/v1
apiKeyEnv: MINIMAX_API_KEY
api: openai-completions
compat:
supportsStore: false
supportsDeveloperRole: false
supportsReasoningEffort: false
reasoningContentField: reasoning_content
models:
- id: MiniMax-M2.5
GLM via z.ai is available as the first-class zai provider. For a private GLM-compatible proxy, keep secrets in an env var and disable OpenAI-only request fields as needed:
providers:
glm-proxy:
baseUrl: https://api.z.ai/api/paas/v4
apiKeyEnv: ZAI_API_KEY
api: openai-completions
compat:
supportsDeveloperRole: false
supportsReasoningEffort: false
models:
- id: glm-4.6
Allowed auth/discovery values
auth:apiKey(default),none, oroauth; formodels.ymlcustom models,oauthis accepted by schema but does not waive theapiKeyrequirementmodels.ymlis strict: unknown provider/model keys fail validation before provider dispatch, so stale keys such asrequestTransformorwireModelIdonly work where this document lists them.discovery.type:ollama,llama.cpp,lm-studio, oropenai-models-listcacheRetention:none,short, orlong; request-time options win over model/modelOverride values, then provider values, thenGJC_CACHE_RETENTION, then the runtime default. The runtime default isshortfor most providers, but the Anthropic provider defaults tolong(ttl: "1h") because the ~5m default is too fragile for long-running subagent workflows. The 1h marker is only emitted on the canonical Anthropic API (api.anthropic.com) for models advertisingsupportsLongCacheRetention; proxies, gateways, and incapable models fall back to the default ephemeral (~5m) breakpoint. For OpenAI Responses, this controlsprompt_cache_retentiononly; it does not disableprompt_cache_keywhen a stable session id exists.
OpenAI-compatible proxy configuration
OpenAI-compatible proxy providers should use schema-supported provider keys first:
providers:
proxy-provider:
baseUrl: https://api.proxy.example/v1
apiKeyEnv: PROXY_API_KEY
api: openai-completions
auth: apiKey
headers:
User-Agent: curl/8.7.1
models:
- id: local-gpt
name: Local GPT
reasoning: true
input: [text]
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
contextWindow: 400000
maxTokens: 128000
Use provider-level headers for proxy-required headers. Keep the provider api set to openai-completions when the proxy exposes Chat Completions-compatible /v1/chat/completions semantics. auth: apiKey sends the resolved token as bearer auth; use auth: none only for trusted local/no-auth endpoints.
auth selects the transport scheme only; it never supplies a credential. A provider that declares models: must therefore also declare where its key comes from, and models.yml validation rejects the config before model discovery otherwise:
| Intent | Required keys |
|---|---|
| Authenticated proxy (recommended) | auth: apiKey (default) + apiKeyEnv: MY_TOKEN |
| Authenticated proxy, key inline | auth: apiKey (default) + apiKey: sk-… (less safe; stored in plaintext) |
| Genuinely unauthenticated endpoint | auth: none, no key |
Omitting both apiKey and apiKeyEnv while leaving auth at its apiKey default fails with Provider <name>: custom models need a credential source, but none is configured. — the fix is to add one of the rows above, not to change api or baseUrl.
input is the model modality list GJC uses to decide whether image content is forwarded. When a custom model omits input, GJC defaults to [text] (unless a bundled model with the same id contributes a reference). Vision-capable upstream models therefore need an explicit input: [text, image]; otherwise read/tool images are stripped before the request and replaced with [image omitted: model does not support vision], even if the remote model can see images.
providers:
ali:
baseUrl: https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1
apiKeyEnv: ALI_API_KEY
api: openai-completions
auth: apiKey
models:
# id-only → text-only; images will be omitted
- id: some-text-model
# vision-capable hosted model must declare image input
- id: qwen3.8-max-preview
name: Qwen3.8 Max Preview
reasoning: true
input: [text, image]
requestTransform and wireModelId remain supported for request-body shaping, but they are not needed for ordinary OpenAI-compatible proxies whose local model id is already the upstream wire id. Unknown config keys fail validation before a provider request is sent.
When request shaping is needed:
requestTransform.profile: openai-proxystrips OpenAI SDK/Stainless telemetry and beta headers at final fetch time and sets a generic GJC user agent.stripHeadersreplaces the preset strip list when provided.setHeadersis applied after stripping; usenullto remove a header.extraBodyis shallow-merged into the JSON request body after provider compatibility fields; core transport keys such asmodel,messages/input,stream,tools, andtool_choiceare protected and ignored.- Model-level
requestTransformoverrides provider-level fields and shallow-mergessetHeaders/extraBody. wireModelIdchanges only the upstream request body model id; local selection still usesprovider/id.
Layofflabs-style proxy example
providers:
layofflabs:
baseUrl: https://api.layofflabs.com/v1
apiKeyEnv: OPENAI_API_KEY
api: openai-completions
auth: apiKey
headers:
User-Agent: curl/8.7.1
models:
- id: gpt-5.5
name: GPT 5.5 via Layofflabs
reasoning: true
thinking:
minLevel: low
maxLevel: xhigh
mode: effort
defaultLevel: high
levels: [low, medium, high, xhigh]
input: [text]
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
contextWindow: 400000
maxTokens: 128000
modelBindings:
modelRoles:
default: layofflabs/gpt-5.5:high
agentModelOverrides:
executor: layofflabs/gpt-5.5:high
Validation rules (current)
Full custom provider (models is non-empty)
Required:
baseUrl- A credential source:
apiKeyEnvorapiKey.authselects the scheme, not the credential, soauth: apiKey(the default) still needs one of them. Exempt:auth: none, andapi: bedrock-converse-stream, which resolves AWS credentials from its own chain. apiat provider level or each model
Override-only provider (models missing or empty)
Must define at least one of:
baseUrlheaderscompatrequestTransformdisableStrictToolsmodelOverridesdiscovery
Discovery
discoveryrequires provider-levelapi.
Model value checks
idrequiredcontextWindowandmaxTokensmust be positive if provided- unknown provider, model, override, and request-transform keys fail schema validation; remove stale keys instead of relying on them being ignored.
Merge and override order
ModelRegistry pipeline (on refresh):
- Load built-in providers/models from
@gajae-code/ai. - Load
models.ymlcustom config. - Apply provider overrides (
baseUrl,headers,requestTransform,disableStrictTools,cacheRetention) to built-in models. - Apply
modelOverrides(per provider + model id). - Merge custom
models:- same
provider + idreplaces existing - otherwise append
- same
- Load cached/runtime-discovered models (Ollama, llama.cpp, LM Studio, plus built-in provider managers), then re-apply model overrides.
Provider-model cache and static fingerprint
Cached per-provider model lists are persisted in the model-cache SQLite
database (schema v3) with a static_fingerprint column that hashes the
static catalog slice merged into the row. When resolveProviderModels
skips the network fetch and the fingerprint of the in-memory static
catalog matches the cached one, the cached rows are returned verbatim —
the static + dynamic merge is bypassed entirely. The fingerprint is
memoized per process via a WeakMap keyed by the static-models array
reference, so repeated cold-start calls do not re-hash.
Canonical model equivalence and coalescing
The registry keeps every concrete provider model and then builds a canonical layer above them.
Canonical ids are official upstream ids only, for example:
anthropic-model-opus-4-6anthropic-model-haiku-4-5gpt-5.3-openai-code
models.yml equivalence config
Example:
providers:
zenmux:
baseUrl: https://api.zenmux.example/v1
apiKey: ZENMUX_API_KEY
api: openai-codex-responses
models:
- id: openai-code
name: Zenmux OpenAI code
reasoning: true
input: [text]
cost:
input: 0
output: 0
cacheRead: 0
cacheWrite: 0
contextWindow: 200000
maxTokens: 32768
equivalence:
overrides:
zenmux/openai-code: gpt-5.3-openai-code
p-openai-code/openai-code: gpt-5.3-openai-code
exclude:
- demo/openai-code-preview
Build order for canonical grouping:
- exact user override from
equivalence.overrides - bundled official-id matches from built-in model metadata
- conservative heuristic normalization for gateway/provider variants
- fallback to the concrete model's own id
Current heuristics are intentionally narrow:
- embedded upstream prefixes can be stripped when present, for example
anthropic/...oropenai/... - dotted and dashed version variants can normalize only when they map to an existing official id, for example
4.6 -> 4-6 - ambiguous families or versions are not merged without a bundled match or explicit override
Canonical resolution behavior
When multiple concrete variants share a canonical id, resolution uses:
- availability and auth
config.ymlmodelProviderOrder- the lowest combined
cost.input + cost.cacheRead - existing registry/provider order if the earlier ranks tie
Disabled or unauthenticated providers are skipped. A session that resolves a canonical selector keeps its concrete variant across discovery refreshes; it changes only after an explicit concrete selection or when that variant is no longer available.
Session state and transcripts continue to record the concrete provider/model that actually executed the turn.
Provider defaults vs per-model overrides:
- Provider
headersare baseline. - Model
headersoverride provider header keys. modelOverridescan override model metadata (name,reasoning,input,cost,contextWindow,maxTokens,headers,compat,contextPromotionTarget).compatis deep-merged for nested routing blocks (openRouterRouting,vercelGatewayRouting,extraBody).
Runtime discovery integration
Implicit Ollama discovery
If ollama is not explicitly configured, registry adds an implicit discoverable provider:
- provider:
ollama - api:
openai-responses - base URL:
OLLAMA_BASE_URLorhttp://127.0.0.1:11434 - auth mode: keyless (
auth: nonebehavior)
Runtime discovery calls Ollama endpoints and normalizes discovered OpenAI-compatible models to openai-responses.
Implicit llama.cpp discovery
If llama.cpp is not explicitly configured, registry adds an implicit discoverable provider:
- provider:
llama.cpp - api:
openai-responses - base URL:
LLAMA_CPP_BASE_URLorhttp://127.0.0.1:8080 - auth mode: keyless (
auth: nonebehavior)
Runtime discovery calls llama.cpp model endpoints and synthesizes model entries with local defaults.
Implicit LM Studio discovery
If lm-studio is not explicitly configured, registry adds an implicit discoverable provider:
- provider:
lm-studio - api:
openai-completions - base URL:
LM_STUDIO_BASE_URLorhttp://127.0.0.1:1234/v1 - auth mode: keyless (
auth: nonebehavior)
Runtime discovery fetches models (GET /models) and synthesizes model entries with local defaults.
Explicit provider discovery
You can configure discovery yourself:
providers:
ollama:
baseUrl: http://127.0.0.1:11434
api: openai-responses
auth: none
discovery:
type: ollama
llama.cpp:
baseUrl: http://127.0.0.1:8080
api: openai-responses
auth: none
discovery:
type: llama.cpp
Extension provider registration
Extensions can register providers at runtime (pi.registerProvider(...)), including:
- model replacement/append for a provider
- custom stream handler registration for new API IDs
- custom OAuth provider registration
Auth and API key resolution order
When requesting a key for a provider, effective order is:
- Runtime override (CLI
--api-key) - Stored API key credential in
agent.db - Stored OAuth credential in
agent.db(with refresh) - Environment variable mapping (
OPENAI_API_KEY,ANTHROPIC_API_KEY, etc.) - ModelRegistry fallback resolver (provider
apiKeyfrommodels.yml, env-name-or-literal semantics)
models.yml apiKey behavior:
- Value is first treated as an environment variable name.
- If no env var exists, the literal string is used as the token.
If authHeader: true and provider apiKey is set, models get:
Authorization: Bearer <resolved-key>header injected.
Keyless providers:
- Providers marked
auth: noneare treated as available without credentials. getApiKey*returnskNoAuthfor them.
Broker mode
When GJC_AUTH_BROKER_URL (or auth.broker.url) is set, the local SQLite credential store is replaced by RemoteAuthCredentialStore. Layers 2 and 3 above (stored API key / OAuth in agent.db) are served from a broker-supplied snapshot whose refresh tokens are redacted; expiry triggers POST /v1/credential/:id/refresh on the broker rather than a local refresh.
AuthStorage.setConfigApiKey lets a models.yml apiKey win over a broker-resolved OAuth token without overriding a runtime --api-key. See auth-broker-gateway.md for the full broker / gateway design and env surface (GJC_AUTH_BROKER_URL, GJC_AUTH_BROKER_TOKEN, auth.broker.url, auth.broker.token).
Model availability vs all models
getAll()returns the loaded model registry (built-in + merged custom + discovered).getAvailable()filters to models that are keyless or have resolvable auth.
So a model can exist in registry but not be selectable until auth is available.
Runtime model resolution
CLI and pattern parsing
model-resolver.ts supports:
- exact
provider/modelId - exact canonical model id
- exact model id (provider inferred)
- fuzzy/substring matching
- glob scope patterns in
--models(e.g.openai/*,*sonnet*) - optional
:thinkingLevelsuffix (off|minimal|low|medium|high|xhigh)
--provider is legacy; --model is preferred.
Resolution precedence for exact selectors:
- exact
provider/modelIdbypasses coalescing - exact canonical id resolves through the canonical index
- exact bare concrete id still works
- fuzzy and glob matching run after the exact paths
Thinking suffixes are split once from the final : only after the complete selector does not resolve. This preserves concrete OpenRouter route IDs such as openrouter/z-ai/glm-4.7:nitro; :high can follow that route suffix. Multiple suffixes are not recursively consumed. A complete provider/modelId selector is exact-only: it never falls back to fuzzy, substring, glob, or another provider when that concrete selector is absent. Exact-case provider/model entries resolve deterministically for custom replacement semantics; a case-insensitive selector that remains ambiguous does not guess.
Initial model selection priority
findInitialModel(...) uses this order:
- explicit CLI provider+model
- first scoped model (if not resuming)
- saved default provider/model
- known provider defaults (e.g. OpenAI/Anthropic/etc.) among available models
- first available model
Role aliases and settings
Supported model roles:
defaultplus the agent assignment targetsexecutor,architect,planner,critic
Role aliases like pi/default expand through settings.modelRoles. Each role value can also append a thinking selector such as :minimal, :low, :medium, or :high.
If a role points at another role, the target model still inherits normally and any explicit suffix on the referring role wins for that role-specific use.
Related settings:
modelRoles(record)enabledModels(scoped pattern list)modelProviderOrder(global canonical-provider precedence)providers.kimiApiFormat(openaioranthropicrequest format)providers.openaiWebsockets(auto|off|onwebsocket preference for OpenAI code provider transport)
modelRoles may store either:
provider/modelIdto pin a concrete provider variant- a canonical id such as
gpt-5.3-openai-codeto allow provider coalescing
For enabledModels and CLI --models:
- exact canonical ids expand to all concrete variants in that canonical group
- explicit
provider/modelIdentries stay exact - globs and fuzzy matches still operate on concrete models
Global enabledModels and disabledProviders entries may also be scoped to a path prefix:
enabledModels:
- anthropic-model-sonnet-4-5
- path: ~/work
models:
- anthropic/anthropic-model-opus-4-5
disabledProviders:
- ollama
- path: ~/private
providers:
- anthropic
String entries apply everywhere. Scoped entries apply when the current working directory is the configured path or one of its subdirectories. Use path, paths, pathPrefix, or pathPrefixes; use models for enabledModels, providers for disabledProviders, or values for either.
/model and --list-models
Both surfaces keep provider-prefixed models visible and selectable.
They now also expose canonical/coalesced models:
/modelincludes a canonical view alongside provider tabs--list-modelsprints a canonical section plus the concrete provider rows
Selecting a canonical entry stores the canonical selector. Selecting a provider row stores the explicit provider/modelId.
Context promotion (model-level fallback chains)
Context promotion is an overflow recovery mechanism for small-context variants (for example *-spark) that automatically promotes to a larger-context sibling when the API rejects a request with a context length error. It is off by default (contextPromotion.enabled is false); opt in to enable it.
Trigger and order
When a turn fails with a context overflow error (e.g. context_length_exceeded), AgentSession attempts promotion before falling back to compaction:
- If
contextPromotion.enabledis true, resolve a promotion target (see below). - If a target is found, switch to it and retry the request — no compaction needed.
- If no target is available, fall through to auto-compaction on the current model.
Target selection
Selection is model-driven, not role-driven:
currentModel.contextPromotionTarget(if configured)- smallest larger-context model on the same provider + API
Candidates are ignored unless credentials resolve (ModelRegistry.getApiKey(...)).
OpenAI code provider websocket handoff
If switching from/to openai-codex-responses, session provider state key openai-codex-responses is closed before model switch. This drops websocket transport state so the next turn starts clean on the promoted model.
Persistence behavior
Promotion uses temporary switching (setModelTemporary):
- recorded as a temporary
model_changein session history - does not rewrite saved role mapping
Configuring explicit fallback chains
Configure fallback directly in model metadata via contextPromotionTarget.
contextPromotionTarget accepts either:
provider/model-id(explicit)model-id(resolved within current provider)
Example (models.yml) for Spark -> non-Spark on the same provider:
providers:
openai-code:
modelOverrides:
gpt-5.3-openai-code-spark:
contextPromotionTarget: openai-code/gpt-5.3-openai-code
The built-in model generator also assigns this automatically for *-spark models when a same-provider base model exists.
Compatibility and routing fields
The compat block on a provider or model overrides the URL-based auto-detection in packages/ai/src/providers/openai-completions-compat.ts. It is validated by OpenAICompatSchema in packages/coding-agent/src/config/model-registry.ts and consumed by every openai-completions transport (packages/ai/src/providers/openai-completions.ts). The canonical type is OpenAICompat in packages/ai/src/types.ts.
models.yml accepts the following keys (all optional; unset falls back to URL detection):
Request shaping:
supportsStore— emitstore: falseon requests. Default: auto (off for non-standard endpoints).supportsDeveloperRole— use thedevelopersystem role for reasoning models instead ofsystem. Default: auto.sendSessionHeaders— forward the agent session id assession_idandx-session-idrequest headers so OpenAI-compatible relays/proxies can do session-affinity routing and reuse a server-side prompt cache. Default:false. Caller-setheaders/requestTransformvalues are never overwritten.supportsUsageInStreaming— sendstream_options: { include_usage: true }to receive token usage on streaming responses. Default:true.maxTokensField—"max_completion_tokens"or"max_tokens". Default: auto.supportsToolChoice— emit thetool_choiceparameter when the caller forces a specific tool. Default:true. Setfalsefor endpoints that 400 ontool_choice(e.g. DeepSeek when reasoning is on).disableReasoningOnForcedToolChoice— dropreasoning_effort/ OpenRouterreasoningwhenevertool_choiceforces a call. Default: auto (Kimi/Anthropic-fronted endpoints).extraBody— extra top-level fields merged into every request body (gateway hints, controller selectors, etc.).
Reasoning / thinking:
supportsReasoningEffort— acceptreasoning_effort. Default: auto (off for Grok and zAI).reasoningEffortMap— partial map from internal effort levels (minimal|low|medium|high|xhigh) to provider-specific strings (e.g. DeepSeek mapsxhigh -> "max").thinkingFormat— request shape for thinking:"openai"(reasoning_effort),"openrouter"(reasoning: { effort }),"zai"(thinking: { type: "enabled" }),"qwen"(top-levelenable_thinking), or"qwen-chat-template"(chat_template_kwargs.enable_thinking). Default:"openai".reasoningContentField— assistant field carrying chain-of-thought:"reasoning_content","reasoning", or"reasoning_text". Default: auto.requiresReasoningContentForToolCalls— assistant tool-call turns must round-trip the reasoning field (DeepSeek-R1, Kimi, OpenRouter when reasoning is on). Default:false.requiresAssistantContentForToolCalls— assistant tool-call turns must include non-empty text content (Kimi). Default:false.
Tool / message normalization:
requiresToolResultName— tool-result messages need anamefield (Mistral). Default: auto.requiresAssistantAfterToolResult— a user message after a tool result needs an assistant turn in between. Default: auto.requiresThinkingAsText— convert thinking blocks to text wrapped in<thinking>delimiters (Mistral). Default: auto.requiresMistralToolIds— normalize tool-call ids to exactly 9 alphanumeric chars. Default: auto.supportsStrictMode— accept the per-toolstrictfield on tool schemas. Default: conservative auto-detect per provider/baseUrl.toolStrictMode—"all_strict"forces strict on every tool,"none"forces it off; unset keeps the existing per-tool mixed behavior.
Gateway routing (only applied when baseUrl matches the gateway):
openRouterRouting.only/openRouterRouting.order— provider routing onopenrouter.ai(see https://openrouter.ai/docs/provider-routing).vercelGatewayRouting.only/vercelGatewayRouting.order— provider routing onai-gateway.vercel.sh(see https://vercel.com/docs/ai-gateway/models-and-providers/provider-options).
Provider-level compat is the baseline; per-model compat is deep-merged on top, with openRouterRouting, vercelGatewayRouting, and extraBody merged as nested objects.
Anthropic compatibility (anthropic-messages)
For anthropic-messages models the runtime uses a separate AnthropicCompat shape (packages/ai/src/types.ts). The models.yml schema currently exposes only the strict-tools opt-out as a top-level provider field (see below); the remaining Anthropic-side knobs (disableAdaptiveThinking, supportsEagerToolInputStreaming, supportsLongCacheRetention) are set by built-in catalog metadata and are not user-configurable from models.yml.
Strict tool schemas (disableStrictTools)
Anthropic's API supports a strict field on tool definitions that forces the model to always follow the provided schema exactly. This is enabled by default for all anthropic-messages providers because it guarantees schema conformance in agentic systems.
Third-party providers that front the Anthropic API (AWS Bedrock, Azure, self-hosted proxies) do not always implement this field and will reject requests that include it. Set disableStrictTools: true at the provider level to opt out:
providers:
bedrock-anthropic:
baseUrl: https://bedrock-runtime.us-east-1.amazonaws.com/anthropic
apiKey: AWS_BEARER_TOKEN
api: anthropic-messages
disableStrictTools: true
models:
- id: anthropic-model-sonnet-4-20250514
name: Anthropic model Sonnet 4 (Bedrock)
input: [text, image]
contextWindow: 200000
maxTokens: 16384
cost:
input: 3.00
output: 15.00
cacheRead: 0.30
cacheWrite: 3.75
disableStrictTools is a provider-level flag that applies to all models in the provider.
Tool schemas going on the wire are normalized by the unified flow in
packages/ai/src/utils/schema/normalize.ts (Google/CCA/MCP dispatchers
plus the OpenAI strict-mode sanitize+enforce pipeline). See
ai-schema-normalize.md for the strict-mode
edge cases (local $ref inlining, single-item allOf collapse,
anyOf-wrapper description hoist, enum/const primitive-type inference)
and the per-provider dispatcher mapping.
Practical examples
Local OpenAI-compatible endpoint (no auth)
providers:
local-openai:
baseUrl: http://127.0.0.1:8000/v1
auth: none
api: openai-completions
models:
- id: Qwen/Qwen2.5-Coder-32B-Instruct
name: Qwen 2.5 Coder 32B (local)
Hosted proxy with env-based key
providers:
anthropic-proxy:
baseUrl: https://proxy.example.com/anthropic
apiKey: ANTHROPIC_PROXY_API_KEY
api: anthropic-messages
authHeader: true
disableStrictTools: true # if the proxy doesn't support strict tool schemas
models:
- id: anthropic-model-sonnet-4-20250514
name: Anthropic model Sonnet 4 (Proxy)
reasoning: true
input: [text, image]
Override built-in provider route + model metadata
providers:
openrouter:
baseUrl: https://my-proxy.example.com/v1
headers:
X-Team: platform
modelOverrides:
anthropic/anthropic-model-sonnet-4:
name: Sonnet 4 (Corp)
compat:
openRouterRouting:
only: [anthropic]
Legacy consumer caveat
Most model configuration now flows through models.yml via ModelRegistry. Explicit .json / .jsonc paths remain supported only when passed programmatically to ModelRegistry; the default user config is ~/.gjc/agent/models.yml.
Failure mode
If models.yml fails schema or validation checks:
- registry keeps operating with built-in models
- error is exposed via
ModelRegistry.getError()and surfaced in UI/notifications