fix(#5989): keep colon-suffixed proxy models distinct
This commit is contained in:
parent
ff89f2ebd6
commit
3ada146d48
2 changed files with 54 additions and 23 deletions
61
static/ui.js
61
static/ui.js
|
|
@ -2732,12 +2732,20 @@ function _providerFromModelValue(modelId){
|
||||||
if(value.startsWith('@')&&value.includes(':')) return value.slice(1,value.lastIndexOf(':'));
|
if(value.startsWith('@')&&value.includes(':')) return value.slice(1,value.lastIndexOf(':'));
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
function _modelPickerOptionIdentity(modelId){
|
function _modelPickerOptionIdentity(modelId, providerId){
|
||||||
let value=String(modelId||'');
|
let value=String(modelId||'');
|
||||||
|
const provider=String(providerId||'').trim();
|
||||||
if(value.startsWith('@')&&value.includes(':')){
|
if(value.startsWith('@')&&value.includes(':')){
|
||||||
value=value.startsWith('@custom:')
|
const exactPrefix=provider ? `@${provider}:` : '';
|
||||||
? value.substring(value.lastIndexOf(':')+1)
|
if(exactPrefix && value.toLowerCase().startsWith(exactPrefix.toLowerCase())){
|
||||||
: value.substring(value.indexOf(':')+1);
|
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();
|
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 options=Array.from(group.children||[]).filter(opt=>opt&&opt.tagName==='OPTION');
|
||||||
const byIdentity=new Map();
|
const byIdentity=new Map();
|
||||||
for(const opt of options){
|
for(const opt of options){
|
||||||
const identity=_modelPickerOptionIdentity(opt.value);
|
const identity=_modelPickerOptionIdentity(opt.value,_getOptionProviderId(opt));
|
||||||
if(!identity) continue;
|
if(!identity) continue;
|
||||||
if(!byIdentity.has(identity)) byIdentity.set(identity,[]);
|
if(!byIdentity.has(identity)) byIdentity.set(identity,[]);
|
||||||
byIdentity.get(identity).push(opt);
|
byIdentity.get(identity).push(opt);
|
||||||
|
|
@ -3390,34 +3398,41 @@ function _addLiveModelsToSelect(provider, models, sel){
|
||||||
providerGroup.dataset.provider=provider;
|
providerGroup.dataset.provider=provider;
|
||||||
}
|
}
|
||||||
const existingIds=new Set([...sel.options].map(o=>o.value));
|
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 _ap=(window._activeProvider||'').toLowerCase();
|
||||||
const _providerLower=String(provider||'').toLowerCase();
|
const _providerLower=String(provider||'').toLowerCase();
|
||||||
const _isNamedCustomActiveProvider=_ap.startsWith('custom:');
|
const _isNamedCustomActiveProvider=_ap.startsWith('custom:');
|
||||||
const _isPortalFetch=_ap && _ap!=='openrouter' && _ap!=='custom' && _ap!=='openai-codex' && (_providerLower===_ap||_isNamedCustomActiveProvider&&_providerLower===_ap);
|
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){
|
for(const m of models){
|
||||||
let mid=m.id;
|
let mid=m.id;
|
||||||
if(_isPortalFetch && !mid.startsWith('@')){
|
if(_isPortalFetch && !mid.startsWith('@')){
|
||||||
mid=`@${provider}:${mid}`;
|
mid=`@${provider}:${mid}`;
|
||||||
}
|
}
|
||||||
if(existingIds.has(mid)) continue;
|
if(existingIds.has(mid)) continue;
|
||||||
const identity=optionIdentity(mid);
|
const identity=optionIdentity(mid,provider);
|
||||||
if(hasExistingNorm(identity)){
|
if(existingNorm.has(identity)){
|
||||||
const sameGroup=Array.from(providerGroup.children||[]).find(o=>optionIdentity(o.value)===identity);
|
const sameGroup=Array.from(providerGroup.children||[]).find(o=>optionIdentity(o.value,_getOptionProviderId(o))===identity);
|
||||||
if(sameGroup){
|
if(sameGroup){
|
||||||
const incomingRoutable=String(mid).startsWith('@');
|
const incomingRoutable=String(mid).startsWith('@');
|
||||||
const existingRoutable=String(sameGroup.value||'').startsWith('@');
|
const existingRoutable=String(sameGroup.value||'').startsWith('@');
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,12 @@ def _function(source, name):
|
||||||
|
|
||||||
|
|
||||||
def _run_harness():
|
def _run_harness():
|
||||||
|
provider_helper = _function(UI_JS, "_getOptionProviderId")
|
||||||
identity = _function(UI_JS, "_modelPickerOptionIdentity")
|
identity = _function(UI_JS, "_modelPickerOptionIdentity")
|
||||||
helper = _function(UI_JS, "_deduplicateModelPickerOptions")
|
helper = _function(UI_JS, "_deduplicateModelPickerOptions")
|
||||||
live_add = _function(UI_JS, "_addLiveModelsToSelect")
|
live_add = _function(UI_JS, "_addLiveModelsToSelect")
|
||||||
script = f"""
|
script = f"""
|
||||||
|
{provider_helper}
|
||||||
{identity}
|
{identity}
|
||||||
{helper}
|
{helper}
|
||||||
{live_add}
|
{live_add}
|
||||||
|
|
@ -87,6 +89,9 @@ const sameSuffix=makeSelect();
|
||||||
addCatalog(sameSuffix,'vendor-a/deepseek-v4-pro');
|
addCatalog(sameSuffix,'vendor-a/deepseek-v4-pro');
|
||||||
addCatalog(sameSuffix,'vendor-b/catalog/deepseek-v4-pro');
|
addCatalog(sameSuffix,'vendor-b/catalog/deepseek-v4-pro');
|
||||||
sameSuffix.value='vendor-a/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');
|
const crossProviderLive=new Node('select');
|
||||||
_addLiveModelsToSelect('custom:a',[{{id:'@custom:a:gpt-4o',label:'GPT-4o A'}}],crossProviderLive);
|
_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);
|
_addLiveModelsToSelect('custom:b',[{{id:'@custom:b:gpt-4o',label:'GPT-4o B'}}],crossProviderLive);
|
||||||
|
|
@ -98,6 +103,7 @@ console.log(JSON.stringify({{
|
||||||
catalogFirst:snapshot(catalogFirst),
|
catalogFirst:snapshot(catalogFirst),
|
||||||
selectedBare:snapshot(selectedBare),
|
selectedBare:snapshot(selectedBare),
|
||||||
sameSuffix:snapshot(sameSuffix),
|
sameSuffix:snapshot(sameSuffix),
|
||||||
|
sameGroupColonSuffix:snapshot(sameGroupColonSuffix),
|
||||||
crossProviderLive:snapshot(crossProviderLive),
|
crossProviderLive:snapshot(crossProviderLive),
|
||||||
unnamespaced:snapshot(unnamespaced),
|
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():
|
def test_live_models_with_same_identity_survive_in_different_provider_groups():
|
||||||
assert _run_harness()["crossProviderLive"] == {
|
assert _run_harness()["crossProviderLive"] == {
|
||||||
"groups": [["@custom:a:gpt-4o"], ["@custom:b:gpt-4o"]],
|
"groups": [["@custom:a:gpt-4o"], ["@custom:b:gpt-4o"]],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue