From 9838173365ee0d54b4863b7f1b643ebd0441fac4 Mon Sep 17 00:00:00 2001 From: ekko <152005280+EKKOLearnAI@users.noreply.github.com> Date: Fri, 29 May 2026 19:18:40 +0800 Subject: [PATCH] fix builtin provider flags (#1125) --- .../server/src/controllers/hermes/models.ts | 18 +++++++-- packages/server/src/shared/providers.ts | 1 + .../model-visibility-controller.test.ts | 38 +++++++++++++++++++ 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/packages/server/src/controllers/hermes/models.ts b/packages/server/src/controllers/hermes/models.ts index 57d7de1..b52fa0f 100644 --- a/packages/server/src/controllers/hermes/models.ts +++ b/packages/server/src/controllers/hermes/models.ts @@ -99,6 +99,7 @@ function providerPresetToGroup(p: any, models?: string[]): AvailableGroup { base_url: p.base_url, models: models || p.models, api_key: '', + ...(p.builtin ? { builtin: true } : {}), ...(envMapping?.base_url_env ? { base_url_env: envMapping.base_url_env } : {}), } } @@ -181,6 +182,15 @@ function providerKeyForCustom(name: string): string { return `custom:${name.trim().toLowerCase().replace(/ /g, '-')}` } +function providerKeyWithoutCustomPrefix(providerKey: string): string { + return providerKey.startsWith('custom:') ? providerKey.slice('custom:'.length) : providerKey +} + +function isBuiltinProviderKey(providerKey: string): boolean { + const normalized = providerKeyWithoutCustomPrefix(providerKey) + return PROVIDER_PRESETS.some((preset: any) => preset.value === normalized && preset.builtin === true) +} + function providerShouldFetchLiveModels(providerKey: string): boolean { return providerKey === 'openrouter' || providerKey === 'cliproxyapi' || @@ -402,13 +412,13 @@ async function buildAvailableForProfile( const fetched = await cachedProviderModels(fetchCache, baseUrl, cp.api_key) if (fetched.length > 0) models = [...new Set([...models, ...fetched])] } - return { providerKey, label: cp.name, base_url: baseUrl, models, api_key: cp.api_key || '' } + return { providerKey, label: cp.name, base_url: baseUrl, models, api_key: cp.api_key || '', builtin: isBuiltinProviderKey(providerKey) } }), ) for (const result of customFetches) { if (result.status === 'fulfilled' && result.value?.models.length) { - const { providerKey, label, base_url, models, api_key } = result.value - addGroup(providerKey, label, base_url, models, api_key) + const { providerKey, label, base_url, models, api_key, builtin } = result.value + addGroup(providerKey, label, base_url, models, api_key, builtin) } } @@ -655,7 +665,7 @@ export async function getAvailable(ctx: any) { if (cp.api_key) { try { const fetched = await fetchProviderModels(baseUrl, cp.api_key); if (fetched.length > 0) models = [...new Set([cp.model, ...fetched])] } catch { } } - return { providerKey, label: cp.name, base_url: baseUrl, models, api_key: cp.api_key || '' } + return { providerKey, label: cp.name, base_url: baseUrl, models, api_key: cp.api_key || '', builtin: isBuiltinProviderKey(providerKey) } }), ) diff --git a/packages/server/src/shared/providers.ts b/packages/server/src/shared/providers.ts index e92208e..7457f27 100644 --- a/packages/server/src/shared/providers.ts +++ b/packages/server/src/shared/providers.ts @@ -34,6 +34,7 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [ base_url: 'https://api.apikey.fun', api_mode: "anthropic_messages", models: [ + 'claude-opus-4-8', 'claude-opus-4-7', 'claude-opus-4-6', 'claude-sonnet-4-6', diff --git a/tests/server/model-visibility-controller.test.ts b/tests/server/model-visibility-controller.test.ts index 3f693c5..1090834 100644 --- a/tests/server/model-visibility-controller.test.ts +++ b/tests/server/model-visibility-controller.test.ts @@ -42,6 +42,7 @@ vi.mock('../../packages/server/src/services/config-helpers', () => ({ fetchProviderModels: mockFetchProviderModels, buildModelGroups: mockBuildModelGroups, PROVIDER_ENV_MAP: { + 'fun-codex': { api_key_env: '', base_url_env: '' }, deepseek: { api_key_env: 'DEEPSEEK_API_KEY', base_url_env: 'DEEPSEEK_BASE_URL' }, lmstudio: { api_key_env: 'LM_API_KEY', base_url_env: 'LM_BASE_URL' }, 'xai-oauth': { api_key_env: '', base_url_env: '' }, @@ -56,29 +57,40 @@ vi.mock('../../packages/server/src/shared/providers', () => ({ openrouter: ['openrouter/auto'], }), PROVIDER_PRESETS: [ + { + value: 'fun-codex', + label: 'Codex-apikey.fun', + base_url: 'https://api.apikey.fun/v1', + models: ['gpt-5.5'], + builtin: true, + }, { value: 'deepseek', label: 'DeepSeek', base_url: 'https://api.deepseek.com/v1', models: ['deepseek-chat', 'deepseek-reasoner'], + builtin: true, }, { value: 'openrouter', label: 'OpenRouter', base_url: 'https://openrouter.ai/api/v1', models: ['openrouter/auto'], + builtin: true, }, { value: 'lmstudio', label: 'LM Studio', base_url: 'http://127.0.0.1:1234/v1', models: [], + builtin: true, }, { value: 'xai-oauth', label: 'xAI Grok OAuth (SuperGrok Subscription)', base_url: 'https://api.x.ai/v1', models: ['grok-4.3', 'grok-4.20-0309-reasoning'], + builtin: true, }, ], })) @@ -277,6 +289,7 @@ describe('models controller — model visibility', () => { expect(ctx.body.allProviders).toEqual(expect.arrayContaining([ expect.objectContaining({ provider: 'deepseek', + builtin: true, base_url_env: 'DEEPSEEK_BASE_URL', }), expect.not.objectContaining({ @@ -286,6 +299,31 @@ describe('models controller — model visibility', () => { ])) }) + it('marks custom-prefixed providers as builtin when their provider key matches a preset', async () => { + mockReadConfigYamlForProfile.mockResolvedValue({ + model: { default: 'gpt-5.5', provider: 'custom:fun-codex' }, + custom_providers: [ + { + name: 'fun-codex', + base_url: 'https://proxy.example.com/v1', + model: 'gpt-5.5', + api_key: 'sk-test', + }, + ], + }) + + const ctx = makeCtx() + ctx.query = { profile: 'default' } + await ctrl.getAvailable(ctx) + + expect(ctx.body.groups).toEqual(expect.arrayContaining([ + expect.objectContaining({ + provider: 'custom:fun-codex', + builtin: true, + }), + ])) + }) + it('returns LM Studio configured default model when env credentials exist and catalog is empty', async () => { mockReadFile.mockResolvedValue('LM_API_KEY=local\nLM_BASE_URL=http://127.0.0.1:1234/v1\n') mockReadConfigYaml.mockResolvedValue({ model: { default: 'eee', provider: 'lmstudio' } })