This plugin discovers models for OpenAI-compatible providers and merges them into the active OpenCode config at startup.
Use provider.<name>.options.modelsDiscovery for provider-specific behavior. This is the only supported configuration boundary in 1.0.0.
OpenCode's own provider config still controls provider identity, npm package, baseURL, credentials, and provider availability. This plugin controls model discovery for providers that OpenCode has made available.
Each provider can configure discovery behavior through provider.<name>.options.modelsDiscovery:
{
"plugin": ["opencode-models-discovery"],
"provider": {
"lmstudio": {
"npm": "@ai-sdk/openai-compatible",
"name": "LM Studio",
"options": {
"baseURL": "http://127.0.0.1:1234/v1",
"modelsDiscovery": {
"enabled": true,
"models": {
"includeBy": [
{ "field": "id", "match": "^llama" }
]
},
"smartModelName": true
}
}
}
}
}| Option | Type | Description |
|---|---|---|
provider.<name>.options.modelsDiscovery.enabled |
boolean |
Force enable or disable discovery for a single provider |
provider.<name>.options.modelsDiscovery.endpoint |
string |
Provider-specific models endpoint path. Defaults to /v1/models |
provider.<name>.options.modelsDiscovery.modelInfoEndpoint |
string |
Override a format-specific metadata endpoint. Defaults to /v1/model/info for "litellm" and /api/v1/models for "lmstudio" |
provider.<name>.options.modelsDiscovery.modelInfoFormat |
string |
Model info response format. Currently supports "bifrost", "litellm", "models.dev", "vllm", and "lmstudio" |
provider.<name>.options.modelsDiscovery.filterNonChat |
boolean |
When model info is available, skip models whose model_info.mode is not chat. Defaults to true |
provider.<name>.options.modelsDiscovery.models.includeRegex |
string[] |
Shortcut regex allow-list for discovered model ids only |
provider.<name>.options.modelsDiscovery.models.excludeRegex |
string[] |
Shortcut regex deny-list for discovered model ids only |
provider.<name>.options.modelsDiscovery.models.includeBy |
{ field: string, equals: string | number | boolean | null }[] or { field: string, match: string }[] |
Allow-list for top-level raw provider model fields |
provider.<name>.options.modelsDiscovery.models.excludeBy |
{ field: string, equals: string | number | boolean | null }[] or { field: string, match: string }[] |
Deny-list for top-level raw provider model fields |
provider.<name>.options.modelsDiscovery.smartModelName |
boolean |
Use human-friendly display names instead of raw discovered model ids |
provider.<name>.options.modelsDiscovery.cache.enabled |
boolean |
Opt in to provider-scoped cached filtered and enriched model configurations; defaults to false |
provider.<name>.options.modelsDiscovery.cache.ttlSeconds |
non-negative finite number |
Cache lifetime in seconds; defaults to 86400 |
Recommended approach:
- Keep the plugin entry simple:
"plugin": ["opencode-models-discovery"]. - Put endpoint, enablement, and model filtering rules on each provider.
- Use
modelsDiscovery.endpointwhenever a provider does not follow the usual/v1/modelsconvention. - Use OpenCode
/connectcredentials orprovider.<name>.options.apiKeyfor secrets; do not duplicate API keys unless needed.
If provider.<name>.options.modelsDiscovery.endpoint is omitted, the plugin uses /v1/models.
Caching is disabled unless modelsDiscovery.cache.enabled is explicitly true. When enabled, a provider's latest successful filtered and metadata-enriched discovered model configurations are cached in plugin-owned XDG data at:
${XDG_DATA_HOME}/opencode-models-discovery/providers/provider-<encoded-provider-id>.json
When XDG_DATA_HOME is unset, the plugin uses the xdg-basedir data-directory fallback. Provider ids are encoded before being used in file names. Cache files are never written to OpenCode or Mimocode auth locations, including ${xdgData}/opencode/auth.json and ${xdgData}/mimocode/auth.json.
{
"modelsDiscovery": {
"cache": {
"enabled": true,
"ttlSeconds": 86400
}
}
}A fresh cached model set is injected without requesting the provider models endpoint, resolving credentials, or repeating metadata enrichment. Once expired, the plugin refreshes it live. If the refresh fails, the expired cached models are not injected; only explicit provider.<name>.models entries remain active.
Each cache file contains a version, provider id, normalized base URL, endpoint, fetch time, and the final discovered model configurations, including enriched OpenCode capability metadata. Models rejected by filters, categorization, or metadata enrichment eligibility are not cached. It never contains API keys, authorization headers, credentials, or raw model-info endpoint responses. A cache file with another provider identity or an unsupported schema version is treated as a cache miss.
Saved per-model overrides are separate from plugin-generated cached model configurations and are managed through /models-discovery:config. Overrides merge recursively for objects, replace arrays, cannot change id, and apply only when the model is present in the current valid discovered model set. Explicit provider.<name>.models configuration is applied last and remains higher priority. An override for a model absent from a refreshed model set stays saved but inactive until that model returns.
See Persisted Model Discovery Cache for the complete cache schema, lifecycle, override behavior, and security boundary.
provider.<name>.options.modelsDiscovery.enabled = trueforces discovery for that provider.provider.<name>.options.modelsDiscovery.enabled = falsedisables discovery for that provider.- If
enabledis omitted,OPENCODE_MODELS_DISCOVERY_DEFAULT_ENABLEDcontrols the default when set. - If the environment variable is omitted or invalid, the built-in default is
true. - OpenCode
enabled_providersanddisabled_providerscontrol whether providers are available at all. This plugin does not override those OpenCode provider availability settings.
Accepted false values are false, 0, no, and off. Accepted true values are true, 1, yes, and on. Invalid values warn and fall back to true.
Provider-level filters live under provider.<name>.options.modelsDiscovery.models.
Prefer includeBy and excludeBy for model filtering. They work for id and for other top-level raw fields returned in the provider's /v1/models response.
Use includeBy or excludeBy with field: "id" and match when filtering model ids by regex. This is the recommended form for new config.
includeRegex and excludeRegex are retained as shortcuts for id-only regex filtering. They are regular expressions evaluated against the discovered model id and cannot filter non-id fields.
Use includeBy and excludeBy when filtering by top-level fields returned in the provider's raw /v1/models response. Each rule must include exactly one of:
equals: strict equality againststring,number,boolean, ornullfield valuesmatch: regular expression matching against string field values
{
"modelsDiscovery": {
"models": {
"excludeBy": [
{ "field": "available", "equals": false },
{ "field": "id", "match": "embedding" }
],
"includeBy": [
{ "field": "id", "match": "^deepseek" }
]
}
}
}includeBy keeps a model when it matches at least one rule. excludeBy removes a model when it matches any rule, and exclusion wins when both include and exclude rules match. Missing fields do not match. Nested paths, type coercion, arrays, and objects are not supported.
includeBy and excludeBy can replace includeRegex and excludeRegex for id filtering by using field: "id" with match.
includeBy and excludeBy are cumulative. A model must pass includeBy first, then excludeBy, before it can be injected.
Recommended field-filter order is:
includeByexcludeBy
Within that order:
includeByis an allow-list: when configured, a model must match at least one rule.excludeByis a deny-list: when a model matches any rule, it is removed.excludeBywins overincludeBywhen both match the same model.
includeRegex and excludeRegex are retained as legacy id-only shortcuts. They are not fully cumulative with each other: when includeRegex is configured, the model id only needs to match includeRegex, and excludeRegex is not applied. excludeRegex is applied only when includeRegex is not configured.
Prefer includeBy and excludeBy with field: "id" and match when you need both allow-list and deny-list regex behavior for model ids.
Provider-specific raw fields such as available are not part of the generic OpenAI-compatible model list contract. The plugin does not hardcode provider-specific behavior; use includeBy or excludeBy only when your provider returns the field.
Version 1.0.0 ignores legacy plugin-level discovery configuration at runtime. It still detects legacy config so users can migrate.
Legacy plugin-level options:
discovery.enabledproviders.includeproviders.excludemodels.includeRegexmodels.excludeRegexsmartModelName
When legacy global config is detected, the plugin logs a warning, shows a toast, and injects /models-discovery:migrate to guide migration. The legacy fields do not change discovery behavior.
Use /models-discovery:config for assistant-guided provider-level setup. Use /models-discovery:migrate when legacy plugin-level config is detected.
Community provider examples live in docs/config_example/.
The generic OpenAI-compatible /v1/models endpoint only guarantees a small model list shape. Extra metadata such as context limits, tool calling, reasoning, image input, or structured output is provider-specific, so metadata enrichment is opt-in.
The plugin currently supports five model info formats:
| Format | Source | Requires modelInfoEndpoint |
Notes |
|---|---|---|---|
"bifrost" |
Fields in Bifrost's /v1/models response |
No | Reads Bifrost inline limits, modalities, and base pricing when present |
"litellm" |
Provider-specific model info endpoint | No | Uses /v1/model/info by default; set modelInfoEndpoint to override it |
"models.dev" |
https://models.dev/models.json |
No | Uses the public models.dev metadata index |
"vllm" |
Fields in the provider's /v1/models response |
No | Reads vLLM-style max_model_len when present |
"lmstudio" |
LM Studio 0.4.0+ /api/v1/models inventory |
No | Uses /api/v1/models by default; set modelInfoEndpoint for another path |
Use modelInfoFormat: "bifrost" for a Bifrost AI Gateway provider. It reads Bifrost's documented inline metadata from the same /v1/models response and does not make another metadata request.
{
"plugin": ["opencode-models-discovery"],
"provider": {
"bifrost": {
"npm": "@ai-sdk/openai-compatible",
"name": "Bifrost",
"options": {
"baseURL": "http://127.0.0.1:8080/v1",
"modelsDiscovery": {
"enabled": true,
"modelInfoFormat": "bifrost"
}
},
"models": {}
}
}
}For each discovered model, the plugin maps Bifrost's reported context_length, max_input_tokens, and max_output_tokens to limit.context, limit.input, and limit.output. Limits are added only when both the context and output limits are available, as OpenCode requires both. It maps architecture.input_modalities and architecture.output_modalities to lower-case OpenCode modalities, translating Bifrost's SPEECH value to audio and ignoring unsupported values. Bifrost's pricing.prompt and pricing.completion are USD per-token rates; the plugin converts them to OpenCode's USD per-million-token cost.input and cost.output values. Costs are added only when both rates are available. Other pricing fields, scoped pricing overrides, and tiered pricing are not represented by this format.
When smartModelName: true is set for the provider, Bifrost's normalized_name is used when it is available. Missing or malformed fields are left unset. The normal unpaginated Bifrost /v1/models request returns the complete aggregated list; avoid configuring a page_size unless you intentionally want a paged subset.
LiteLLM exposes a richer /v1/model/info endpoint in addition to the OpenAI-compatible /v1/models endpoint.
Set modelInfoFormat to "litellm" to enable it. The plugin requests /v1/model/info by default; set modelInfoEndpoint only when the provider uses another path.
{
"plugin": ["opencode-models-discovery"],
"provider": {
"litellm": {
"npm": "@ai-sdk/openai-compatible",
"name": "LiteLLM",
"options": {
"baseURL": "http://127.0.0.1:4000/v1",
"modelsDiscovery": {
"enabled": true,
"endpoint": "/v1/models",
"modelInfoFormat": "litellm"
}
},
"models": {}
}
}
}When model info is available, the plugin uses LiteLLM model_info fields to populate OpenCode model configuration:
max_input_tokens,max_output_tokens, andmax_tokensbecomelimit.context,limit.input, andlimit.outputsupports_reasoningenablesreasoningsupports_*_reasoning_effortandsupported_openai_paramscreate reasoningvariants- By default, entries whose
model_info.modeis notchatare skipped
Use modelInfoFormat: "vllm" for a vLLM-compatible provider whose /v1/models response includes a numeric max_model_len field for each model. This does not make another metadata request.
{
"plugin": ["opencode-models-discovery"],
"provider": {
"local-vllm": {
"npm": "@ai-sdk/openai-compatible",
"name": "Local vLLM",
"options": {
"baseURL": "http://127.0.0.1:8000/v1",
"modelsDiscovery": {
"enabled": true,
"modelInfoFormat": "vllm"
}
},
"models": {}
}
}
}For each discovered model with a positive numeric max_model_len, the plugin sets limit.context and limit.output to that value. max_model_len represents the total request sequence length shared by prompt and generated tokens; it is not used as an independent input limit.
max_model_len is not part of the standard OpenAI-compatible /v1/models response. If a vLLM deployment or proxy does not expose it, discovery still succeeds but no limit is added. This format does not infer reasoning, tool-calling, modalities, or other capabilities.
Use modelInfoFormat: "lmstudio" with LM Studio 0.4.0+, which officially released the native v1 REST API and GET /api/v1/models, to discover models through /v1/models and enrich them from /api/v1/models. Set modelInfoEndpoint only when LM Studio uses another inventory path.
{
"plugin": ["opencode-models-discovery"],
"provider": {
"lmstudio": {
"npm": "@ai-sdk/openai-compatible",
"name": "LM Studio",
"options": {
"baseURL": "http://127.0.0.1:1234/v1",
"modelsDiscovery": {
"enabled": true,
"modelInfoFormat": "lmstudio"
}
},
"models": {}
}
}
}Only models returned by /v1/models are injected. A model is enriched only when its id exactly matches an inventory key; inventory-only models are not injected. modelsDiscovery.endpoint controls discovery, while modelsDiscovery.modelInfoEndpoint controls the inventory request.
When available, the plugin sets limit.context from the largest loaded instance config.context_length, otherwise it uses max_context_length. LM Studio does not report a distinct output limit, so the plugin writes limit.output: 0: this satisfies OpenCode's requirement that a limit object include both context and output while preserving OpenCode's default or configured output-token fallback. The plugin maps capabilities.vision to image input, capabilities.trained_for_tool_use to tool_call, and reported reasoning options to reasoning plus low, medium, and high variants. Missing or malformed metadata is left unset without preventing discovery.
Use modelInfoFormat: "models.dev" to enrich discovered models from the public models.dev metadata index.
This project is not affiliated with, endorsed by, or sponsored by models.dev.
This does not require modelInfoEndpoint, because the source is fixed to https://models.dev/models.json:
{
"plugin": ["opencode-models-discovery"],
"provider": {
"openrouter": {
"npm": "@ai-sdk/openai-compatible",
"name": "OpenRouter",
"options": {
"baseURL": "https://openrouter.ai/api/v1",
"apiKey": "YOUR_OPENROUTER_API_KEY",
"modelsDiscovery": {
"enabled": true,
"modelInfoFormat": "models.dev"
}
},
"models": {}
}
}
}When a discovered model can be matched to models.dev metadata, the plugin may populate:
limit.context,limit.input, andlimit.outputattachmentreasoningtool_callstructured_outputtemperaturemodalities
The current models.dev data uses a flat provider/model-keyed object. Its limit object is singular, and its context, input, and output fields are independently optional. structured_output and temperature may also be omitted; the plugin leaves omitted fields unset rather than inferring false. The current models.dev dataset does not provide variants metadata.
Matching is intentionally conservative:
- Exact model ids are preferred.
- Provider ids are not used for models.dev matching; only the model id segment after the provider prefix is matched.
- Prefix matching is limited to strong model id segment variants, such as date-suffixed model ids.
If models.dev cannot be fetched, or if no safe match is found, discovery still succeeds and the plugin leaves metadata fields unset. It does not inject hardcoded default context or output limits for unknown models.
Because this option makes a public network request to models.dev during discovery, it is disabled unless explicitly configured.
For providers with custom metadata paths or non-standard behavior:
{
"provider": {
"custom": {
"npm": "@ai-sdk/openai-compatible",
"options": {
"baseURL": "http://127.0.0.1:9000/v1",
"modelsDiscovery": {
"modelInfoEndpoint": "/custom/model-info",
"modelInfoFormat": "litellm",
"filterNonChat": false
}
}
}
}
}{
"plugin": ["opencode-models-discovery"],
"provider": {
"lmstudio": {
"npm": "@ai-sdk/openai-compatible",
"name": "LM Studio",
"options": {
"baseURL": "http://127.0.0.1:1234/v1",
"modelsDiscovery": {
"enabled": true,
"endpoint": "/v1/models",
"models": {
"includeBy": [
{ "field": "id", "match": "^gpt-" }
]
},
"smartModelName": true
}
},
"models": {}
},
"deepseek": {
"npm": "@ai-sdk/openai-compatible",
"name": "DeepSeek",
"options": {
"baseURL": "https://api.deepseek.com",
"apiKey": "sk-example-deepseek-key",
"modelsDiscovery": {
"enabled": true,
"endpoint": "/models",
"smartModelName": true
}
},
"models": {}
}
}
}In this example:
lmstudioexplicitly enables discovery and uses the default/v1/modelsendpoint.lmstudiolimits discovery to model ids matching^gpt-withincludeBy.deepseekexplicitly enables discovery but uses"/models"instead of/v1/models.- The API key uses an example placeholder and should be replaced in real configs.
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-models-discovery"],
"provider": {
"ollama": {
"npm": "@ai-sdk/openai-compatible",
"name": "Ollama",
"options": {
"baseURL": "http://127.0.0.1:11434/v1",
"modelsDiscovery": {
"enabled": true,
"models": {
"includeBy": [
{ "field": "id", "match": "^qwen/" }
]
}
}
}
},
"deepseek": {
"npm": "@ai-sdk/openai-compatible",
"name": "DeepSeek",
"options": {
"baseURL": "https://api.deepseek.com",
"apiKey": "YOUR_DEEPSEEK_API_KEY",
"modelsDiscovery": {
"enabled": true,
"endpoint": "/models",
"smartModelName": true
}
}
}
}
}In this example:
- The plugin entry is simple and contains no legacy global discovery config.
ollamauses the default discovery path derived from its/v1baseURL.deepseekdoes not rely on/v1/modelsand explicitly uses"/models".- Each provider can evolve independently without changing global include or endpoint rules.
For new configs, enable or disable discovery on the provider itself:
{
"provider": {
"ollama": {
"options": {
"modelsDiscovery": {
"enabled": true
}
}
},
"lmstudio": {
"options": {
"modelsDiscovery": {
"enabled": false
}
}
}
}
}Legacy plugin-level provider filters are ignored in 1.0.0 and are shown here only to help identify config that should be migrated:
| Option | Type | Description |
|---|---|---|
providers.include |
string[] |
If non-empty, only these providers will be discovered |
providers.exclude |
string[] |
These providers will be skipped when include is empty |
{
"plugin": [
["opencode-models-discovery", {
"providers": {
"include": ["ollama"],
"exclude": ["lmstudio"]
}
}]
]
}Control which discovered models are auto-injected with provider-level field filters:
| Option | Type | Description |
|---|---|---|
provider.<name>.options.modelsDiscovery.models.includeBy |
{ field: string, equals: string | number | boolean | null }[] or { field: string, match: string }[] |
If non-empty, only discovered models matching at least one rule will be added for this provider |
provider.<name>.options.modelsDiscovery.models.excludeBy |
{ field: string, equals: string | number | boolean | null }[] or { field: string, match: string }[] |
Discovered models matching any rule will be skipped for this provider |
provider.<name>.options.modelsDiscovery.models.includeRegex |
string[] |
Id-only shortcut for includeBy with field: "id" and match |
provider.<name>.options.modelsDiscovery.models.excludeRegex |
string[] |
Id-only shortcut for excludeBy with field: "id" and match |
Filtering only applies to auto-discovered models. Models already explicitly configured by the user are preserved.
{
"provider": {
"ollama": {
"options": {
"modelsDiscovery": {
"models": {
"includeBy": [
{ "field": "id", "match": "^qwen/|gpt-4" }
],
"excludeBy": [
{ "field": "id", "match": "embedding|test" }
]
}
}
}
}
}
}Legacy plugin-level model filters are ignored in 1.0.0. Move them to provider.<name>.options.modelsDiscovery.models when you still need those filters. Prefer migrating regex id filters to includeBy/excludeBy rules using field: "id" and match; provider-level includeRegex/excludeRegex remain available as id-only shortcuts.