fix(openai-compatibility): append chat and models paths directly to base URLs

This commit is contained in:
lzt404 committed 2026-08-07 13:47:49 +08:00
1 parent 7c2dab9fc2
commit 65df80091c
3 files changed
+62 -8

No files matched your search

+9 -7
View File
@@ -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<ModelOption[]> {
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<string, string> = { ...customHeaders };
const hasHeader = (name: string) =>
+7 -1
View File
@@ -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<string, string>, 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({
+46
View File
@@ -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',