Description
Auto-discover models from custom openai-compat providers via /v1/models
Summary
Allow custom providers (let's call them openai-compat) in crush.json to omit the models array and have Crush populate it automatically by querying the provider's standard GET /v1/models endpoint at load time.
Motivation
Today, every custom provider must enumerate its models by hand:
If the array is empty, internal/config/load.go:374-379 silently deletes the provider:
if len(providerConfig.Models) == 0 {
slog.Warn("Skipping custom provider because the provider has no models", "provider", id)
c.Providers.Del(id)
continue
}
Catwalk solves this for built-in providers, but custom / corporate / proxy gateways (LiteLLM, vLLM, llama.cpp, LM Studio, OpenRouter mirrors, internal company gateways, etc.) are on their own. Users end up writing wrapper scripts that hit /v1/models and rewrite crush.json — exactly the kind of glue Crush should subsume.
Most OpenAI-compatible servers already implement GET /v1/models per the OpenAI spec, returning at minimum a list of {id, object, created, owned_by} entries. That is enough to seed a provider's model list without any per-model hand configuration.
Proposed behavior
- Trigger: When a custom provider has
"models": [] (or the field is omitted) and type is openai-compat / openai / anthropic, Crush calls GET {base_url}/models using the configured api_key and extra_headers during config load.
- Mapping: Each returned
id becomes a model entry. Unknown fields (context_window, default_max_tokens, cost_per_1m_*, can_reason, supports_attachments) get sensible defaults — same defaults Crush already uses when those keys are omitted in the config today.
- Overrides: A new optional
model_defaults object on the provider lets users tune the per-family fallbacks without listing every model. A new optional model_overrides map keyed by model id lets users override individual models without re-enumerating the rest.
- Opt-out: A provider-level
"discover_models": false (default true when models is empty) preserves the current behavior of failing closed — useful if a gateway's /v1/models is slow or unreliable and the user prefers to enumerate models by hand.
- No on-disk cache.
/v1/models is effectively free on every provider we've checked (one GET per process launch, sub-second). Skipping the cache avoids stale-data bugs and TTL knobs; users who need offline startup can enumerate models explicitly, just like today.
Example config
No models array, no manual sync — Crush asks the gateway what it offers.
Why not "just maintain a script"
That is the current workaround, and it's what I'm doing now. It works, but:
- Every Crush user behind a corporate OpenAI-compatible gateway re-implements the same script.
- The script has to know Crush's config schema and risks corrupting
crush.json on partial writes.
- It can't react when the gateway adds a model mid-session.
- It defeats the "drop in a
base_url, go" UX Crush already offers for built-in providers.
Related issues
This issue generalizes #2740 to any OpenAI-compatible custom provider.
Acceptance criteria
Willing to contribute
Happy to take a stab at a PR if maintainers agree on the shape above.
Description
Auto-discover models from custom
openai-compatproviders via/v1/modelsSummary
Allow custom providers (let's call them
openai-compat) incrush.jsonto omit themodelsarray and have Crush populate it automatically by querying the provider's standardGET /v1/modelsendpoint at load time.Motivation
Today, every custom provider must enumerate its models by hand:
{ "providers": { "gateway": { "type": "openai-compat", "base_url": "https://example.com/v1", "api_key": "$GATEWAY_API_KEY", "models": [ { "id": "model-a", "context_window": 200000, ... }, { "id": "model-b", "context_window": 128000, ... }, // …dozens more entries that change every few weeks ] } } }If the array is empty,
internal/config/load.go:374-379silently deletes the provider:Catwalk solves this for built-in providers, but custom / corporate / proxy gateways (LiteLLM, vLLM, llama.cpp, LM Studio, OpenRouter mirrors, internal company gateways, etc.) are on their own. Users end up writing wrapper scripts that hit
/v1/modelsand rewritecrush.json— exactly the kind of glue Crush should subsume.Most OpenAI-compatible servers already implement
GET /v1/modelsper the OpenAI spec, returning at minimum a list of{id, object, created, owned_by}entries. That is enough to seed a provider's model list without any per-model hand configuration.Proposed behavior
"models": [](or the field is omitted) andtypeisopenai-compat/openai/anthropic, Crush callsGET {base_url}/modelsusing the configuredapi_keyandextra_headersduring config load.idbecomes a model entry. Unknown fields (context_window,default_max_tokens,cost_per_1m_*,can_reason,supports_attachments) get sensible defaults — same defaults Crush already uses when those keys are omitted in the config today.model_defaultsobject on the provider lets users tune the per-family fallbacks without listing every model. A new optionalmodel_overridesmap keyed by model id lets users override individual models without re-enumerating the rest."discover_models": false(defaulttruewhenmodelsis empty) preserves the current behavior of failing closed — useful if a gateway's/v1/modelsis slow or unreliable and the user prefers to enumerate models by hand./v1/modelsis effectively free on every provider we've checked (one GET per process launch, sub-second). Skipping the cache avoids stale-data bugs and TTL knobs; users who need offline startup can enumeratemodelsexplicitly, just like today.Example config
{ "providers": { "gateway": { "type": "openai-compat", "base_url": "https://example.com/v1", "api_key": "$GATEWAY_API_KEY", "extra_headers": { "X-Custom-Header": "$GATEWAY_API_KEY" }, "model_defaults": { "context_window": 128000, "default_max_tokens": 4096 }, "model_overrides": { "model-a": { "context_window": 200000, "supports_attachments": true }, "model-b": { "context_window": 400000, "can_reason": true } } } } }No
modelsarray, no manual sync — Crush asks the gateway what it offers.Why not "just maintain a script"
That is the current workaround, and it's what I'm doing now. It works, but:
crush.jsonon partial writes.base_url, go" UX Crush already offers for built-in providers.Related issues
fantasylibrary)update-providers <url>to fetch from/v1/models-style endpointsThis issue generalizes #2740 to any OpenAI-compatible custom provider.
Acceptance criteria
modelsand reachable/v1/modelsloads successfully with the discovered list.model_defaultsand per-idmodel_overrides./v1/modelsemits the existing "no models configured" warning and skips the provider as today (fail-closed, no silent partial state).discover_models: falsepreserves current behavior exactly.Willing to contribute
Happy to take a stab at a PR if maintainers agree on the shape above.