feat(models): 增加模型显示名重命名 (#614)
* feat(models): add WUI model display aliases Persist display-only model aliases in Web UI app config, surface them in the model selector/search, and keep canonical model IDs for Hermes calls. * fix(models): improve WUI model alias editing * fix(models): clarify unlisted model picker * fix(models): scope aliases to providers
This commit is contained in:
@@ -10,10 +10,57 @@ import { MODEL_CONTEXT_TABLE } from '../../db/hermes/schemas'
|
||||
|
||||
const PROVIDER_MODEL_CATALOG = buildProviderModelMap()
|
||||
|
||||
type ModelMeta = { preview?: boolean; disabled?: boolean }
|
||||
type ModelMeta = { preview?: boolean; disabled?: boolean; alias?: string }
|
||||
type AvailableGroup = { provider: string; label: string; base_url: string; models: string[]; api_key: string; builtin?: boolean; model_meta?: Record<string, ModelMeta>; available_models?: string[] }
|
||||
type ModelVisibility = Record<string, ModelVisibilityRule>
|
||||
|
||||
const RESERVED_ALIAS_KEYS = new Set(['__proto__', 'prototype', 'constructor'])
|
||||
|
||||
function isSafeAliasKey(value: string): boolean {
|
||||
const trimmed = value.trim()
|
||||
return !!trimmed && trimmed.length <= 512 && !RESERVED_ALIAS_KEYS.has(trimmed)
|
||||
}
|
||||
|
||||
function createAliasMap(): Record<string, string> {
|
||||
return Object.create(null) as Record<string, string>
|
||||
}
|
||||
|
||||
function createProviderAliasMap(): Record<string, Record<string, string>> {
|
||||
return Object.create(null) as Record<string, Record<string, string>>
|
||||
}
|
||||
|
||||
function normalizeAliases(value: unknown): Record<string, Record<string, string>> {
|
||||
const normalized = createProviderAliasMap()
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return normalized
|
||||
for (const [provider, models] of Object.entries(value as Record<string, unknown>)) {
|
||||
if (!isSafeAliasKey(provider) || !models || typeof models !== 'object' || Array.isArray(models)) continue
|
||||
for (const [model, alias] of Object.entries(models as Record<string, unknown>)) {
|
||||
if (!isSafeAliasKey(model) || typeof alias !== 'string') continue
|
||||
const trimmed = alias.trim()
|
||||
if (!trimmed || trimmed.length > 512) continue
|
||||
if (!Object.hasOwn(normalized, provider)) normalized[provider] = createAliasMap()
|
||||
normalized[provider][model] = trimmed
|
||||
}
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
function applyModelAliases<T extends { provider: string; models: string[]; model_meta?: Record<string, ModelMeta> }>(groups: T[], aliases: Record<string, Record<string, string>>): T[] {
|
||||
return groups.map((group) => {
|
||||
const providerAliases = aliases[group.provider]
|
||||
if (!providerAliases) return group
|
||||
const modelMeta: Record<string, ModelMeta> = { ...(group.model_meta || {}) }
|
||||
let changed = false
|
||||
for (const model of group.models) {
|
||||
const alias = providerAliases[model]
|
||||
if (!alias) continue
|
||||
modelMeta[model] = { ...(modelMeta[model] || {}), alias }
|
||||
changed = true
|
||||
}
|
||||
return changed ? { ...group, model_meta: modelMeta } : group
|
||||
})
|
||||
}
|
||||
|
||||
function uniqueStrings(values: unknown): string[] {
|
||||
if (!Array.isArray(values)) return []
|
||||
return Array.from(new Set(values.map(v => String(v || '').trim()).filter(Boolean)))
|
||||
@@ -158,6 +205,7 @@ export async function getAvailable(ctx: any) {
|
||||
// 时也不返回。避免误把 VS Code/gh CLI 用户的全局凭证当作 hermes provider。
|
||||
const appConfig = await readAppConfig()
|
||||
const copilotEnabled = appConfig.copilotEnabled === true
|
||||
const modelAliases = normalizeAliases(appConfig.modelAliases)
|
||||
const modelVisibility = normalizeModelVisibility(appConfig.modelVisibility)
|
||||
|
||||
// 兼容老用户:上一版本会"自动 fallback discovery"出 Copilot;升级后这些用户的
|
||||
@@ -186,7 +234,7 @@ export async function getAvailable(ctx: any) {
|
||||
}
|
||||
const catalogModels = PROVIDER_MODEL_CATALOG[providerKey]
|
||||
let modelsList: string[] = catalogModels && catalogModels.length > 0 ? [...catalogModels] : []
|
||||
let modelMeta: Record<string, { preview?: boolean; disabled?: boolean }> | undefined
|
||||
let modelMeta: Record<string, ModelMeta> | undefined
|
||||
if (providerKey === 'copilot') {
|
||||
const live = await getCopilotLive()
|
||||
if (live.length > 0) {
|
||||
@@ -250,7 +298,8 @@ export async function getAvailable(ctx: any) {
|
||||
}
|
||||
|
||||
for (const g of groups) { g.models = Array.from(new Set(g.models)) }
|
||||
const visibleGroups = applyModelVisibility(groups, modelVisibility)
|
||||
const groupsWithAliases = applyModelAliases(groups, modelAliases)
|
||||
const visibleGroups = applyModelVisibility(groupsWithAliases, modelVisibility)
|
||||
const visibleDefault = resolveVisibleDefault(currentDefault, currentDefaultProvider, visibleGroups)
|
||||
|
||||
// 动态拉一次 copilot 模型用于 allProviders 展示(同一请求复用缓存)
|
||||
@@ -264,6 +313,7 @@ export async function getAvailable(ctx: any) {
|
||||
base_url: p.base_url,
|
||||
models: p.value === 'copilot' && liveCopilotIds.length > 0 ? liveCopilotIds : p.models,
|
||||
}))
|
||||
const allProviders = applyModelAliases(allProvidersBase, modelAliases)
|
||||
|
||||
if (groups.length === 0) {
|
||||
const fallback = buildModelGroups(config)
|
||||
@@ -278,13 +328,15 @@ export async function getAvailable(ctx: any) {
|
||||
api_key: '',
|
||||
}
|
||||
})
|
||||
const visibleFallbackGroups = applyModelVisibility(fallbackGroups, modelVisibility)
|
||||
const fallbackGroupsWithAliases = applyModelAliases(fallbackGroups, modelAliases)
|
||||
const visibleFallbackGroups = applyModelVisibility(fallbackGroupsWithAliases, modelVisibility)
|
||||
const fallbackDefault = resolveVisibleDefault(fallback.default, currentDefaultProvider, visibleFallbackGroups)
|
||||
ctx.body = {
|
||||
default: fallbackDefault.defaultModel,
|
||||
default_provider: fallbackDefault.defaultProvider,
|
||||
groups: visibleFallbackGroups,
|
||||
allProviders: allProvidersBase,
|
||||
allProviders,
|
||||
model_aliases: modelAliases,
|
||||
model_visibility: modelVisibility,
|
||||
}
|
||||
return
|
||||
@@ -294,7 +346,8 @@ export async function getAvailable(ctx: any) {
|
||||
default: visibleDefault.defaultModel,
|
||||
default_provider: visibleDefault.defaultProvider,
|
||||
groups: visibleGroups,
|
||||
allProviders: allProvidersBase,
|
||||
allProviders,
|
||||
model_aliases: modelAliases,
|
||||
model_visibility: modelVisibility,
|
||||
}
|
||||
} catch (err: any) {
|
||||
@@ -303,6 +356,55 @@ export async function getAvailable(ctx: any) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function setModelAlias(ctx: any) {
|
||||
const body = ctx.request.body
|
||||
const provider = body && typeof body === 'object' && !Array.isArray(body) ? body.provider : undefined
|
||||
const model = body && typeof body === 'object' && !Array.isArray(body) ? body.model : undefined
|
||||
const alias = body && typeof body === 'object' && !Array.isArray(body) ? body.alias : undefined
|
||||
|
||||
if (typeof provider !== 'string' || typeof model !== 'string' || (alias !== undefined && typeof alias !== 'string')) {
|
||||
ctx.status = 400
|
||||
ctx.body = { error: 'Invalid provider, model, or alias' }
|
||||
return
|
||||
}
|
||||
|
||||
const cleanProvider = provider.trim()
|
||||
const cleanModel = model.trim()
|
||||
const cleanAlias = (alias || '').trim()
|
||||
|
||||
if (!isSafeAliasKey(cleanProvider) || !isSafeAliasKey(cleanModel)) {
|
||||
ctx.status = 400
|
||||
ctx.body = { error: 'Invalid provider or model' }
|
||||
return
|
||||
}
|
||||
|
||||
if (cleanAlias.length > 512) {
|
||||
ctx.status = 400
|
||||
ctx.body = { error: 'Alias is too long' }
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const appConfig = await readAppConfig()
|
||||
const modelAliases = normalizeAliases(appConfig.modelAliases)
|
||||
if (cleanAlias) {
|
||||
if (!Object.hasOwn(modelAliases, cleanProvider)) modelAliases[cleanProvider] = createAliasMap()
|
||||
modelAliases[cleanProvider][cleanModel] = cleanAlias
|
||||
} else {
|
||||
if (Object.hasOwn(modelAliases, cleanProvider)) delete modelAliases[cleanProvider][cleanModel]
|
||||
if (Object.hasOwn(modelAliases, cleanProvider) && Object.keys(modelAliases[cleanProvider]).length === 0) {
|
||||
delete modelAliases[cleanProvider]
|
||||
}
|
||||
}
|
||||
await writeAppConfig({ modelAliases })
|
||||
ctx.body = { success: true, model_aliases: modelAliases }
|
||||
} catch (err: any) {
|
||||
ctx.status = 500
|
||||
ctx.body = { error: err.message }
|
||||
}
|
||||
}
|
||||
|
||||
export async function getConfigModels(ctx: any) {
|
||||
try {
|
||||
const config = await readConfigYaml()
|
||||
|
||||
Reference in New Issue
Block a user