fix(ui): Fix the issue where custom models are not displayed in the model configuration list

- Fix the issue where custom models are not shown
- Fix the issue where custom models are not ollama but go through the ollama model processing function, causing the hyphen '-' in the model name to be replaced with a space " " and the last letter to be lowercase
This commit is contained in:
王浩生
2026-05-13 14:42:03 +08:00
parent 9268f411d8
commit a49c0fbf8b
+115 -16
View File
@@ -764,26 +764,67 @@ async function populateModelDropdown(){
const _modelsRes=await fetch(new URL('api/models',document.baseURI||location.href).href,{credentials:'include'});
if(_redirectIfUnauth(_modelsRes)) return;
const data=await _modelsRes.json();
if(!data.groups||!data.groups.length) return; // keep HTML defaults
// Store active provider globally so the send path can warn on mismatch
window._activeProvider=data.active_provider||null;
// Store default model so newSession() can apply it (#872).
// Per-page-load — not synced across browser tabs.
window._defaultModel=data.default_model||null;
window._configuredModelBadges=data.configured_model_badges||{};
const _synthGroupsFromConfigured=()=>{
const badgeMap=window._configuredModelBadges||{};
const grouped=new Map();
const addModel=(providerId,modelId)=>{
const pid=String(providerId||'configured').trim()||'configured';
const mid=String(modelId||'').trim();
if(!mid) return;
if(!grouped.has(pid)) grouped.set(pid,[]);
const arr=grouped.get(pid);
if(arr.some(m=>m.id===mid)) return;
arr.push({id:mid,label:getModelLabel(mid)});
};
for(const [modelId,badge] of Object.entries(badgeMap)){
const mid=String(modelId||'').trim();
// Prefer canonical IDs only; skip derived aliases such as
// @provider:model and provider/model to avoid noisy duplicates.
if(!mid||mid.startsWith('@')||mid.includes('/')) continue;
const provider=(badge&&badge.provider)||'configured';
addModel(provider,mid);
}
if(grouped.size===0&&data&&data.default_model){
addModel(data.active_provider||'configured',data.default_model);
}
const groups=[];
for(const [providerId,models] of grouped.entries()){
const display=(String(providerId).startsWith('custom:')
? String(providerId).slice('custom:'.length)
: String(providerId))||'Configured';
groups.push({provider:display,provider_id:providerId,models});
}
return groups;
};
const groups=(Array.isArray(data.groups)&&data.groups.length)
? data.groups
: _synthGroupsFromConfigured();
if(!groups.length) return; // no server groups and no configured fallback
// Clear existing options
sel.innerHTML='';
_dynamicModelLabels={};
for(const g of data.groups){
for(const g of groups){
const og=document.createElement('optgroup');
og.label=g.provider;
if(g.provider_id) og.dataset.provider=g.provider_id;
for(const m of g.models){
for(const m of (Array.isArray(g.models)?g.models:[])){
const opt=document.createElement('option');
opt.value=m.id;
opt.textContent=m.label;
og.appendChild(opt);
_dynamicModelLabels[m.id]=m.label;
_dynamicModelLabels[m.id]=m.id;
}
// Hydrate the label map from extra_models too (the catalog tail that
// doesn't render as <option> entries when the picker is capped — see
@@ -793,7 +834,7 @@ async function populateModelDropdown(){
// instead of falling back to the bare ID. #1567.
if(Array.isArray(g.extra_models)){
for(const m of g.extra_models){
if(m && m.id) _dynamicModelLabels[m.id]=m.label||m.id;
if(m && m.id) _dynamicModelLabels[m.id]=m.id;
}
}
sel.appendChild(og);
@@ -1033,11 +1074,19 @@ function renderModelDropdown(){
if(child.tagName==='OPTGROUP'){
const providerId=child.dataset&&child.dataset.provider?child.dataset.provider:'';
for(const opt of Array.from(child.children)){
_modelData.push({value:opt.value,name:esc(opt.textContent||getModelLabel(opt.value)),id:esc(opt.value),group:child.label||'',badge:_getConfiguredModelBadge(opt.value,_badgeMap,providerId)});
const rawValue=String(opt.value||'');
const displayName=rawValue.startsWith('@custom:')
? getModelLabel(rawValue)
: (opt.textContent||getModelLabel(rawValue));
_modelData.push({value:opt.value,name:esc(displayName),id:esc(opt.value),group:child.label||'',badge:_getConfiguredModelBadge(opt.value,_badgeMap,providerId)});
}
}
if(child.tagName==='OPTION'){
_modelData.push({value:child.value,name:esc(child.textContent||getModelLabel(child.value)),id:esc(child.value),group:'',badge:_getConfiguredModelBadge(child.value,_badgeMap)});
const rawValue=String(child.value||'');
const displayName=rawValue.startsWith('@custom:')
? getModelLabel(rawValue)
: (child.textContent||getModelLabel(rawValue));
_modelData.push({value:child.value,name:esc(displayName),id:esc(child.value),group:'',badge:_getConfiguredModelBadge(child.value,_badgeMap)});
}
}
const _existingConfiguredKeys=new Set(_modelData.map(existing=>_normalizeConfiguredModelKey(existing.value)));
@@ -1091,8 +1140,32 @@ function renderModelDropdown(){
}
}
const matches=(m)=>!term||found.has(m.value);
const configuredModels=_modelData
.filter(m=>m.badge&&matches(m))
const configuredCandidates=_modelData
.filter(m=>m.badge&&matches(m));
const configuredBySemanticKey=new Map();
const _configuredProviderKey=(m)=>String((m&&m.badge&&m.badge.provider)||_providerFromModelValue(m&&m.value)||'').toLowerCase();
const _configuredModelKey=(m)=>_normalizeConfiguredModelKey(m&&m.value||'');
const _configuredDisplayPriority=(m)=>{
// Prefer plain IDs over provider-qualified aliases for readability.
const v=String((m&&m.value)||'');
if(v.startsWith('@')) return 0;
if(v.includes('/')) return 1;
return 2;
};
for(const candidate of configuredCandidates){
const semanticKey=`${_configuredProviderKey(candidate)}::${_configuredModelKey(candidate)}`;
const existing=configuredBySemanticKey.get(semanticKey);
if(!existing){
configuredBySemanticKey.set(semanticKey,candidate);
continue;
}
const candidatePriority=_configuredDisplayPriority(candidate);
const existingPriority=_configuredDisplayPriority(existing);
if(candidatePriority>existingPriority){
configuredBySemanticKey.set(semanticKey,candidate);
}
}
const configuredModels=[...configuredBySemanticKey.values()]
.sort((a,b)=>{
const configuredRankA=_configuredRank(a.badge);
const configuredRankB=_configuredRank(b.badge);
@@ -1112,17 +1185,28 @@ function renderModelDropdown(){
configuredHeading.className='model-group';
configuredHeading.textContent=t('model_group_configured')||'Configured';
dd.appendChild(configuredHeading);
// 为了显示原始ID,建立 badgeKeyMap: badge对象->原始key
const badgeKeyMap = new Map();
for(const [k, v] of Object.entries(_badgeMap)){
badgeKeyMap.set(v, k);
}
for(const m of configuredModels){
const row=document.createElement('div');
row.className='model-opt'+(m.value===sel.value?' active':'');
// Add provider info to badge label (e.g., "Primary (jingdong)")
let badgeLabel=m.badge?(m.badge.label||'Configured'):'';
if(m.badge&&m.badge.provider){
const providerName=m.badge.provider.replace(/^custom:/,'').split('/')[0];
badgeLabel+=` (${providerName})`;
let badgeLabel = '';
let modelName = m.name;
if (m.badge) {
// 直接用badge的原始key(即config.yaml里的ID
const rawId = badgeKeyMap.get(m.badge) || m.value || m.badge.label || 'Configured';
badgeLabel = rawId;
modelName = rawId; // model-opt-name直接用原始ID
if(m.badge.provider){
const providerName=m.badge.provider.replace(/^custom:/,'').split('/')[0];
badgeLabel += ` (${providerName})`;
}
}
const badgeHtml=m.badge?`<span class="model-opt-badge model-opt-badge--${esc(m.badge.role||'configured')}">${esc(badgeLabel)}</span>`:'';
row.innerHTML=`<div class="model-opt-top"><span class="model-opt-name">${m.name}</span>${badgeHtml}</div><span class="model-opt-id">${m.id}</span>`;
row.innerHTML=`<div class="model-opt-top"><span class="model-opt-name">${esc(modelName)}</span>${badgeHtml}</div><span class="model-opt-id">${m.id}</span>`;
row.onclick=()=>selectModelFromDropdown(m.value);
dd.appendChild(row);
}
@@ -2086,6 +2170,17 @@ function _fmtOllamaLabel(mid){
function getModelLabel(modelId){
if(!modelId) return 'Unknown';
const rawId=String(modelId||'');
// Preserve custom gateway model IDs exactly as configured.
// Examples:
// @custom:ai_gateway:Qwen3.6-35B-A3B -> Qwen3.6-35B-A3B
// @custom:qwen397b-64k -> qwen397b-64k
if(rawId.startsWith('@custom:')){
const rest=rawId.slice('@custom:'.length);
if(rest.includes(':')) return rest.slice(rest.lastIndexOf(':')+1)||rawId;
if(rest.includes('/')) return rest.split('/').pop()||rawId;
return rest||rawId;
}
// Check dynamic labels first, then fall back to splitting the ID
if(_dynamicModelLabels[modelId]) return _dynamicModelLabels[modelId];
// Static fallback for common models
@@ -2096,11 +2191,15 @@ function getModelLabel(modelId){
// Strip @provider: prefix if present (e.g. @ollama-cloud:kimi-k2.6)
if (_last.startsWith('@') && _last.includes(':')) _last = _last.split(':').slice(1).join(':');
const looksLikeOllamaTag = /^[a-z0-9][\w.-]*:[\w.-]+$/i.test(_last);
const atProvider=(rawId.startsWith('@')&&rawId.includes(':'))
? rawId.slice(1,rawId.indexOf(':')).toLowerCase()
: '';
const allowOllamaFormat=!atProvider||atProvider.startsWith('ollama');
// Narrow: only apply Ollama formatter to IDs with explicit @ollama prefix or colon-tag format.
// Avoids reformatting bare provider model IDs like claude-sonnet-4-6 or gpt-4o.
const looksLikeBareOllamaId = modelId.startsWith('@ollama') || looksLikeOllamaTag;
const ollamaLabel = _fmtOllamaLabel(_last);
if ((modelId.startsWith('ollama/') || modelId.startsWith('@ollama') || looksLikeOllamaTag || looksLikeBareOllamaId) && ollamaLabel !== _last) {
if (allowOllamaFormat && (modelId.startsWith('ollama/') || modelId.startsWith('@ollama') || looksLikeOllamaTag || looksLikeBareOllamaId) && ollamaLabel !== _last) {
return ollamaLabel;
}
return _last || 'Unknown';