mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 11:10:18 +00:00
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:
+115
-16
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user