Skip to content

Commit c0865c2

Browse files
committed
feat(chat): centralize LLM provider configs and align client endpoint routing
1 parent 02ced6d commit c0865c2

3 files changed

Lines changed: 74 additions & 115 deletions

File tree

apps/webuiapps/src/lib/__tests__/llmClient.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,16 @@ describe('chat()', () => {
345345
expect(headers['Authorization']).toBe('Bearer sk-test-key');
346346
});
347347

348+
it('uses v1/chat/completions when baseUrl has no version suffix', async () => {
349+
const mockFetch = vi.fn().mockResolvedValueOnce(makeOpenAIResponse('ok'));
350+
globalThis.fetch = mockFetch;
351+
352+
await chat(MOCK_MESSAGES, [], MOCK_OPENAI_CONFIG);
353+
354+
const headers = mockFetch.mock.calls[0][1].headers as Record<string, string>;
355+
expect(headers['X-LLM-Target-URL']).toBe('https://api.openai.com/v1/chat/completions');
356+
});
357+
348358
it('includes tools in body when tools array is non-empty', async () => {
349359
const mockFetch = vi.fn().mockResolvedValueOnce(makeOpenAIResponse('ok'));
350360
globalThis.fetch = mockFetch;
@@ -421,6 +431,20 @@ describe('chat()', () => {
421431
expect(headers['x-api-key']).toBe('ant-test-key');
422432
});
423433

434+
it('uses /messages when baseUrl already includes /v1', async () => {
435+
const mockFetch = vi.fn().mockResolvedValueOnce(makeAnthropicResponse('Anthropic response'));
436+
globalThis.fetch = mockFetch;
437+
438+
const configWithVersion: LLMConfig = {
439+
...MOCK_ANTHROPIC_CONFIG,
440+
baseUrl: 'https://api.anthropic.com/v1',
441+
};
442+
await chat(MOCK_MESSAGES, [], configWithVersion);
443+
444+
const headers = mockFetch.mock.calls[0][1].headers as Record<string, string>;
445+
expect(headers['X-LLM-Target-URL']).toBe('https://api.anthropic.com/v1/messages');
446+
});
447+
424448
it('extracts system message to top-level system field', async () => {
425449
const messages: ChatMessage[] = [
426450
{ role: 'system', content: 'You are helpful.' },

apps/webuiapps/src/lib/llmClient.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,22 @@ interface LLMResponse {
9494
toolCalls: ToolCall[];
9595
}
9696

97+
function hasVersionSuffix(url: string): boolean {
98+
return /\/v\d+\/?$/.test(url);
99+
}
100+
101+
function joinUrl(baseUrl: string, path: string): string {
102+
return `${baseUrl.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`;
103+
}
104+
105+
function getOpenAICompletionsPath(baseUrl: string): string {
106+
return hasVersionSuffix(baseUrl) ? 'chat/completions' : 'v1/chat/completions';
107+
}
108+
109+
function getAnthropicMessagesPath(baseUrl: string): string {
110+
return hasVersionSuffix(baseUrl) ? 'messages' : 'v1/messages';
111+
}
112+
97113
function parseCustomHeaders(raw?: string): Record<string, string> {
98114
if (!raw) return {};
99115
const headers: Record<string, string> = {};
@@ -143,7 +159,7 @@ async function chatOpenAI(
143159
body.tools = tools;
144160
}
145161

146-
const targetUrl = `${config.baseUrl}/chat/completions`;
162+
const targetUrl = joinUrl(config.baseUrl, getOpenAICompletionsPath(config.baseUrl));
147163
const toolNames = Array.isArray(tools) ? tools.map((t) => t.function?.name).filter(Boolean) : [];
148164
logger.info('ToolLog', 'LLM Request: toolCount=', tools.length, 'toolNames=', toolNames);
149165
logger.info('LLM', 'Request:', {
@@ -250,7 +266,7 @@ async function chatAnthropic(
250266
'toolNames=',
251267
anthropicToolNames,
252268
);
253-
const targetUrl = `${config.baseUrl}/v1/messages`;
269+
const targetUrl = joinUrl(config.baseUrl, getAnthropicMessagesPath(config.baseUrl));
254270
logger.info('LLM', 'Anthropic Request:', {
255271
targetUrl,
256272
model: config.model,

apps/webuiapps/src/lib/llmModels.ts

Lines changed: 32 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ export interface ModelInfo {
66
id: string;
77
name: string;
88
category?: ModelCategory;
9-
description?: string;
109
}
1110

1211
export interface LLMConfig {
@@ -30,44 +29,19 @@ export const LLM_PROVIDER_CONFIGS: Record<LLMProvider, ProviderModelConfig> = {
3029
baseUrl: 'https://api.openai.com/v1',
3130
defaultModel: 'gpt-5.4',
3231
models: [
33-
{ id: 'gpt-5.4', name: 'GPT-5.4', category: 'flagship', description: '当前最强' },
34-
{
35-
id: 'gpt-5.4-pro',
36-
name: 'GPT-5.4 Pro',
37-
category: 'flagship',
38-
description: '更高精度/更贵',
39-
},
40-
{
41-
id: 'gpt-5.4-thinking',
42-
name: 'GPT-5.4 Thinking',
43-
category: 'thinking',
44-
description: '强化推理',
45-
},
46-
{ id: 'gpt-5.3', name: 'GPT-5.3', category: 'general', description: '通用' },
47-
{
48-
id: 'gpt-5.3-instant',
49-
name: 'GPT-5.3 Instant',
50-
category: 'general',
51-
description: '更快更便宜',
52-
},
53-
{
54-
id: 'gpt-5.3-codex',
55-
name: 'GPT-5.3 Codex',
56-
category: 'coding',
57-
description: '最强代码 Agent',
58-
},
59-
{
60-
id: 'gpt-5.3-codex-spark',
61-
name: 'GPT-5.3 Codex Spark',
62-
category: 'coding',
63-
description: '超低延迟实时编程',
64-
},
32+
{ id: 'gpt-5.4', name: 'GPT-5.4', category: 'flagship' },
33+
{ id: 'gpt-5.4-pro', name: 'GPT-5.4 Pro', category: 'flagship' },
34+
{ id: 'gpt-5.4-thinking', name: 'GPT-5.4 Thinking', category: 'thinking' },
35+
{ id: 'gpt-5.3', name: 'GPT-5.3', category: 'general' },
36+
{ id: 'gpt-5.3-instant', name: 'GPT-5.3 Instant', category: 'general' },
37+
{ id: 'gpt-5.3-codex', name: 'GPT-5.3 Codex', category: 'coding' },
38+
{ id: 'gpt-5.3-codex-spark', name: 'GPT-5.3 Codex Spark', category: 'coding' },
6539
{ id: 'gpt-5-mini', name: 'GPT-5 mini', category: 'lightweight' },
6640
{ id: 'gpt-5-nano', name: 'GPT-5 nano', category: 'lightweight' },
6741
{ id: 'gpt-4.1', name: 'GPT-4.1', category: 'general' },
6842
{ id: 'gpt-4.1-mini', name: 'GPT-4.1 mini', category: 'lightweight' },
6943
{ id: 'gpt-4.1-nano', name: 'GPT-4.1 nano', category: 'lightweight' },
70-
{ id: 'gpt-4o', name: 'GPT-4o', category: 'general', description: '多模态' },
44+
{ id: 'gpt-4o', name: 'GPT-4o', category: 'general' },
7145
{ id: 'gpt-4o-mini', name: 'GPT-4o mini', category: 'lightweight' },
7246
{ id: 'gpt-4-turbo', name: 'GPT-4 Turbo', category: 'general' },
7347
],
@@ -78,36 +52,11 @@ export const LLM_PROVIDER_CONFIGS: Record<LLMProvider, ProviderModelConfig> = {
7852
baseUrl: 'https://api.anthropic.com/v1',
7953
defaultModel: 'claude-sonnet-4-6',
8054
models: [
81-
{
82-
id: 'claude-opus-4-6',
83-
name: 'Claude Opus 4.6',
84-
category: 'flagship',
85-
description: '最强推理',
86-
},
87-
{
88-
id: 'claude-opus-4-5',
89-
name: 'Claude Opus 4.5',
90-
category: 'flagship',
91-
description: '上一代旗舰',
92-
},
93-
{
94-
id: 'claude-sonnet-4-6',
95-
name: 'Claude Sonnet 4.6',
96-
category: 'general',
97-
description: '性价比最高',
98-
},
99-
{
100-
id: 'claude-sonnet-4-5',
101-
name: 'Claude Sonnet 4.5',
102-
category: 'general',
103-
description: '编程主力',
104-
},
105-
{
106-
id: 'claude-haiku-4-5',
107-
name: 'Claude Haiku 4.5',
108-
category: 'lightweight',
109-
description: '超快/超便宜',
110-
},
55+
{ id: 'claude-opus-4-6', name: 'Claude Opus 4.6', category: 'flagship' },
56+
{ id: 'claude-opus-4-5', name: 'Claude Opus 4.5', category: 'flagship' },
57+
{ id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', category: 'general' },
58+
{ id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5', category: 'general' },
59+
{ id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5', category: 'lightweight' },
11160
],
11261
},
11362

@@ -116,13 +65,8 @@ export const LLM_PROVIDER_CONFIGS: Record<LLMProvider, ProviderModelConfig> = {
11665
baseUrl: 'https://api.deepseek.com/v1',
11766
defaultModel: 'deepseek-chat',
11867
models: [
119-
{ id: 'deepseek-chat', name: 'DeepSeek Chat', category: 'general', description: '通用对话' },
120-
{
121-
id: 'deepseek-reasoner',
122-
name: 'DeepSeek Reasoner',
123-
category: 'thinking',
124-
description: '强化推理',
125-
},
68+
{ id: 'deepseek-chat', name: 'DeepSeek Chat', category: 'general' },
69+
{ id: 'deepseek-reasoner', name: 'DeepSeek Reasoner', category: 'thinking' },
12670
],
12771
},
12872

@@ -131,28 +75,13 @@ export const LLM_PROVIDER_CONFIGS: Record<LLMProvider, ProviderModelConfig> = {
13175
baseUrl: 'https://api.minimax.io/anthropic/v1',
13276
defaultModel: 'MiniMax-M2.5',
13377
models: [
134-
{ id: 'MiniMax-M2.5', name: 'MiniMax M2.5', category: 'flagship', description: '最新主力' },
135-
{
136-
id: 'MiniMax-M2.5-highspeed',
137-
name: 'MiniMax M2.5 Highspeed',
138-
category: 'general',
139-
description: '高速版',
140-
},
141-
{ id: 'MiniMax-M2.1', name: 'MiniMax M2.1', category: 'coding', description: '多语言编程' },
142-
{
143-
id: 'MiniMax-M2.1-highspeed',
144-
name: 'MiniMax M2.1 Highspeed',
145-
category: 'coding',
146-
description: '高速版',
147-
},
148-
{ id: 'MiniMax-M2.7', name: 'MiniMax M2.7', category: 'flagship', description: '自我改进' },
149-
{
150-
id: 'MiniMax-M2.7-highspeed',
151-
name: 'MiniMax M2.7 Highspeed',
152-
category: 'general',
153-
description: '高速版',
154-
},
155-
{ id: 'MiniMax-M2', name: 'MiniMax M2', category: 'general', description: 'Agent能力' },
78+
{ id: 'MiniMax-M2.5', name: 'MiniMax M2.5', category: 'flagship' },
79+
{ id: 'MiniMax-M2.5-highspeed', name: 'MiniMax M2.5 Highspeed', category: 'general' },
80+
{ id: 'MiniMax-M2.1', name: 'MiniMax M2.1', category: 'coding' },
81+
{ id: 'MiniMax-M2.1-highspeed', name: 'MiniMax M2.1 Highspeed', category: 'coding' },
82+
{ id: 'MiniMax-M2.7', name: 'MiniMax M2.7', category: 'flagship' },
83+
{ id: 'MiniMax-M2.7-highspeed', name: 'MiniMax M2.7 Highspeed', category: 'general' },
84+
{ id: 'MiniMax-M2', name: 'MiniMax M2', category: 'general' },
15685
],
15786
},
15887

@@ -161,21 +90,16 @@ export const LLM_PROVIDER_CONFIGS: Record<LLMProvider, ProviderModelConfig> = {
16190
baseUrl: 'https://api.z.ai/api/coding/paas/v4',
16291
defaultModel: 'glm-5',
16392
models: [
164-
{ id: 'glm-5', name: 'GLM-5', category: 'flagship', description: '最新旗舰' },
165-
{ id: 'glm-5-code', name: 'GLM-5 Code', category: 'coding', description: '代码专用' },
93+
{ id: 'glm-5', name: 'GLM-5', category: 'flagship' },
94+
{ id: 'glm-5-code', name: 'GLM-5 Code', category: 'coding' },
16695
{ id: 'glm-4.7', name: 'GLM-4.7', category: 'general' },
16796
{ id: 'glm-4.6', name: 'GLM-4.6', category: 'general' },
16897
{ id: 'glm-4.5', name: 'GLM-4.5', category: 'general' },
169-
{ id: 'glm-4.5-x', name: 'GLM-4.5-X', category: 'general', description: '增强版' },
170-
{ id: 'glm-4.5-air', name: 'GLM-4.5 Air', category: 'lightweight', description: '高性价比' },
98+
{ id: 'glm-4.5-x', name: 'GLM-4.5-X', category: 'general' },
99+
{ id: 'glm-4.5-air', name: 'GLM-4.5 Air', category: 'lightweight' },
171100
{ id: 'glm-4.5-airx', name: 'GLM-4.5 AirX', category: 'lightweight' },
172-
{ id: 'glm-4.7-flash', name: 'GLM-4.7 Flash', category: 'lightweight', description: '高速' },
173-
{
174-
id: 'glm-4.7-flashx',
175-
name: 'GLM-4.7 FlashX',
176-
category: 'lightweight',
177-
description: '超高速',
178-
},
101+
{ id: 'glm-4.7-flash', name: 'GLM-4.7 Flash', category: 'lightweight' },
102+
{ id: 'glm-4.7-flashx', name: 'GLM-4.7 FlashX', category: 'lightweight' },
179103
{ id: 'glm-4.5-flash', name: 'GLM-4.5 Flash', category: 'lightweight' },
180104
{ id: 'glm-4-32b-0414-128k', name: 'GLM-4 32B (128K)', category: 'general' },
181105
],
@@ -186,15 +110,10 @@ export const LLM_PROVIDER_CONFIGS: Record<LLMProvider, ProviderModelConfig> = {
186110
baseUrl: 'https://api.moonshot.cn/v1',
187111
defaultModel: 'kimi-k2-5',
188112
models: [
189-
{ id: 'kimi-k2-5', name: 'Kimi K2.5', category: 'flagship', description: '最新强化版' },
190-
{ id: 'kimi-k2', name: 'Kimi K2', category: 'flagship', description: '开源/Agent导向' },
191-
{
192-
id: 'kimi-k2-thinking',
193-
name: 'Kimi K2 Thinking',
194-
category: 'thinking',
195-
description: '强化推理',
196-
},
197-
{ id: 'kimi-k2-turbo', name: 'Kimi K2 Turbo', category: 'general', description: '高速版' },
113+
{ id: 'kimi-k2-5', name: 'Kimi K2.5', category: 'flagship' },
114+
{ id: 'kimi-k2', name: 'Kimi K2', category: 'flagship' },
115+
{ id: 'kimi-k2-thinking', name: 'Kimi K2 Thinking', category: 'thinking' },
116+
{ id: 'kimi-k2-turbo', name: 'Kimi K2 Turbo', category: 'general' },
198117
],
199118
},
200119
};

0 commit comments

Comments
 (0)