fix(models): fix builtin provider detection and model matching (#120)

- Add glm-coding-plan to PROVIDER_ENV_MAP for proper env mapping
- Rename GLMCodingPlan value from 'glm' to 'glm-coding-plan' (kebab-case)
- Match custom providers against PROVIDER_PRESETS to reuse builtin models
- Fix provider key matching in create/update (use entry.name consistently)
- Clear stale base_url/api_key from config on provider create
- Clear model config when all providers are removed
- Add gateway restart on provider remove

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ekko
2026-04-22 00:11:39 +08:00
committed by GitHub
parent c4bea63a5e
commit 83ad9642e2
4 changed files with 51 additions and 22 deletions
@@ -72,11 +72,15 @@ export async function getAvailable(ctx: any) {
if (!cp.base_url) return null if (!cp.base_url) return null
const providerKey = `custom:${cp.name.trim().toLowerCase().replace(/ /g, '-')}` const providerKey = `custom:${cp.name.trim().toLowerCase().replace(/ /g, '-')}`
const baseUrl = cp.base_url.replace(/\/+$/, '') const baseUrl = cp.base_url.replace(/\/+$/, '')
let models = [cp.model] const bareKey = cp.name.trim().toLowerCase().replace(/ /g, '-')
const builtinPreset = PROVIDER_PRESETS.find(p => p.value === bareKey)
let models = builtinPreset?.models?.length ? [...builtinPreset.models] : [cp.model]
if (cp.api_key) { if (cp.api_key) {
try { const fetched = await fetchProviderModels(baseUrl, cp.api_key); if (fetched.length > 0) models = fetched } catch { } try { const fetched = await fetchProviderModels(baseUrl, cp.api_key); if (fetched.length > 0) models = fetched } catch { }
} }
return { providerKey, label: cp.name, base_url: baseUrl, models, api_key: cp.api_key || '' } const label = builtinPreset?.label || cp.name
const presetBaseUrl = builtinPreset?.base_url || ''
return { providerKey, label, base_url: presetBaseUrl || baseUrl, models, api_key: cp.api_key || '' }
}), }),
) )
@@ -9,6 +9,7 @@ export async function create(ctx: any) {
const { name, base_url, api_key, model, providerKey } = ctx.request.body as { const { name, base_url, api_key, model, providerKey } = ctx.request.body as {
name: string; base_url: string; api_key: string; model: string; providerKey?: string | null name: string; base_url: string; api_key: string; model: string; providerKey?: string | null
} }
console.log(name, base_url, api_key, model, providerKey)
if (!name || !base_url || !model) { if (!name || !base_url || !model) {
ctx.status = 400; ctx.body = { error: 'Missing name, base_url, or model' }; return ctx.status = 400; ctx.body = { error: 'Missing name, base_url, or model' }; return
} }
@@ -18,31 +19,48 @@ export async function create(ctx: any) {
try { try {
const poolKey = providerKey || `custom:${name.trim().toLowerCase().replace(/ /g, '-')}` const poolKey = providerKey || `custom:${name.trim().toLowerCase().replace(/ /g, '-')}`
const isBuiltin = poolKey in PROVIDER_ENV_MAP const isBuiltin = poolKey in PROVIDER_ENV_MAP
const config = await readConfigYaml()
if (typeof config.model !== 'object' || config.model === null) { config.model = {} }
if (!isBuiltin) { if (!isBuiltin) {
const config = await readConfigYaml()
if (!Array.isArray(config.custom_providers)) { config.custom_providers = [] } if (!Array.isArray(config.custom_providers)) { config.custom_providers = [] }
const existing = (config.custom_providers as any[]).find( const existing = (config.custom_providers as any[]).find(
(e: any) => `custom:${e.name.trim().toLowerCase().replace(/ /g, '-')}` === poolKey (e: any) => `custom:${e.name}` === poolKey
) )
if (existing) { if (existing) {
existing.base_url = base_url existing.base_url = base_url
existing.api_key = api_key existing.api_key = api_key
existing.model = model existing.model = model
} else { } else {
config.custom_providers.push({ name, base_url, api_key, model }) config.custom_providers.push({ name: name.trim().toLowerCase().replace(/ /g, '-'), base_url, api_key, model })
}
config.model.default = model
config.model.provider = poolKey
} else {
console.log(PROVIDER_ENV_MAP[poolKey])
if (PROVIDER_ENV_MAP[poolKey].api_key_env) {
await saveEnvValue(PROVIDER_ENV_MAP[poolKey].api_key_env, api_key)
if (PROVIDER_ENV_MAP[poolKey].base_url_env) { await saveEnvValue(PROVIDER_ENV_MAP[poolKey].base_url_env, base_url) }
config.model.default = model
config.model.provider = poolKey
} else {
if (!Array.isArray(config.custom_providers)) { config.custom_providers = [] }
const existing = (config.custom_providers as any[]).find(
(e: any) => `custom:${e.name}` === `custom:${poolKey}`
)
if (existing) {
existing.base_url = base_url
existing.api_key = api_key
existing.model = model
} else {
config.custom_providers.push({ name: poolKey, base_url, api_key, model })
}
config.model.default = model
config.model.provider = `custom:${poolKey}`
} }
await writeConfigYaml(config)
} }
const envMapping = isBuiltin ? (PROVIDER_ENV_MAP[poolKey] || PROVIDER_ENV_MAP[providerKey || '']) : null delete config.model.base_url
if (envMapping) { delete config.model.api_key
await saveEnvValue(envMapping.api_key_env, api_key) await writeConfigYaml(config)
if (envMapping.base_url_env) { await saveEnvValue(envMapping.base_url_env, base_url) }
}
const config2 = await readConfigYaml()
if (typeof config2.model !== 'object' || config2.model === null) { config2.model = {} }
config2.model.default = model
config2.model.provider = poolKey
await writeConfigYaml(config2)
try { await hermesCli.restartGateway() } catch (e: any) { logger.error(e, 'Gateway restart failed') } try { await hermesCli.restartGateway() } catch (e: any) { logger.error(e, 'Gateway restart failed') }
ctx.body = { success: true } ctx.body = { success: true }
} catch (err: any) { } catch (err: any) {
@@ -95,8 +113,8 @@ export async function remove(ctx: any) {
if (isCustom) { if (isCustom) {
const idx = Array.isArray(config.custom_providers) const idx = Array.isArray(config.custom_providers)
? (config.custom_providers as any[]).findIndex((e: any) => { ? (config.custom_providers as any[]).findIndex((e: any) => {
return `custom:${e.name.trim().toLowerCase().replace(/ /g, '-')}` === poolKey return `custom:${e.name.trim().toLowerCase().replace(/ /g, '-')}` === poolKey
}) })
: -1 : -1
if (idx === -1) { if (idx === -1) {
ctx.status = 404; ctx.body = { error: `Custom provider "${poolKey}" not found` }; return ctx.status = 404; ctx.body = { error: `Custom provider "${poolKey}" not found` }; return
@@ -123,15 +141,21 @@ export async function remove(ctx: any) {
if (currentProvider === poolKey) { if (currentProvider === poolKey) {
const freshConfig = await readConfigYaml() const freshConfig = await readConfigYaml()
const remaining = Array.isArray(freshConfig.custom_providers) ? freshConfig.custom_providers as any[] : [] const remaining = Array.isArray(freshConfig.custom_providers) ? freshConfig.custom_providers as any[] : []
const fallbackCp = remaining[0] if (remaining.length > 0) {
if (fallbackCp) { const fallbackCp = remaining[0]
const fallbackKey = `custom:${fallbackCp.name.trim().toLowerCase().replace(/ /g, '-')}` const fallbackKey = `custom:${fallbackCp.name.trim().toLowerCase().replace(/ /g, '-')}`
if (typeof freshConfig.model !== 'object' || freshConfig.model === null) { freshConfig.model = {} } if (typeof freshConfig.model !== 'object' || freshConfig.model === null) { freshConfig.model = {} }
freshConfig.model.default = fallbackCp.model freshConfig.model.default = fallbackCp.model
freshConfig.model.provider = fallbackKey freshConfig.model.provider = fallbackKey
delete freshConfig.model.base_url
delete freshConfig.model.api_key
await writeConfigYaml(freshConfig)
} else {
freshConfig.model = {}
await writeConfigYaml(freshConfig) await writeConfigYaml(freshConfig)
} }
} }
try { await hermesCli.restartGateway() } catch (e: any) { logger.error(e, 'Gateway restart failed') }
ctx.body = { success: true } ctx.body = { success: true }
} catch (err: any) { } catch (err: any) {
ctx.status = 500; ctx.body = { error: err.message } ctx.status = 500; ctx.body = { error: err.message }
@@ -9,6 +9,7 @@ import { logger } from './logger'
// --- Provider env var mapping (from hermes providers.py HERMES_OVERLAYS + config.py) --- // --- Provider env var mapping (from hermes providers.py HERMES_OVERLAYS + config.py) ---
export const PROVIDER_ENV_MAP: Record<string, { api_key_env: string; base_url_env: string }> = { export const PROVIDER_ENV_MAP: Record<string, { api_key_env: string; base_url_env: string }> = {
openrouter: { api_key_env: 'OPENROUTER_API_KEY', base_url_env: '' }, openrouter: { api_key_env: 'OPENROUTER_API_KEY', base_url_env: '' },
'glm-coding-plan': { api_key_env: '', base_url_env: '' },
zai: { api_key_env: 'GLM_API_KEY', base_url_env: '' }, zai: { api_key_env: 'GLM_API_KEY', base_url_env: '' },
'kimi-coding-cn': { api_key_env: 'KIMI_CN_API_KEY', base_url_env: '' }, 'kimi-coding-cn': { api_key_env: 'KIMI_CN_API_KEY', base_url_env: '' },
moonshot: { api_key_env: 'MOONSHOT_API_KEY', base_url_env: '' }, moonshot: { api_key_env: 'MOONSHOT_API_KEY', base_url_env: '' },
+2 -2
View File
@@ -59,8 +59,8 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [
models: ['glm-5.1', 'glm-5', 'glm-5v-turbo', 'glm-5-turbo', 'glm-4.7', 'glm-4.5', 'glm-4.5-flash'], models: ['glm-5.1', 'glm-5', 'glm-5v-turbo', 'glm-5-turbo', 'glm-4.7', 'glm-4.5', 'glm-4.5-flash'],
}, },
{ {
label: 'GLMCodingPlan', label: 'GLM-Coding-Plan',
value: 'glm', value: 'glm-coding-plan',
builtin: true, builtin: true,
base_url: 'https://api.z.ai/api/anthropic', base_url: 'https://api.z.ai/api/anthropic',
models: ['glm-5.1', 'glm-5', 'glm-5-turbo', 'glm-4.7', 'glm-4.5', 'glm-4.5-flash'], models: ['glm-5.1', 'glm-5', 'glm-5-turbo', 'glm-4.7', 'glm-4.5', 'glm-4.5-flash'],