fix(models): preserve provider-qualified model selections in the picker

- Stop provider-qualified or slash-qualified model inputs from fuzzy-matching a
  sibling catalog entry when the exact requested model is missing from the
  curated picker list.
- Preserve the raw typed selection so uncatalogued provider-routed models
  fall through to a temporary custom option instead of silently snapping to a
  nearby curated model.
- Add generalized regression coverage for provider-qualified uncatalogued
  picker selections.
This commit is contained in:
Philippe Le Rohellec
2026-05-29 10:24:44 -07:00
parent cf003ae986
commit e6aa9271c2
2 changed files with 49 additions and 3 deletions

View File

@@ -1068,6 +1068,13 @@ function _findModelInDropdown(modelId, sel, preferredProviderId){
if(opts.includes(modelId)) return modelId;
const exact=opts.find(o=>norm(o)===target);
if(exact) return exact;
// If the request is provider-qualified (either explicit @provider:model or
// a slash-qualified vendor/model id), do NOT fuzzy-match a sibling model
// once exact/provider-aware lookup failed. Returning null lets the caller
// preserve the raw typed value instead of snapping to the closest catalog
// entry. This keeps uncatalogued models routable instead of silently turning
// them into a nearby curated sibling.
if(rawModel.startsWith('@')||rawModel.includes('/')) return null;
// 3. Prefix/substring: require the candidate to start with the FULL normalized target
// (not a truncated base). This avoids false matches like gpt.5.5 → gpt.5.4.mini (#1188).
// Only fall back to the shorter base form if target itself is very short (a bare root

View File

@@ -45,10 +45,22 @@ function extractFunc(name) {
}
return ui.slice(start, i);
}
function _getOptionProviderId(opt) {
if (!opt) return '';
if (opt.dataset && opt.dataset.provider) return opt.dataset.provider;
const group = opt.parentElement;
if (group && group.tagName === 'OPTGROUP' && group.dataset && group.dataset.provider) {
return group.dataset.provider;
}
const value = String(opt.value || '');
if (value.startsWith('@') && value.includes(':')) return value.slice(1, value.lastIndexOf(':'));
return '';
}
eval(extractFunc('_findModelInDropdown'));
const args = JSON.parse(process.argv[3]);
const sel = { options: args.options.map(v => ({value: v})) };
const got = _findModelInDropdown(args.modelId, sel);
const got = _findModelInDropdown(args.modelId, sel, args.preferredProvider || undefined);
process.stdout.write(JSON.stringify(got));
"""
@@ -60,11 +72,11 @@ def driver_path(tmp_path_factory):
return str(p)
def _find(driver_path, model_id: str, options: list[str]):
def _find(driver_path, model_id: str, options: list[str], preferred: str | None = None):
import json
result = subprocess.run(
[NODE, driver_path, str(UI_JS_PATH),
json.dumps({"modelId": model_id, "options": options})],
json.dumps({"modelId": model_id, "options": options, "preferredProvider": preferred})],
capture_output=True, text=True, timeout=10,
)
if result.returncode != 0:
@@ -113,6 +125,33 @@ class TestPreservedFuzzyMatches:
)
assert got == "@nous:openai/gpt-5.5"
def test_explicit_provider_hint_does_not_snap_to_sibling_model(self, driver_path):
"""Provider-qualified requests must not fuzzy-match a sibling catalog entry.
This is the generalized regression behind #3113: any slash-qualified or
@provider-qualified model that is missing from the curated catalog should
preserve its exact value instead of snapping to a nearby curated sibling.
"""
got = _find(
driver_path,
"vendor/special-model",
["@openrouter:vendor/special-model-pro"],
preferred="openrouter",
)
assert got is None, (
"provider-qualified uncatalogued models must not fuzzy-match sibling models "
"(#3113)"
)
def test_provider_qualified_exact_match_still_resolves(self, driver_path):
got = _find(
driver_path,
"vendor/special-model",
["@openrouter:vendor/special-model"],
preferred="openrouter",
)
assert got == "@openrouter:vendor/special-model"
def test_bare_root_gpt_matches_versioned_option(self, driver_path):
"""Short root targets still fall back to the looser prefix match."""
got = _find(driver_path, "gpt", ["@nous:openai/gpt-5.4-mini"])