|
|
|
@@ -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()
|
|
|
|
|