diff --git a/src/services/modelService.ts b/src/services/modelService.ts index 75663c6..e4d3591 100644 --- a/src/services/modelService.ts +++ b/src/services/modelService.ts @@ -40,21 +40,23 @@ const stripKnownSuffix = (baseUrl: string) => .replace(/\/(?:v1beta|v1)\/models$/i, '') .replace(/\/models$/i, ''); -const endpointCandidates = (provider: ModelProvider, baseUrl: string): string[] => { +export const modelEndpointCandidates = (provider: ModelProvider, baseUrl: string): string[] => { const resolvedBaseUrl = baseUrl.trim() || (provider === 'gemini' ? DEFAULT_GEMINI_BASE_URL : provider === 'claude' ? DEFAULT_CLAUDE_BASE_URL : ''); - const base = stripKnownSuffix(resolvedBaseUrl); - if (!base) return []; + const normalized = normalizeBaseUrl(resolvedBaseUrl); + if (!normalized) return []; + if (provider === 'openai') { + return [/\/models$/i.test(normalized) ? normalized : `${normalized}/models`]; + } + const base = stripKnownSuffix(normalized); const withoutVersion = base.replace(/\/(?:v1beta|v1)$/i, ''); if (provider === 'gemini') return [`${withoutVersion}/v1beta/models`]; if (provider === 'claude') return [`${withoutVersion}/v1/models`]; - const first = /\/v1$/i.test(base) ? `${base}/models` : `${base}/v1/models`; - const fallback = `${base}/models`; - return first === fallback ? [first] : [first, fallback]; + return [/\/v1$/i.test(base) ? `${base}/models` : `${base}/v1/models`]; }; const normalizeModelList = (payload: unknown): ModelOption[] => { @@ -95,7 +97,7 @@ export async function fetchModels( timeoutMs?: number, ): Promise { const normalized = baseUrl.trim() ? normalizeBaseUrl(baseUrl) : ''; - const candidates = endpointCandidates(provider, normalized); + const candidates = modelEndpointCandidates(provider, normalized); if (candidates.length === 0) throw new Error(modelText('model.error.baseUrlRequired')); const headers: Record = { ...customHeaders }; const hasHeader = (name: string) => diff --git a/src/services/providerHealthCheck.ts b/src/services/providerHealthCheck.ts index 7b88e27..d6efdba 100644 --- a/src/services/providerHealthCheck.ts +++ b/src/services/providerHealthCheck.ts @@ -44,6 +44,12 @@ const endpointRoot = (provider: ModelProvider, baseUrl: string) => { return normalized.replace(/\/(?:v1beta|v1)$/i, ''); }; +const openAIChatCompletionsEndpoint = (baseUrl: string) => { + const normalized = normalizeBaseUrl(baseUrl); + if (!normalized) return ''; + return `${normalized}/chat/completions`; +}; + const hasHeader = (headers: Record, name: string) => Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase()); @@ -148,7 +154,7 @@ export function buildProviderHealthProbe( } return { - url: `${root}/v1/chat/completions`, + url: openAIChatCompletionsEndpoint(baseUrl), header: headers, protocol: 'openai-chat', data: JSON.stringify({ diff --git a/tests/providerHealthCheck.test.ts b/tests/providerHealthCheck.test.ts index 42650a9..35bd9da 100644 --- a/tests/providerHealthCheck.test.ts +++ b/tests/providerHealthCheck.test.ts @@ -5,6 +5,7 @@ import { primaryProviderHealthCredential, runProviderModelHealthChecks, } from '../src/services/providerHealthCheck'; +import { modelEndpointCandidates } from '../src/services/modelService'; describe('API 接入健康检测', () => { it('健康检测只保留已勾选模型,并使用发现结果补充模型信息', () => { @@ -59,6 +60,51 @@ describe('API 接入健康检测', () => { expect(probe.protocol).toBe('openai-chat'); }); + it('为非 v1 的 OpenAI 兼容版本前缀保留原始版本号', () => { + const probe = buildProviderHealthProbe( + 'openai', + 'https://open.bigmodel.cn/api/coding/paas/v4', + 'glm-test', + 'secret-key', + ); + + expect(probe.url).toBe( + 'https://open.bigmodel.cn/api/coding/paas/v4/chat/completions', + ); + expect(modelEndpointCandidates( + 'openai', + 'https://open.bigmodel.cn/api/coding/paas/v4', + )).toEqual([ + 'https://open.bigmodel.cn/api/coding/paas/v4/models', + ]); + }); + + it('把不含版本号的中转地址视为完整 API 路径前缀', () => { + const baseUrl = 'https://relay.example/custom/openai'; + const probe = buildProviderHealthProbe( + 'openai', + baseUrl, + 'mapped-model', + 'secret-key', + ); + + expect(probe.url).toBe( + 'https://relay.example/custom/openai/chat/completions', + ); + expect(modelEndpointCandidates('openai', baseUrl)).toEqual([ + 'https://relay.example/custom/openai/models', + ]); + expect(modelEndpointCandidates( + 'openai', + 'https://relay.example/custom/openai/v1/models', + )).toEqual([ + 'https://relay.example/custom/openai/v1/models', + ]); + expect(modelEndpointCandidates('codex', 'https://codex.example')).toEqual([ + 'https://codex.example/v1/models', + ]); + }); + it('不会覆盖供应商已有的自定义认证头', () => { const probe = buildProviderHealthProbe( 'claude',