feat: filter discovered models with include/exclude globs - #4
Conversation
Add includeModels and excludeModels plugin options so only a subset of the models discovered from CLIProxyAPI's /v1/models endpoint is exposed to OpenCode. Entries are glob patterns (* and ?) matched against model IDs; excludeModels takes precedence over includeModels. Filtering to zero models throws at startup so misconfiguration is not silently ignored.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: be84aaad9a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| apiKey, | ||
| protocol: options.protocol ?? "chat", | ||
| catalog, | ||
| catalog: filteredCatalog, |
There was a problem hiding this comment.
Keep excluded existing models out of the provider
Passing filteredCatalog here does not fully enforce the new filters because addProvider later merges existing?.models back into the provider via mergeModels(discovered, existing?.models). In any config that already has a customized or stale entry for a model that the filter excludes, such as includeModels: ["claude-*"] with an existing gpt-5.6-terra override, that model is still exposed in OpenCode even though the log reports it was filtered out. Please apply the same filter when preserving existing model entries, or only merge overrides for models that survived filtering.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch — fixed in fcb4512.
The root cause was mergeModels re-merging every entry from the existing provider model config, so an override for a filtered-out model (e.g. includeModels: ["claude-*"] with a pre-existing gpt-5.6-terra override) still leaked through.
Fix: addProvider/mergeModels now take an optional allowedModelIDs set, populated with the surviving discovered IDs only when a filter is active. The merge loop skips any existing entry not in that set, so overrides are preserved solely for models that survived filtering. With no filter configured, allowedModelIDs is undefined and behavior is unchanged (existing models still merge as before).
Added a regression test (filtering drops existing overrides for excluded models but keeps included ones) covering exactly the scenario you described: a gpt-5.6-terra override is dropped under includeModels: ["claude-*"], while a claude-sonnet-4-6 override is still applied. bun run check passes (33 tests).
mergeModels previously re-merged every entry from existing provider model config, so a user override for a model the filter excludes (e.g. includeModels: ["claude-*"] with an existing gpt-5.6-terra override) was still exposed. Pass an allowlist of surviving model IDs (only when a filter is active) so overrides are preserved only for models that survive filtering. No behavior change when no filter is configured.
Summary
Adds
includeModelsandexcludeModelsplugin options so that not every model exposed by CLIProxyAPI's/v1/modelsendpoint has to be exposed to OpenCode. Useful when CLIProxyAPI fronts many providers/models but you only want a specific subset available in OpenCode's/modelspicker.Filtering runs after discovery and before the provider is merged into the OpenCode config, so provider behavior (protocol routing, capability hints, user model overrides) is unchanged.
How it works
*matches any run of characters?matches a single character.) are matched literallyincludeModelskeeps only matching models;excludeModelsdrops matching models.excludeModelstakes precedence overincludeModelswhen both match.{ "plugin": [ [ "opencode-cliproxyapi", { "baseURL": "http://your-server:8317", "apiKey": "your-key", "includeModels": ["claude-*", "gpt-5.*"], "excludeModels": ["*-image"] } ] ] }Implementation
src/catalog.ts: addglobToRegExp(tiny, dependency-free glob -> RegExp) andfilterModels(models, { include, exclude }), plus an exportedModelFiltertype.src/index.ts: parse the two options inreadOptions, applyfilterModelsto the discovered catalog, throw when the result is empty, and surface the filter in the discovery log line.README.md: document the new options (table + a "Filtering discovered models" section) with examples.CHANGELOG.md: entry under[Unreleased].Notes
CONTRIBUTING.mdthis is a user-visible behavior change; I went straight to a PR with tests + docs for review rather than opening an issue first, but happy to split it out into an issue for discussion if you prefer.Checklist
bun run checkpasses (typecheck + 32 tests + build; was 14 before)globToRegExp,filterModels, and the plugin(include, exclude-wins, and empty-result error path)