diff --git a/packages/client/src/components/hermes/models/ProviderFormModal.vue b/packages/client/src/components/hermes/models/ProviderFormModal.vue index d9f9408..aeb71b6 100644 --- a/packages/client/src/components/hermes/models/ProviderFormModal.vue +++ b/packages/client/src/components/hermes/models/ProviderFormModal.vue @@ -214,7 +214,8 @@ function handleClose() { v-model:value="formData.model" :options="modelOptions" filterable - :placeholder="t('models.selectModel')" + tag + :placeholder="t('models.selectOrInput')" style="flex: 1" /> = { openrouter: { api_key_env: 'OPENROUTER_API_KEY', base_url_env: 'OPENROUTER_BASE_URL' }, zai: { api_key_env: 'ZAI_API_KEY', base_url_env: '' }, - 'kimi-for-coding': { api_key_env: 'KIMI_API_KEY', base_url_env: '' }, + 'kimi-coding': { api_key_env: 'KIMI_API_KEY', base_url_env: '' }, + 'kimi-coding-cn': { api_key_env: 'KIMI_API_KEY', base_url_env: '' }, + moonshot: { api_key_env: 'MOONSHOT_API_KEY', base_url_env: 'MOONSHOT_BASE_URL' }, minimax: { api_key_env: 'MINIMAX_API_KEY', base_url_env: 'MINIMAX_BASE_URL' }, 'minimax-cn': { api_key_env: 'MINIMAX_API_KEY', base_url_env: 'MINIMAX_CN_BASE_URL' }, deepseek: { api_key_env: 'DEEPSEEK_API_KEY', base_url_env: 'DEEPSEEK_BASE_URL' }, @@ -19,11 +21,12 @@ const PROVIDER_ENV_MAP: Record { @@ -415,7 +418,6 @@ interface ModelGroup { // Build model list from user's actual config.yaml using js-yaml function buildModelGroups(config: Record): { default: string; groups: ModelGroup[] } { let defaultModel = '' - let defaultProvider = '' const groups: ModelGroup[] = [] const allModelIds = new Set() @@ -423,7 +425,6 @@ function buildModelGroups(config: Record): { default: string; group const modelSection = config.model if (typeof modelSection === 'object' && modelSection !== null) { defaultModel = String(modelSection.default || '').trim() - defaultProvider = String(modelSection.provider || '').trim() } else if (typeof modelSection === 'string') { defaultModel = modelSection.trim() } @@ -539,7 +540,51 @@ fsRoutes.get('/api/hermes/available-models', async (ctx) => { } } - // Fallback: if no providers returned models, fall back to config.yaml parsing + // Merge custom_providers from config.yaml (ensures manually-input model names appear) + const customProviders = Array.isArray(config.custom_providers) + ? config.custom_providers as Array<{ name: string; base_url: string; model: string }> + : [] + for (const cp of customProviders) { + if (!cp.base_url || !cp.model) continue + const baseUrl = cp.base_url.replace(/\/+$/, '') + // Check if we already have a group for this base_url + const existing = dedupedGroups.find(g => g.base_url.replace(/\/+$/, '') === baseUrl) + if (existing) { + if (!existing.models.includes(cp.model)) { + existing.models.push(cp.model) + } + } else { + dedupedGroups.push({ + provider: `custom:${cp.name.trim().toLowerCase().replace(/ /g, '-')}`, + label: cp.name, + base_url: baseUrl, + models: [cp.model], + }) + } + } + + // Ensure config's current default model appears in the model list + if (currentDefault) { + const currentProvider = typeof config.model === 'object' ? String(config.model.provider || '').trim() : '' + if (currentProvider) { + const targetGroup = dedupedGroups.find(g => g.provider === currentProvider) + if (targetGroup && !targetGroup.models.includes(currentDefault)) { + targetGroup.models.unshift(currentDefault) + } + } else { + // No provider specified — add to the first group that matches via base_url + // or just prepend to all groups + let found = false + for (const g of dedupedGroups) { + if (!found && !g.models.includes(currentDefault)) { + g.models.unshift(currentDefault) + found = true + } + } + } + } + + // Fallback: if still no providers, fall back to config.yaml parsing if (dedupedGroups.length === 0) { const fallback = buildModelGroups(config) ctx.body = fallback @@ -702,22 +747,29 @@ fsRoutes.delete('/api/hermes/config/providers/:poolKey', async (ctx) => { return } + // Case-insensitive key lookup: normalize poolKey to match credential_pool + let resolvedKey = poolKey if (!(poolKey in auth.credential_pool)) { - ctx.status = 404 - ctx.body = { error: `Provider "${poolKey}" not found` } - return + const normalized = poolKey.toLowerCase() + const match = Object.keys(auth.credential_pool).find(k => k.toLowerCase() === normalized) + if (!match) { + ctx.status = 404 + ctx.body = { error: `Provider "${poolKey}" not found` } + return + } + resolvedKey = match } // Check if this is the current active provider const config = await readConfigYaml() const currentProvider = config.model?.provider - const isCurrent = currentProvider === poolKey + const isCurrent = currentProvider === poolKey || currentProvider === resolvedKey // Save base_url before deleting - const deletedBaseUrl = auth.credential_pool[poolKey]?.[0]?.base_url + const deletedBaseUrl = auth.credential_pool[resolvedKey]?.[0]?.base_url // 1. Delete from auth.json - delete auth.credential_pool[poolKey] + delete auth.credential_pool[resolvedKey] await saveAuthJson(auth) // 2. Remove matching entry from config.yaml custom_providers diff --git a/packages/server/src/shared/providers.ts b/packages/server/src/shared/providers.ts index f8a757c..72d3904 100644 --- a/packages/server/src/shared/providers.ts +++ b/packages/server/src/shared/providers.ts @@ -16,6 +16,7 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [ value: 'anthropic', base_url: 'https://api.anthropic.com', models: [ + 'claude-opus-4-7', 'claude-opus-4-6', 'claude-sonnet-4-6', 'claude-opus-4-5-20251101', @@ -50,11 +51,11 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [ label: 'Z.AI / GLM', value: 'zai', base_url: 'https://api.z.ai/api/paas/v4', - models: ['glm-5', '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: 'Kimi for Coding', - value: 'kimi-for-coding', + value: 'kimi-coding', base_url: 'https://api.kimi.com/coding/v1', models: [ 'kimi-for-coding', @@ -65,22 +66,33 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [ 'kimi-k2-0905-preview', ], }, + { + label: 'Kimi for Coding (CN)', + value: 'kimi-coding-cn', + base_url: 'https://api.kimi.com/coding/v1', + models: [ + 'kimi-k2.5', + 'kimi-k2-thinking', + 'kimi-k2-turbo-preview', + 'kimi-k2-0905-preview', + ], + }, + { + label: 'Moonshot', + value: 'moonshot', + base_url: 'https://api.moonshot.cn/v1', + models: [ + 'kimi-k2.5', + 'kimi-k2-thinking', + 'kimi-k2-turbo-preview', + 'kimi-k2-0905-preview', + ], + }, { label: 'xAI', value: 'xai', base_url: 'https://api.x.ai/v1', - models: [ - 'grok-4.20-0309-reasoning', - 'grok-4.20-0309-non-reasoning', - 'grok-4-1-fast-reasoning', - 'grok-4-1-fast-non-reasoning', - 'grok-4-fast-reasoning', - 'grok-4-fast-non-reasoning', - 'grok-4-0709', - 'grok-code-fast-1', - 'grok-3', - 'grok-3-mini', - ], + models: ['grok-4.20-reasoning', 'grok-4-1-fast-reasoning'], }, { label: 'MiniMax', @@ -131,7 +143,7 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [ }, { label: 'Kilo Code', - value: 'kilo', + value: 'kilocode', base_url: 'https://api.kilo.ai/api/gateway', models: [ 'anthropic/claude-opus-4.6', @@ -143,7 +155,7 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [ }, { label: 'Vercel AI Gateway', - value: 'vercel', + value: 'ai-gateway', base_url: 'https://ai-gateway.vercel.sh/v1', models: [ 'anthropic/claude-opus-4.6', @@ -162,32 +174,58 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [ }, { label: 'OpenCode Zen', - value: 'opencode', + value: 'opencode-zen', base_url: 'https://opencode.ai/zen/v1', models: [ 'gpt-5.4-pro', 'gpt-5.4', 'gpt-5.3-codex', + 'gpt-5.3-codex-spark', 'gpt-5.2', + 'gpt-5.2-codex', 'gpt-5.1', + 'gpt-5.1-codex', + 'gpt-5.1-codex-max', + 'gpt-5.1-codex-mini', + 'gpt-5', + 'gpt-5-codex', + 'gpt-5-nano', 'claude-opus-4-6', + 'claude-opus-4-5', + 'claude-opus-4-1', 'claude-sonnet-4-6', + 'claude-sonnet-4-5', + 'claude-sonnet-4', 'claude-haiku-4-5', + 'claude-3-5-haiku', 'gemini-3.1-pro', 'gemini-3-pro', 'gemini-3-flash', 'minimax-m2.7', 'minimax-m2.5', + 'minimax-m2.5-free', + 'minimax-m2.1', 'glm-5', 'glm-4.7', + 'glm-4.6', 'kimi-k2.5', + 'kimi-k2-thinking', + 'kimi-k2', + 'qwen3-coder', + 'big-pickle', ], }, { label: 'OpenCode Go', value: 'opencode-go', base_url: 'https://opencode.ai/zen/go/v1', - models: ['glm-5', 'kimi-k2.5', 'mimo-v2-pro', 'mimo-v2-omni', 'minimax-m2.7', 'minimax-m2.5'], + models: ['glm-5.1', 'glm-5', 'kimi-k2.5', 'mimo-v2-pro', 'mimo-v2-omni', 'minimax-m2.7', 'minimax-m2.5'], + }, + { + label: 'Arcee AI', + value: 'arcee', + base_url: 'https://api.arcee.ai/v1', + models: ['trinity-large-thinking', 'trinity-large-preview', 'trinity-mini'], }, { label: 'OpenRouter',