Stop hardcoding model versions in your OpenCode config.
Model IDs go stale. Sonnet gets point releases, previews get promoted, and the IDs pinned
in your agents keep pointing at last month's model. This OpenCode v2
plugin adds floating aliases to the model catalog: a stable ID such as
opencode/zen-plan that always resolves to the newest model matching your rules.
Use a concrete model ID when you want to pin a model. Use an alias when you want to follow a family, or "the newest model that can do this job".
Add the plugin and one alias to opencode.json:
{
"plugins": [
{
"package": "opencode-model-aliases@latest",
"options": {
"aliases": {
"github-copilot/claude-sonnet": { "match": "github-copilot/claude-sonnet-*" }
}
}
}
]
}Run /model-aliases to see what each alias resolved to. Select github-copilot/claude-sonnet
like any other model; requests go to the real winning model.
OpenCode caches plugin packages. Update with
opencode plugin update opencode-model-aliases@latest; restarting alone won't.
Each alias key is <provider>/<alias-id>. Every pattern must use the same literal provider
as its key: an alias never selects a model from another provider.
Track the newest model in each family:
"openai/gpt-sol": { "match": "openai/gpt-*-sol" },
"openai/gpt-luna": { "match": "openai/gpt-*-luna" }Use exclude to keep previews out, or to avoid one model you don't want:
"openai/latest": {
"match": ["openai/gpt-*", "openai/o*"],
"exclude": ["openai/*-preview", "openai/gpt-6.1-sol-pro"]
}Choose only models with tools, image input and a large context window:
"github-copilot/vision": {
"match": "github-copilot/gemini-*-flash",
"filter": {
"capabilities": { "tools": true, "input": ["image"], "output": ["text"] },
"minContext": 128000
}
}"The best free model for planning" is a policy, not a model ID. Filter the free set by what each job needs, then point agents at the aliases:
{
"plugins": [{
"package": "opencode-model-aliases@latest",
"options": {
"aliases": {
"opencode/zen-plan": {
"match": ["opencode/*-free", "opencode/big-pickle"],
"filter": { "capabilities": { "tools": true }, "minContext": 256000 },
"name": "Zen Free — Plan"
},
"opencode/zen-build": {
"match": ["opencode/*-free", "opencode/big-pickle"],
"filter": {
"capabilities": { "tools": true, "input": ["text"], "output": ["text"] },
"minContext": 64000
},
"name": "Zen Free — Build"
}
}
}
}],
"agents": {
"plan": { "model": "opencode/zen-plan" },
"build": { "model": "opencode/zen-build" }
}
}Filters decide eligibility, not quality. If several models qualify, the newest wins, so
both aliases may currently pick the same model. *-free is a naming convention, not a price
check, and free offers can change.
Only active models are eligible by default. Opt in explicitly:
"openai/bleeding-edge": {
"match": "openai/gpt-*",
"filter": { "status": ["active", "alpha", "beta"] }
}At every catalog refresh, each alias:
- Matches models with
match, then removesexcludematches. Patterns are picomatch globs. - Filters by
enabled, status, capabilities andminContext. All filters must pass; missing metadata fails the filter that needs it. - Selects the newest
time.released. Ties use the descending model ID (gpt-6.1-sol-probeatsgpt-6.1-sol-fast). Models without release dates never win.
The alias gets a copy of the winner's model info under the alias ID. Aliases never select other aliases, and declaration order doesn't matter.
| Option | Default | Notes |
|---|---|---|
aliases |
required | Object of aliases; {} is valid. |
strict |
false |
Fail startup if any alias doesn't resolve. |
debug |
false |
Log each resolution as a [debug] warning. |
match |
required | Glob or list of globs: "provider/pattern". |
exclude |
none | Same format as match. |
filter.status |
["active"] |
Any of active, alpha, beta. |
filter.capabilities |
none | tools (boolean), input/output (all listed modalities required). |
filter.minContext |
none | Minimum context window, inclusive. |
name |
generated | Display name. Without it, sonnet becomes Sonnet (alias). |
select |
latest |
Only { "strategy": "latest" } is supported. |
Aliases can also live in .opencode/opencode-model-aliases.jsonc. The plugin uses the
nearest file found from the working directory upward. Inline options win: an inline alias
replaces a file alias with the same key, and inline strict/debug values win. Changes to
inline options apply when OpenCode reloads its configuration. The .jsonc file is read when
the plugin starts, so restart OpenCode after editing it.
- Invalid configuration fails at startup, including unknown keys and invalid globs.
- An alias that would overwrite a real model fails at startup.
- An alias with no candidate logs a warning and is left out; other aliases keep working.
With
strict: true, startup fails instead. - If a target disappears later, the alias disappears until a matching model returns.
Run /model-aliases, or choose Model aliases from the command palette. It shows each
target, unresolved reasons and the real model ID used for requests. It never calls a model.
In OpenCode 2.0.22, selecting the slash suggestion leaves a trailing space; press Enter again.
Without a TUI:
opencode api --standalone post /api/rpc/opencode-model-aliases/inspect --data '{"input":{}}'The first resolution sets a silent baseline. When an alias later changes target, the TUI shows
a toast and /model-aliases shows the previous target, the current target and when the change
was detected. Changing an alias's match or filter rules resets its baseline.
Run /model-aliases explain github-copilot/sonnet to see matching patterns,
rejected candidates and their reasons, and why the winner was selected. Eligible
runners-up show whether they lost on release date or the descending model ID
tie-break. Models outside the alias provider/include patterns are counted rather
than listed. The regular /model-aliases view remains compact.
The backend exposes explain({ alias: "github-copilot/sonnet" }) on the existing
opencode-model-aliases RPC. Its structured report comes from the same resolution
that materialized the alias, confirmed against the final catalog. It contains
public decision fields only, with stable reason codes and deterministic ordering.
An unresolved alias identifies the failed stage; an inactive alias retains its
selection explanation. Failed refreshes or conflicting downstream rewrites return
unavailable, and an unconfigured key returns unknown-alias.
Explanation uses the catalog visible to the plugin's transform; the existing config-disabled model limitation still applies. In strict mode, a startup failure prevents the plugin and its RPC from becoming available. Explanation performs no model requests and adds no persistent history or automatic logging.
- Only the
lateststrategy exists. The plugin can't rank by price or quality. - Aliases are provider-isolated and never chain.
- The plugin only reads OpenCode's catalog; it never fetches external model lists.
- Models hidden with
disabled: truein your OpenCode provider config can still be selected by an alias (#42). Useexcludeto keep a model out of an alias.
Requires Node.js 22+ and pnpm 11.
pnpm install
pnpm run verify # lint, typecheck, tests and build
pnpm run check:release # offline release checks
pnpm smoke:opencode # optional; needs an installed OpenCode v2 CLIMIT License — see LICENSE.