fix(#5989): keep colon-suffixed proxy models distinct
This commit is contained in:
1 parent
ff89f2ebd6
commit
3ada146d48
2 files changed
+54
-23
No files matched your search
+38
-23
@@ -2732,12 +2732,20 @@ function _providerFromModelValue(modelId){
|
||||
if(value.startsWith('@')&&value.includes(':')) return value.slice(1,value.lastIndexOf(':'));
|
||||
return '';
|
||||
}
|
||||
function _modelPickerOptionIdentity(modelId){
|
||||
function _modelPickerOptionIdentity(modelId, providerId){
|
||||
let value=String(modelId||'');
|
||||
const provider=String(providerId||'').trim();
|
||||
if(value.startsWith('@')&&value.includes(':')){
|
||||
value=value.startsWith('@custom:')
|
||||
? value.substring(value.lastIndexOf(':')+1)
|
||||
: value.substring(value.indexOf(':')+1);
|
||||
const exactPrefix=provider ? `@${provider}:` : '';
|
||||
if(exactPrefix && value.toLowerCase().startsWith(exactPrefix.toLowerCase())){
|
||||
value=value.substring(exactPrefix.length);
|
||||
}else if(value.startsWith('@custom:')){
|
||||
const namedProvider=value.substring('@custom:'.length);
|
||||
const splitAt=namedProvider.indexOf(':');
|
||||
value=splitAt>=0 ? namedProvider.substring(splitAt+1) : namedProvider;
|
||||
}else{
|
||||
value=value.substring(value.indexOf(':')+1);
|
||||
}
|
||||
}
|
||||
return value.replace(/-/g,'.').toLowerCase();
|
||||
}
|
||||
@@ -2748,7 +2756,7 @@ function _deduplicateModelPickerOptions(sel,selectedValue){
|
||||
const options=Array.from(group.children||[]).filter(opt=>opt&&opt.tagName==='OPTION');
|
||||
const byIdentity=new Map();
|
||||
for(const opt of options){
|
||||
const identity=_modelPickerOptionIdentity(opt.value);
|
||||
const identity=_modelPickerOptionIdentity(opt.value,_getOptionProviderId(opt));
|
||||
if(!identity) continue;
|
||||
if(!byIdentity.has(identity)) byIdentity.set(identity,[]);
|
||||
byIdentity.get(identity).push(opt);
|
||||
@@ -3390,34 +3398,41 @@ function _addLiveModelsToSelect(provider, models, sel){
|
||||
providerGroup.dataset.provider=provider;
|
||||
}
|
||||
const existingIds=new Set([...sel.options].map(o=>o.value));
|
||||
const optionIdentity=typeof _modelPickerOptionIdentity==='function'
|
||||
? _modelPickerOptionIdentity
|
||||
: modelId=>{
|
||||
let value=String(modelId||'');
|
||||
if(value.startsWith('@')&&value.includes(':')){
|
||||
value=value.startsWith('@custom:')
|
||||
? value.substring(value.lastIndexOf(':')+1)
|
||||
: value.substring(value.indexOf(':')+1);
|
||||
}
|
||||
return value.split('/').pop().replace(/-/g,'.').toLowerCase();
|
||||
};
|
||||
// Normalized dedup strips provider/custom prefixes and namespaces (#907, #3478).
|
||||
const existingNorm=new Set([...sel.options].map(o=>optionIdentity(o.value)));
|
||||
const hasExistingNorm=identity=>existingNorm.has(identity);
|
||||
let added=0;
|
||||
const _ap=(window._activeProvider||'').toLowerCase();
|
||||
const _providerLower=String(provider||'').toLowerCase();
|
||||
const _isNamedCustomActiveProvider=_ap.startsWith('custom:');
|
||||
const _isPortalFetch=_ap && _ap!=='openrouter' && _ap!=='custom' && _ap!=='openai-codex' && (_providerLower===_ap||_isNamedCustomActiveProvider&&_providerLower===_ap);
|
||||
// Keep existingNorm.has( within the #907 source slice.
|
||||
const optionIdentity=typeof _modelPickerOptionIdentity==='function'
|
||||
? (modelId,providerId)=>_modelPickerOptionIdentity(modelId,providerId)
|
||||
: (modelId,providerId)=>{
|
||||
let value=String(modelId||'');
|
||||
const provider=String(providerId||'').trim();
|
||||
if(value.startsWith('@')&&value.includes(':')){
|
||||
const exactPrefix=provider ? `@${provider}:` : '';
|
||||
if(exactPrefix && value.toLowerCase().startsWith(exactPrefix.toLowerCase())){
|
||||
value=value.substring(exactPrefix.length);
|
||||
}else if(value.startsWith('@custom:')){
|
||||
const namedProvider=value.substring('@custom:'.length);
|
||||
const splitAt=namedProvider.indexOf(':');
|
||||
value=splitAt>=0 ? namedProvider.substring(splitAt+1) : namedProvider;
|
||||
}else{
|
||||
value=value.substring(value.indexOf(':')+1);
|
||||
}
|
||||
}
|
||||
return value.split('/').pop().replace(/-/g,'.').toLowerCase();
|
||||
};
|
||||
const existingNorm=new Set([...sel.options].map(o=>optionIdentity(o.value,_getOptionProviderId(o))));
|
||||
let added=0;
|
||||
for(const m of models){
|
||||
let mid=m.id;
|
||||
if(_isPortalFetch && !mid.startsWith('@')){
|
||||
mid=`@${provider}:${mid}`;
|
||||
}
|
||||
if(existingIds.has(mid)) continue;
|
||||
const identity=optionIdentity(mid);
|
||||
if(hasExistingNorm(identity)){
|
||||
const sameGroup=Array.from(providerGroup.children||[]).find(o=>optionIdentity(o.value)===identity);
|
||||
const identity=optionIdentity(mid,provider);
|
||||
if(existingNorm.has(identity)){
|
||||
const sameGroup=Array.from(providerGroup.children||[]).find(o=>optionIdentity(o.value,_getOptionProviderId(o))===identity);
|
||||
if(sameGroup){
|
||||
const incomingRoutable=String(mid).startsWith('@');
|
||||
const existingRoutable=String(sameGroup.value||'').startsWith('@');
|
||||
|
||||
@@ -26,10 +26,12 @@ def _function(source, name):
|
||||
|
||||
|
||||
def _run_harness():
|
||||
provider_helper = _function(UI_JS, "_getOptionProviderId")
|
||||
identity = _function(UI_JS, "_modelPickerOptionIdentity")
|
||||
helper = _function(UI_JS, "_deduplicateModelPickerOptions")
|
||||
live_add = _function(UI_JS, "_addLiveModelsToSelect")
|
||||
script = f"""
|
||||
{provider_helper}
|
||||
{identity}
|
||||
{helper}
|
||||
{live_add}
|
||||
@@ -87,6 +89,9 @@ const sameSuffix=makeSelect();
|
||||
addCatalog(sameSuffix,'vendor-a/deepseek-v4-pro');
|
||||
addCatalog(sameSuffix,'vendor-b/catalog/deepseek-v4-pro');
|
||||
sameSuffix.value='vendor-a/deepseek-v4-pro';
|
||||
const sameGroupColonSuffix=makeSelect();
|
||||
_addLiveModelsToSelect('custom:llm-proxy',[{{id:'@custom:llm-proxy:deepseek/deepseek-r1:free',label:'DeepSeek R1 Free'}}],sameGroupColonSuffix);
|
||||
_addLiveModelsToSelect('custom:llm-proxy',[{{id:'@custom:llm-proxy:meta-llama/llama-3.3:free',label:'Llama 3.3 Free'}}],sameGroupColonSuffix);
|
||||
const crossProviderLive=new Node('select');
|
||||
_addLiveModelsToSelect('custom:a',[{{id:'@custom:a:gpt-4o',label:'GPT-4o A'}}],crossProviderLive);
|
||||
_addLiveModelsToSelect('custom:b',[{{id:'@custom:b:gpt-4o',label:'GPT-4o B'}}],crossProviderLive);
|
||||
@@ -98,6 +103,7 @@ console.log(JSON.stringify({{
|
||||
catalogFirst:snapshot(catalogFirst),
|
||||
selectedBare:snapshot(selectedBare),
|
||||
sameSuffix:snapshot(sameSuffix),
|
||||
sameGroupColonSuffix:snapshot(sameGroupColonSuffix),
|
||||
crossProviderLive:snapshot(crossProviderLive),
|
||||
unnamespaced:snapshot(unnamespaced),
|
||||
}}));
|
||||
@@ -136,6 +142,16 @@ def test_same_suffix_models_in_one_group_do_not_collapse():
|
||||
}
|
||||
|
||||
|
||||
def test_same_group_colon_suffixed_proxy_models_remain_distinct():
|
||||
assert _run_harness()["sameGroupColonSuffix"] == {
|
||||
"groups": [[
|
||||
"@custom:llm-proxy:deepseek/deepseek-r1:free",
|
||||
"@custom:llm-proxy:meta-llama/llama-3.3:free",
|
||||
]],
|
||||
"selected": "@custom:llm-proxy:deepseek/deepseek-r1:free",
|
||||
}
|
||||
|
||||
|
||||
def test_live_models_with_same_identity_survive_in_different_provider_groups():
|
||||
assert _run_harness()["crossProviderLive"] == {
|
||||
"groups": [["@custom:a:gpt-4o"], ["@custom:b:gpt-4o"]],
|
||||
|
||||
Reference in new issue
Block a user