Skip to content

Commit 1868b3b

Browse files
committed
fix(chat): resolve llm client endpoint conflicts with model base urls
1 parent f9e7fbf commit 1868b3b

2 files changed

Lines changed: 42 additions & 2 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,

0 commit comments

Comments
 (0)