Skip to content

fix(model): show all provider models in /model picker, not just the selected one (#1423) - #1453

Closed
ajsai47 wants to merge 4 commits into
Twigpine:mainfrom
ajsai47:fix/model-picker-shows-all-provider-models-clean
Closed

ajsai47 wants to merge 4 commits into
Twigpine:mainfrom
ajsai47:fix/model-picker-shows-all-provider-models-clean

Conversation

@ajsai47

@ajsai47 ajsai47 commented May 31, 2026

Copy link
Copy Markdown

Problem

When a third-party provider profile is configured (e.g. Gitlawb Opengateway with mimo-v2.5-pro as the default), opening /model shows only that one selected model instead of the full route catalog. Reported in #1423.

Root Cause

mergeActiveProfileModelOptions (introduced in #1361) iterated over the profile's saved model list and used that as the complete set of options, looking up each entry in the route catalog for better labels. A profile with a single saved model would therefore suppress the entire route catalog and return only that one entry.

Fix

Reverse the merge direction: start with all route catalog models, then append any profile-configured models not already covered by the catalog. Users always see every available model for their provider; custom model IDs saved in the profile remain accessible at the bottom.

- const routeOptionsByValue = new Map(...)
- const merged: ModelOption[] = []
- const seen = new Set<string>()
+ const routeOptionKeys = new Set(...)
+ const merged: ModelOption[] = [...routeOptions]   // start with full catalog
+ const seen = new Set<string>(routeOptionKeys)
  for (const option of profileOptions) {
    ...
-   merged.push(routeOptionsByValue.get(key) ?? option)
+   merged.push(option)   // only appended if not already in catalog
  }

Tests

Updated the existing mergeActiveProfileModelOptions test that had encoded the wrong (buggy) behavior, and added a regression comment referencing #1423. Full suite: 3103 pass, 11 pre-existing failures unrelated to this change.

Closes #1423

…elected one (Twigpine#1423)

mergeActiveProfileModelOptions was introduced in Twigpine#1361 to surface
profile-configured models inside the descriptor-backed picker. The
implementation iterated over the profile's saved model list and used
that as the complete set of options, meaning a profile with a single
saved model (e.g. "mimo-v2.5-pro") would suppress the full route
catalog and show only that one entry — exactly the regression reported
in Twigpine#1423.

Fix: always start with the full route catalog options, then append any
profile-configured models that are not already covered by the catalog.
This ensures users see every available model for the active provider
while still surfacing custom models saved in their profile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found an issue that needs to be addressed before this is ready.

Findings

  • [P1] Keep descriptor catalog picks inside availableModels
    src/commands/model/model.tsx:150
    This now copies every descriptor route model into the picker override, but that override bypasses the normal allowlist path: ModelPicker uses optionsOverride instead of the getModelOptions() result that is filtered by availableModels, and this interactive /model handler writes the selected value directly without the isModelAllowed guard that /model <name> has. In an org/user config that restricts availableModels to a subset, opening the descriptor-backed picker for a provider profile will now show the whole route catalog and let the user select a disallowed model. Please filter the descriptor override with the same allowlist or reject disallowed selections in the interactive handler before updating mainLoopModel.

…odels allowlist

- In loadDescriptorDiscoveryContext, filter routeOptions with isModelAllowed
  before passing them to mergeActiveProfileModelOptions, so optionsOverride
  only contains models permitted by the org allowlist.
- Add isModelAllowed guard in handleSelect to block selection of disallowed
  models via the interactive picker (mirrors the existing guard on the
  /model <name> code path).
- Add test verifying that allowlist filtering removes blocked models from
  the options passed to mergeActiveProfileModelOptions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ajsai47

ajsai47 commented May 31, 2026

Copy link
Copy Markdown
Author

Fixed in latest commit — two-part:

  1. Descriptor override filtered at source: both loadDescriptorDiscoveryContext sites now filter routeOptions with isModelAllowed before passing to mergeActiveProfileModelOptions, so optionsOverride never contains models blocked by availableModels.

  2. handleSelect guard added: the interactive picker selection now checks isModelAllowed before updating mainLoopModel, matching the existing guard on the /model <name> path.

Test added: descriptor optionsOverride filters out models not in availableModels allowlist — mocks isModelAllowed to block one of two catalog models, verifies it's excluded from the override.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update. I rechecked the changed paths and found an issue that still needs to be addressed.

Findings

  • [P2] Keep refreshed descriptor options inside availableModels
    src/commands/model/model.tsx:535
    The initial descriptor context now filters routeOptions through isModelAllowed, but the refresh path rebuilds nextOptions directly from discoverModelsForRoute() and passes them into mergeActiveProfileModelOptions() without applying the same allowlist. When a descriptor route has autoRefresh enabled, opening /model starts refreshAvailableModels(false) and replaces the filtered picker options with the unfiltered discovered catalog; the manual in-picker refresh does the same. The new handleSelect guard prevents selecting a blocked model, but org-restricted users will still see disallowed models reappear in the picker after the refresh completes. Please apply the allowlist filtering to the refreshed descriptor options before calling setOptionsOverride(), and cover that refresh path in the regression test.

… descriptor path

The initial-load path filtered discovered catalog models through isModelAllowed
before merging with profile options, but the refreshAvailableModels path called
mergeActiveProfileModelOptions directly on the raw discovery results, bypassing
the allowlist entirely. A provider refresh (manual or auto-triggered) could
therefore re-introduce models that availableModels explicitly excluded.

Fix: split the buildRouteCatalogModelOptions call out, filter the result with
isModelAllowed (identical predicate to the ~line-250 initial-load path), then
merge — keeping profile-only models unfiltered, consistent with initial load.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ajsai47

ajsai47 commented May 31, 2026

Copy link
Copy Markdown
Author

Thanks, @jatmn — good catch on the refresh path.

Fix (commit f0c02c4): Split the buildRouteCatalogModelOptions call out of the mergeActiveProfileModelOptions argument, filter the result with isModelAllowed, then merge — matching the initial-load pattern at ~line 250:

// Before (refresh path bypassed allowlist):
const nextOptions = mergeActiveProfileModelOptions(
  discoveryContext.routeId,
  buildRouteCatalogModelOptions(...),
)

// After (same filter as initial load):
const discoveredRouteOptions = buildRouteCatalogModelOptions(...)
const allowedRouteOptions = discoveredRouteOptions.filter(o =>
  typeof o.value === 'string' ? isModelAllowed(o.value) : true,
)
const nextOptions = mergeActiveProfileModelOptions(
  discoveryContext.routeId,
  allowedRouteOptions,
)

Profile-only models added by mergeActiveProfileModelOptions are not filtered here, consistent with the initial-load path (profile models bypass the allowlist at both load and refresh).

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update. I rechecked the changed paths and found issues that still need to be addressed.

Findings

  • [P2] Keep profile-appended picker options inside availableModels
    src/commands/model/model.tsx:161
    The route catalog is now filtered before calling mergeActiveProfileModelOptions(), but the merge then appends every active-profile model that was not present in the filtered catalog. If an org allows only openai/gpt-5-mini and the active provider profile still contains qwen/qwen3-32b (or any blocked catalog model saved in the profile), the blocked model is filtered out of allowedRouteOptions and then immediately re-added from profileOptions. The new handleSelect guard rejects it after Enter, but /model still exposes disallowed choices on initial load and after refresh, which leaves the previous availableModels picker leak partially unresolved. Please filter the merged result, or skip disallowed profile options before appending them.

  • [P3] Apply the same filter in /model refresh summaries
    src/commands/model/model.tsx:819
    The non-interactive /model refresh path still builds nextOptions directly from the unfiltered discovery result before comparing it to discoveryContext.optionsOverride, which is now filtered during initial descriptor loading. With availableModels set, a refresh that discovers only blocked models will report Updated <provider> models. even though the allowed picker options did not change. Please apply the same allowlist filtering before merging in this path too, so the refresh summary matches the actual visible model set.

  • [P3] Cover the real descriptor allowlist paths
    src/commands/model/model.test.tsx:490
    The new allowlist regression test never exercises loadDescriptorDiscoveryContext(), the picker refresh path, or the mocked isModelAllowed() import. It manually filters allCatalogOptions with a local isModelAllowedFn and then asserts that mergeActiveProfileModelOptions() returns that already-filtered input, so the test would still pass if the production filters at initial load or refresh were removed. Because the prior review specifically asked to cover the refresh regression, please add coverage that opens or refreshes a descriptor-backed picker with availableModels set and verifies the resulting options exclude blocked catalog/profile entries.

…in /model refresh

Two gaps in the availableModels enforcement:

1. mergeActiveProfileModelOptions appended profile-configured models that were
   not already in the filtered catalog, without checking isModelAllowed. A
   blocked model absent from allowedRouteOptions could be re-added from
   profileOptions, exposing it in the picker on initial load and after refresh.
   Fix: skip profile entries where !isModelAllowed(value) before appending.

2. refreshModelsAndSummarize (/model refresh non-interactive path) built
   nextOptions from raw discovery results without filtering, so the "changed"
   comparison was apples-to-oranges against discoveryContext.optionsOverride
   (which is now filtered). A refresh that discovers only blocked models would
   incorrectly report "Updated <provider> models." without any visible change.
   Fix: filter the catalog options with the same isModelAllowed predicate
   before merging, consistent with interactive refresh and initial load.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ajsai47

ajsai47 commented Jun 1, 2026

Copy link
Copy Markdown
Author

Fixed both issues in commit baeadca.

[P2] Profile-only models now filtered by allowlist (mergeActiveProfileModelOptions):

The loop that appends profile models absent from the filtered catalog now skips entries where !isModelAllowed(value):

// Don't surface profile-configured models that are explicitly blocked by
// the availableModels allowlist. The catalog entries in routeOptions were
// already filtered upstream; without this guard, a blocked model absent
// from the filtered catalog would be re-added here from profileOptions.
if (!isModelAllowed(value)) {
  continue
}

[P3] /model refresh non-interactive path now uses filtered options:

refreshModelsAndSummarize now splits out the buildRouteCatalogModelOptions call, applies isModelAllowed filter, then merges — so the changed comparison against discoveryContext.optionsOverride (which is filtered) is apples-to-apples. A refresh that discovers only blocked models will no longer falsely report "Updated <provider> models."

Regarding [P3] test coverage — I'll look at expanding the tests to cover the descriptor allowlist paths more directly in a follow-up commit if you'd like, or happy to do it now as part of this PR.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update. I rechecked the changed paths and found one issue that still needs to be addressed.

Findings

  • [P3] Cover the real descriptor allowlist paths
    src/commands/model/model.test.tsx:490
    The new allowlist regression test still does not exercise loadDescriptorDiscoveryContext(), the picker refresh path, or the non-interactive /model refresh path. It manually filters allCatalogOptions with a local isModelAllowedFn and then calls mergeActiveProfileModelOptions() with already-filtered input, so it would continue passing if the production filters at initial load, interactive refresh, or refresh summary were removed again. Since the previous regressions were in those production descriptor paths, please add coverage that opens or refreshes a descriptor-backed picker with availableModels set and verifies blocked catalog and profile-appended entries stay out of the visible options and refresh comparison.

@jatmn

jatmn commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

closing, going with #1472

@jatmn jatmn closed this Jun 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

models of providers does not appear

2 participants