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:
@@ -72,11 +72,15 @@ export async function getAvailable(ctx: any) {
|
||||
if (!cp.base_url) return null
|
||||
const providerKey = `custom:${cp.name.trim().toLowerCase().replace(/ /g, '-')}`
|
||||
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) {
|
||||
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 {
|
||||
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) {
|
||||
ctx.status = 400; ctx.body = { error: 'Missing name, base_url, or model' }; return
|
||||
}
|
||||
@@ -18,31 +19,48 @@ export async function create(ctx: any) {
|
||||
try {
|
||||
const poolKey = providerKey || `custom:${name.trim().toLowerCase().replace(/ /g, '-')}`
|
||||
const isBuiltin = poolKey in PROVIDER_ENV_MAP
|
||||
const config = await readConfigYaml()
|
||||
if (typeof config.model !== 'object' || config.model === null) { config.model = {} }
|
||||
if (!isBuiltin) {
|
||||
const config = await readConfigYaml()
|
||||
if (!Array.isArray(config.custom_providers)) { config.custom_providers = [] }
|
||||
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) {
|
||||
existing.base_url = base_url
|
||||
existing.api_key = api_key
|
||||
existing.model = model
|
||||
} 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
|
||||
if (envMapping) {
|
||||
await saveEnvValue(envMapping.api_key_env, api_key)
|
||||
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)
|
||||
delete config.model.base_url
|
||||
delete config.model.api_key
|
||||
await writeConfigYaml(config)
|
||||
try { await hermesCli.restartGateway() } catch (e: any) { logger.error(e, 'Gateway restart failed') }
|
||||
ctx.body = { success: true }
|
||||
} catch (err: any) {
|
||||
@@ -95,8 +113,8 @@ export async function remove(ctx: any) {
|
||||
if (isCustom) {
|
||||
const idx = Array.isArray(config.custom_providers)
|
||||
? (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
|
||||
if (idx === -1) {
|
||||
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) {
|
||||
const freshConfig = await readConfigYaml()
|
||||
const remaining = Array.isArray(freshConfig.custom_providers) ? freshConfig.custom_providers as any[] : []
|
||||
const fallbackCp = remaining[0]
|
||||
if (fallbackCp) {
|
||||
if (remaining.length > 0) {
|
||||
const fallbackCp = remaining[0]
|
||||
const fallbackKey = `custom:${fallbackCp.name.trim().toLowerCase().replace(/ /g, '-')}`
|
||||
if (typeof freshConfig.model !== 'object' || freshConfig.model === null) { freshConfig.model = {} }
|
||||
freshConfig.model.default = fallbackCp.model
|
||||
freshConfig.model.provider = fallbackKey
|
||||
delete freshConfig.model.base_url
|
||||
delete freshConfig.model.api_key
|
||||
await writeConfigYaml(freshConfig)
|
||||
} else {
|
||||
freshConfig.model = {}
|
||||
await writeConfigYaml(freshConfig)
|
||||
}
|
||||
}
|
||||
try { await hermesCli.restartGateway() } catch (e: any) { logger.error(e, 'Gateway restart failed') }
|
||||
ctx.body = { success: true }
|
||||
} catch (err: any) {
|
||||
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) ---
|
||||
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: '' },
|
||||
'glm-coding-plan': { api_key_env: '', 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: '' },
|
||||
moonshot: { api_key_env: 'MOONSHOT_API_KEY', base_url_env: '' },
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
{
|
||||
label: 'GLMCodingPlan',
|
||||
value: 'glm',
|
||||
label: 'GLM-Coding-Plan',
|
||||
value: 'glm-coding-plan',
|
||||
builtin: true,
|
||||
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'],
|
||||
|
||||
Reference in New Issue
Block a user