From 53dbe4b2b5fcee9b2df69bc54681652c2fa7f171 Mon Sep 17 00:00:00 2001 From: ekko <152005280+EKKOLearnAI@users.noreply.github.com> Date: Thu, 7 May 2026 22:16:52 +0800 Subject: [PATCH] chore: update FUN-Codex and FUN-Claude provider models (#522) FUN-Codex: add GPT models (5.5, 5.4, 5.4-mini, 5.3-codex, 5.3-codex-spark) FUN-Claude: replace with actual Claude models from API (opus-4-7 down to 3-5-haiku) Co-authored-by: Claude Opus 4.7 --- packages/client/src/api/hermes/system.ts | 1 + .../components/hermes/models/ProviderCard.vue | 2 +- .../hermes/models/ProviderFormModal.vue | 36 +++++++++++++++++++ .../hermes/settings/ModelSettings.vue | 5 ++- .../src/components/layout/AppSidebar.vue | 8 +++++ packages/client/src/i18n/locales/de.ts | 1 + packages/client/src/i18n/locales/en.ts | 2 ++ packages/client/src/i18n/locales/es.ts | 1 + packages/client/src/i18n/locales/fr.ts | 1 + packages/client/src/i18n/locales/ja.ts | 1 + packages/client/src/i18n/locales/ko.ts | 1 + packages/client/src/i18n/locales/pt.ts | 1 + packages/client/src/i18n/locales/zh.ts | 2 ++ .../client/src/views/hermes/ModelsView.vue | 1 + .../server/src/controllers/hermes/models.ts | 17 ++++----- .../src/controllers/hermes/providers.ts | 16 +++++++-- .../server/src/services/config-helpers.ts | 2 ++ packages/server/src/shared/providers.ts | 27 ++++++++++++++ 18 files changed, 112 insertions(+), 13 deletions(-) diff --git a/packages/client/src/api/hermes/system.ts b/packages/client/src/api/hermes/system.ts index 920c3488..c3589511 100644 --- a/packages/client/src/api/hermes/system.ts +++ b/packages/client/src/api/hermes/system.ts @@ -31,6 +31,7 @@ export interface AvailableModelGroup { base_url: string models: string[] api_key: string + builtin?: boolean /** 可选:模型 ID -> 元数据(preview/disabled)。目前仅 Copilot 提供。 */ model_meta?: Record } diff --git a/packages/client/src/components/hermes/models/ProviderCard.vue b/packages/client/src/components/hermes/models/ProviderCard.vue index b59f889b..2dc9d02e 100644 --- a/packages/client/src/components/hermes/models/ProviderCard.vue +++ b/packages/client/src/components/hermes/models/ProviderCard.vue @@ -17,7 +17,7 @@ const chatStore = useChatStore() const message = useMessage() const dialog = useDialog() -const isCustom = computed(() => props.provider.provider.startsWith('custom:')) +const isCustom = computed(() => !props.provider.builtin && props.provider.provider.startsWith('custom:')) const isCopilot = computed(() => props.provider.provider === 'copilot') const displayName = computed(() => props.provider.label) const deleting = ref(false) diff --git a/packages/client/src/components/hermes/models/ProviderFormModal.vue b/packages/client/src/components/hermes/models/ProviderFormModal.vue index 2847ad4b..9840d415 100644 --- a/packages/client/src/components/hermes/models/ProviderFormModal.vue +++ b/packages/client/src/components/hermes/models/ProviderFormModal.vue @@ -60,6 +60,13 @@ const presetOptions = computed(() => modelsStore.allProviders.map(g => ({ label: g.label, value: g.provider })), ) +const FUN_LINK_MAP: Record = { + 'fun-codex': 'https://apikey.fun/register?aff=LIBAPI', + 'fun-claude': 'https://apikey.fun/register?aff=LIBAPI', +} + +const funProviderLink = computed(() => selectedPreset.value ? FUN_LINK_MAP[selectedPreset.value] || '' : '') + function autoGenerateName(url: string): string { const clean = url.replace(/^https?:\/\//, '').replace(/\/v1\/?$/, '') const host = clean.split('/')[0] @@ -322,6 +329,12 @@ function handleClose() { :placeholder="t('models.chooseProvider')" filterable /> + @@ -417,6 +430,29 @@ function handleClose() { diff --git a/packages/client/src/i18n/locales/de.ts b/packages/client/src/i18n/locales/de.ts index 050cb3f7..c64c099b 100644 --- a/packages/client/src/i18n/locales/de.ts +++ b/packages/client/src/i18n/locales/de.ts @@ -68,6 +68,7 @@ export default { sidebar: { chat: 'Chat', search: 'Suche', + apiRelay: 'API-Relay', history: 'Verlauf', jobs: 'Geplante Aufgaben', models: 'Modelle', diff --git a/packages/client/src/i18n/locales/en.ts b/packages/client/src/i18n/locales/en.ts index 5190a6bc..416b910e 100644 --- a/packages/client/src/i18n/locales/en.ts +++ b/packages/client/src/i18n/locales/en.ts @@ -70,6 +70,7 @@ export default { sidebar: { chat: 'Chat', search: 'Search', + apiRelay: 'API Relay', history: 'History', jobs: 'Jobs', models: 'Models', @@ -331,6 +332,7 @@ export default { custom: 'Custom', selectProvider: 'Select Provider', chooseProvider: 'Choose a provider...', + getApiKey: 'Get API Key', name: 'Name', autoGeneratedName: 'Auto-generated from Base URL', baseUrl: 'Base URL', diff --git a/packages/client/src/i18n/locales/es.ts b/packages/client/src/i18n/locales/es.ts index 7e4958fc..85315183 100644 --- a/packages/client/src/i18n/locales/es.ts +++ b/packages/client/src/i18n/locales/es.ts @@ -68,6 +68,7 @@ export default { sidebar: { chat: 'Chat', search: 'Buscar', + apiRelay: 'API Relay', history: 'Historial', jobs: 'Tareas programadas', models: 'Modelos', diff --git a/packages/client/src/i18n/locales/fr.ts b/packages/client/src/i18n/locales/fr.ts index d5665bec..3fb0ebfd 100644 --- a/packages/client/src/i18n/locales/fr.ts +++ b/packages/client/src/i18n/locales/fr.ts @@ -68,6 +68,7 @@ export default { sidebar: { chat: 'Discussion', search: 'Rechercher', + apiRelay: 'API Relay', history: 'Historique', jobs: 'Taches planifiees', models: 'Modeles', diff --git a/packages/client/src/i18n/locales/ja.ts b/packages/client/src/i18n/locales/ja.ts index ebdf9f88..8b0a8e13 100644 --- a/packages/client/src/i18n/locales/ja.ts +++ b/packages/client/src/i18n/locales/ja.ts @@ -68,6 +68,7 @@ export default { sidebar: { chat: 'チャット', search: '検索', + apiRelay: 'APIリレー', history: '履歴', jobs: 'ジョブ', models: 'モデル', diff --git a/packages/client/src/i18n/locales/ko.ts b/packages/client/src/i18n/locales/ko.ts index d4169783..b9c68208 100644 --- a/packages/client/src/i18n/locales/ko.ts +++ b/packages/client/src/i18n/locales/ko.ts @@ -68,6 +68,7 @@ export default { sidebar: { chat: '채팅', search: '검색', + apiRelay: 'API 릴레이', history: '기록', jobs: '예약 작업', models: '모델', diff --git a/packages/client/src/i18n/locales/pt.ts b/packages/client/src/i18n/locales/pt.ts index 2e4ef127..ab99acf0 100644 --- a/packages/client/src/i18n/locales/pt.ts +++ b/packages/client/src/i18n/locales/pt.ts @@ -68,6 +68,7 @@ export default { sidebar: { chat: 'Chat', search: 'Pesquisar', + apiRelay: 'API Relay', history: 'Historico', jobs: 'Tarefas agendadas', models: 'Modelos', diff --git a/packages/client/src/i18n/locales/zh.ts b/packages/client/src/i18n/locales/zh.ts index d24adf90..d44490b3 100644 --- a/packages/client/src/i18n/locales/zh.ts +++ b/packages/client/src/i18n/locales/zh.ts @@ -70,6 +70,7 @@ export default { sidebar: { chat: '对话', search: '搜索', + apiRelay: '中转站', history: '历史', jobs: '任务', models: '模型', @@ -331,6 +332,7 @@ export default { custom: '自定义', selectProvider: '选择 Provider', chooseProvider: '选择一个 provider...', + getApiKey: '获取 API Key', name: '名称', autoGeneratedName: '根据 Base URL 自动生成', baseUrl: 'Base URL', diff --git a/packages/client/src/views/hermes/ModelsView.vue b/packages/client/src/views/hermes/ModelsView.vue index 1b2dacea..e335b31c 100644 --- a/packages/client/src/views/hermes/ModelsView.vue +++ b/packages/client/src/views/hermes/ModelsView.vue @@ -70,6 +70,7 @@ async function handleSaved() { flex-direction: column; } + .models-content { flex: 1; overflow-y: auto; diff --git a/packages/server/src/controllers/hermes/models.ts b/packages/server/src/controllers/hermes/models.ts index 38598a09..b450e467 100644 --- a/packages/server/src/controllers/hermes/models.ts +++ b/packages/server/src/controllers/hermes/models.ts @@ -41,7 +41,7 @@ export async function getAvailable(ctx: any) { currentDefault = modelSection.trim() } - const groups: Array<{ provider: string; label: string; base_url: string; models: string[]; api_key: string; model_meta?: Record }> = [] + const groups: Array<{ provider: string; label: string; base_url: string; models: string[]; api_key: string; builtin?: boolean; model_meta?: Record }> = [] const seenProviders = new Set() let envContent = '' @@ -57,10 +57,10 @@ export async function getAvailable(ctx: any) { const match = envContent.match(new RegExp(`^${key}\\s*=\\s*(.+)`, 'm')) return match?.[1]?.trim() || '' } - const addGroup = (provider: string, label: string, base_url: string, models: string[], api_key: string, model_meta?: Record) => { + const addGroup = (provider: string, label: string, base_url: string, models: string[], api_key: string, builtin?: boolean, model_meta?: Record) => { if (seenProviders.has(provider)) return seenProviders.add(provider) - groups.push({ provider, label, base_url, models: [...models], api_key, ...(model_meta ? { model_meta } : {}) }) + groups.push({ provider, label, base_url, models: [...models], api_key, ...(builtin ? { builtin: true } : {}), ...(model_meta ? { model_meta } : {}) }) } const isOAuthAuthorized = (providerKey: string): boolean => { @@ -150,7 +150,7 @@ export async function getAvailable(ctx: any) { } if (modelsList.length > 0) { const apiKey = envMapping.api_key_env ? envGetValue(envMapping.api_key_env) : '' - addGroup(providerKey, label, baseUrl, modelsList, apiKey, modelMeta) + addGroup(providerKey, label, baseUrl, modelsList, apiKey, true, modelMeta) } } @@ -166,19 +166,20 @@ export async function getAvailable(ctx: any) { 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) { + // Skip dynamic fetch for builtin presets — their model list is maintained in providers.ts + if (!builtinPreset && cp.api_key) { try { const fetched = await fetchProviderModels(baseUrl, cp.api_key); if (fetched.length > 0) models = [...new Set([cp.model, ...fetched])] } catch { } } const label = builtinPreset?.label || cp.name const presetBaseUrl = builtinPreset?.base_url || '' - return { providerKey, label, base_url: presetBaseUrl || baseUrl, models, api_key: cp.api_key || '' } + return { providerKey, label, base_url: presetBaseUrl || baseUrl, models, api_key: cp.api_key || '', builtin: !!builtinPreset } }), ) for (const result of customFetches) { if (result.status === 'fulfilled' && result.value) { - const { providerKey, label, base_url, models, api_key: cpApiKey } = result.value - addGroup(providerKey, label, base_url, models, cpApiKey) + const { providerKey, label, base_url, models, api_key: cpApiKey, builtin: cpBuiltin } = result.value as any + addGroup(providerKey, label, base_url, models, cpApiKey, cpBuiltin) } } diff --git a/packages/server/src/controllers/hermes/providers.ts b/packages/server/src/controllers/hermes/providers.ts index c4dd93fd..5661be63 100644 --- a/packages/server/src/controllers/hermes/providers.ts +++ b/packages/server/src/controllers/hermes/providers.ts @@ -3,6 +3,7 @@ import { writeFile } from 'fs/promises' import { getActiveAuthPath } from '../../services/hermes/hermes-profile' import * as hermesCli from '../../services/hermes/hermes-cli' import { readConfigYaml, writeConfigYaml, saveEnvValue, PROVIDER_ENV_MAP } from '../../services/config-helpers' +import { PROVIDER_PRESETS } from '../../shared/providers' import { logger } from '../../services/logger' const OPTIONAL_API_KEY_PROVIDERS = new Set(['cliproxyapi']) @@ -39,18 +40,22 @@ export async function create(ctx: any) { existing.base_url = base_url existing.api_key = api_key existing.model = model + const preset = PROVIDER_PRESETS.find(p => p.value === poolKey.replace('custom:', '')) + if (preset?.api_mode) existing.api_mode = preset.api_mode if (context_length && context_length > 0) { if (!existing.models) existing.models = {} existing.models[model] = existing.models[model] || {} existing.models[model].context_length = context_length } } else { - config.custom_providers.push(buildProviderEntry(name.trim().toLowerCase().replace(/ /g, '-'), base_url, api_key, model, context_length)) + const entry = buildProviderEntry(name.trim().toLowerCase().replace(/ /g, '-'), base_url, api_key, model, context_length) + const preset = PROVIDER_PRESETS.find(p => p.value === poolKey.replace('custom:', '')) + if (preset?.api_mode) entry.api_mode = preset.api_mode + config.custom_providers.push(entry) } 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) } @@ -65,13 +70,18 @@ export async function create(ctx: any) { existing.base_url = base_url existing.api_key = api_key existing.model = model + const preset = PROVIDER_PRESETS.find(p => p.value === poolKey) + if (preset?.api_mode) existing.api_mode = preset.api_mode if (context_length && context_length > 0) { if (!existing.models) existing.models = {} existing.models[model] = existing.models[model] || {} existing.models[model].context_length = context_length } } else { - config.custom_providers.push(buildProviderEntry(poolKey, base_url, api_key, model, context_length)) + const entry = buildProviderEntry(poolKey, base_url, api_key, model, context_length) + const preset = PROVIDER_PRESETS.find(p => p.value === poolKey) + if (preset?.api_mode) entry.api_mode = preset.api_mode + config.custom_providers.push(entry) } config.model.default = model config.model.provider = `custom:${poolKey}` diff --git a/packages/server/src/services/config-helpers.ts b/packages/server/src/services/config-helpers.ts index 487dcdb8..8bd0b294 100644 --- a/packages/server/src/services/config-helpers.ts +++ b/packages/server/src/services/config-helpers.ts @@ -8,6 +8,8 @@ import { logger } from './logger' // --- Provider env var mapping (from hermes providers.py HERMES_OVERLAYS + config.py) --- export const PROVIDER_ENV_MAP: Record = { + 'fun-codex': { api_key_env: '', base_url_env: '' }, + 'fun-claude': { api_key_env: '', 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: '' }, diff --git a/packages/server/src/shared/providers.ts b/packages/server/src/shared/providers.ts index 5fd99c5e..75ea8812 100644 --- a/packages/server/src/shared/providers.ts +++ b/packages/server/src/shared/providers.ts @@ -9,9 +9,36 @@ export interface ProviderPreset { base_url: string models: string[] builtin: boolean + api_mode?: 'openai' | 'anthropic' | 'anthropic_messages' } export const PROVIDER_PRESETS: ProviderPreset[] = [ + { + label: 'FUN-Codex', + value: 'fun-codex', + builtin: true, + base_url: 'https://api.apikey.fun/v1', + models: [ + 'gpt-5.5', + 'gpt-5.4', + 'gpt-5.4-mini', + 'gpt-5.3-codex', + 'gpt-5.3-codex-spark', + ], + }, + { + label: 'FUN-Claude', + value: 'fun-claude', + builtin: true, + base_url: 'https://api.apikey.fun', + api_mode: "anthropic_messages", + models: [ + 'claude-opus-4-7', + 'claude-opus-4-6', + 'claude-sonnet-4-6', + 'claude-haiku-4-5' + ], + }, { label: 'Anthropic', value: 'anthropic',