OpenCode plugin that hides every paid model by default and turns Ctrl+L in the built-in Select model dialog into a toggle between the free-only and paid-only lists.
- Default catalog: free models only.
Ctrl+Lin the model picker: show paid models only.Ctrl+Lagain: back to free models only.- Free and paid auto routers are classified by their own cost entry, so a
free router (
openrouter/free,freellmapi/auto) is in the free list and a priced router (openrouter/autoat a non-zero price) is in the paid list. - The real picker is kept: search, favorites, recents, provider groups, model variants and selection all keep working.
The plugin has two halves and a local install needs both:
-
Server half — filters the catalog and serves the toggle RPC. Copy or junction this repository into the server's local plugin directory:
cmd /c mklink /J "$env:USERPROFILE\.config\opencode\plugins\free-model-filter" "C:\dev\opencode\workspace-8\opencode-free-model-filter"
(A plain copy of the repository also works; the junction just keeps the working tree live.)
-
TUI half — binds Ctrl+L in the model dialog. Add the package to
~/.config/opencode/cli.json:{ "plugins": [ { "package": "C:\\dev\\opencode\\workspace-8\\opencode-free-model-filter" } ] }
Restart OpenCode (or start a new session) and open Select model. Paid models are gone from the list.
| Where | Key | Result |
|---|---|---|
| Select model dialog | Ctrl+L |
Switch to the other bucket and show a toast |
| Anywhere else | Ctrl+L |
Unchanged; the plugin's binding only exists in dialogs |
The mode is runtime state for the current server location. A fresh server (or a restart) always starts in free mode, which is the default you asked for.
The rule is deliberately explicit and conservative:
- Explicit cost wins. If the model has a non-empty
costarray, every entry must haveinput === 0andoutput === 0. Cache prices are ignored. A non-zero entry means paid. - Empty or missing cost is free only when the provider is known to be free:
- a local/private base URL:
localhost,*.localhost,127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1,0.0.0.0,host.docker.internal; - the provider id is in
freeProviders(defaults:freellmapi,freellmapi-anthropic,logfare), or the provider id/name containsfree; - the model id ends in
:free/-free, or the name contains(free)or the wordfree.
- a local/private base URL:
- Unknown cost on a remote provider is paid. This is what keeps mixed
gateways such as
xkiro(108 models withcost: [], only 47 free) from leaking paid models into the default list. Those free models are hidden too until the provider declares a cost or you add it tofreeProviders.
freeProviders can be extended without editing code with a comma-separated
environment variable on the server:
$env:OPENCODE_FREE_MODEL_FILTER_PROVIDERS = "my-gateway,another-free-gateway"The server plugin also accepts plugin options freeProviders: string[] and
defaultMode: "free" | "paid" | "all" when it is loaded as a configured
package. Local directory discovery has no options; the environment variable is
the simple path.
The built-in model dialog has no plugin hook for its list and no plugin API for selecting a model, so the plugin does not replace it. Instead:
- The server half registers a replayable
ctx.model.transformthat setsenabled = falseon every model outside the active bucket. Transforms are rebuilt from the untouched catalog on every read, so flipping the bucket is reversible and a toggle never accumulates state. - The TUI half registers
Ctrl+Lin keymap modemodal, which OpenCode's dialog stack activates. The layer is also limited to a focused list-filter input, so the key stays inert in alerts, confirms and prompts. - The TUI half calls the location-scoped RPC
free-model-filter/toggle. The server updates the mode, callsctx.model.reload(), and OpenCode publishesmodel.updated; the open picker refreshes in place. - Models outside the bucket are only disabled, never removed, and the original
enabledvalue comes from the catalog on every replay. Toggling back restores the exact free list, including models the user disabled themselves.
bun install
bun testThe suite has three layers:
test/classify.test.ts— 40 unit tests for every classifier branch, including explicit zero cost, empty cost, local URLs, free markers, and free and paid auto routers.test/server.e2e.test.ts— starts the realopencode servewith an isolated config/project, waits for the plugin to activate, and asserts the real/api/modelendpoint returns exactly the free fixture models, that toggling via the realPOST /api/rpc/free-model-filter/togglereturns paid-only, thatmodel.updatedis published, and that toggling back restores the free list.test/tui.e2e.test.tsx— drives the installed OpenCode TUI through a Windows ConPTY: opens the real Select model dialog, verifies the exact free rows, presses Ctrl+L twice, verifies paid-only rows, closes/reopens the dialog to check mode persistence, verifies Ctrl+L is inert outside the dialog, and selects a model through the picker.
The e2e tests use the installed CLI. Set OPENCODE_BIN to another executable to
run them against a different build. They create a temp config/project with a
directory junction to this repository, so they never read or write your live
OpenCode state.
- Cost is only as good as the catalog. A provider that reports
cost: []for a mix of free and paid models is treated as paid; add it tofreeProvidersonly if every model behind it is genuinely free. - The toggle is per server location and resets to free on restart by design.
- Replacing the picker was rejected on purpose: model selection is not exposed to plugins, and a custom dialog could not select a model reliably on the home/new-session route.
MIT