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.
yourcasualdev
commented
Aug 9, 2026
Thanks for the PR — this is well put together. Focused scope, dependency-free glob implementation, unit + plugin tests, and both README and CHANGELOG updated. I checked out the branch and One real issue with the follow-up fix in fcb4512, plus a doc gap and some nits. The override fix drops models the filter never targeted
opencode-cliproxyapi/src/index.ts Lines 93 to 95 in fcb4512 and then opencode-cliproxyapi/src/index.ts Lines 214 to 215 in fcb4512 So a model the user declared by hand in their own OpenCode config — one CLIProxyAPI doesn't report — is deleted the moment any filter is set. Two cases I confirmed against the branch:
The second case misses the rule the CHANGELOG states ("overrides are only preserved for models that survive filtering") — that model survives the filter, it just wasn't discovered. Before this PR hand-added models were always preserved, so this is a silent regression for anyone combining a filter with a manual model entry. The fix is to filter existing IDs by the filter rather than by the discovered set — pass the if(modelFilter&&filterModels([{id: modelID}],modelFilter).length===0)continueThat still covers what Codex originally reported (a README should mention what happens to existing model entriesThe CHANGELOG says overrides are only preserved for surviving models, but the README describes the options purely as filters on "discovered model IDs" and doesn't say what happens to entries in the user's own Nits
No need for an issue first — the PR is a fine place to have had this discussion. Happy to merge once the |
Gating mergeModels on the surviving discovered IDs dropped hand-added models the filter never targeted: an exclude-only filter removed non-matching manual entries, and an include filter dropped manual entries matching the include glob that CLIProxyAPI does not report. Pass the ModelFilter down instead and test each existing entry against it, so overrides survive exactly when they match the filter. Also derive the filtering flag via hasModelFilter to share emptiness logic, and make glob escaping unicode-mode-safe.
fsx8
commented
Aug 21, 2026
Thanks for the thorough review — you're right on all counts. I verified both cases against the branch: gating Main fix: if(modelFilter&&filterModels([{id: modelID}],modelFilter).length===0)continueSo the original Codex report stays fixed (a Tests: added the exclude-only + hand-added case you asked for, plus the include + hand-added-matching case, and a hand-added entry that does match an exclude pattern (dropped). README: added a paragraph to "Filtering discovered models" describing what happens to Nits (addressed):
|
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)