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:
@@ -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
|
||||
|
||||
@@ -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"])
|
||||
|
||||
Reference in New Issue
Block a user