diff --git a/src/api/providers/__tests__/openai-native.spec.ts b/src/api/providers/__tests__/openai-native.spec.ts index 0acdb6202e3..201973d34fa 100644 --- a/src/api/providers/__tests__/openai-native.spec.ts +++ b/src/api/providers/__tests__/openai-native.spec.ts @@ -99,6 +99,41 @@ describe("OpenAiNativeHandler", () => { }) expect(handlerWithoutKey).toBeInstanceOf(OpenAiNativeHandler) }) + + it("should append /v1 to base URL when it's a plain URL without path", () => { + // Test with plain URL (like llama.cpp server) + const handler1 = new OpenAiNativeHandler({ + apiModelId: "gpt-4.1", + openAiNativeApiKey: "test-api-key", + openAiNativeBaseUrl: "http://127.0.0.1:8080", + }) + expect(handler1).toBeInstanceOf(OpenAiNativeHandler) + // The client should have the /v1 appended internally + + // Test with URL that already has /v1 + const handler2 = new OpenAiNativeHandler({ + apiModelId: "gpt-4.1", + openAiNativeApiKey: "test-api-key", + openAiNativeBaseUrl: "http://localhost:8080/v1", + }) + expect(handler2).toBeInstanceOf(OpenAiNativeHandler) + + // Test with URL that has a different path + const handler3 = new OpenAiNativeHandler({ + apiModelId: "gpt-4.1", + openAiNativeApiKey: "test-api-key", + openAiNativeBaseUrl: "https://api.openai.com/v1", + }) + expect(handler3).toBeInstanceOf(OpenAiNativeHandler) + + // Test with URL with trailing slash + const handler4 = new OpenAiNativeHandler({ + apiModelId: "gpt-4.1", + openAiNativeApiKey: "test-api-key", + openAiNativeBaseUrl: "http://localhost:11434/", + }) + expect(handler4).toBeInstanceOf(OpenAiNativeHandler) + }) }) describe("createMessage", () => { diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index 2ba85669631..0bf1e92515b 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -57,7 +57,23 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio this.options.enableGpt5ReasoningSummary = true } const apiKey = this.options.openAiNativeApiKey ?? "not-provided" - this.client = new OpenAI({ baseURL: this.options.openAiNativeBaseUrl, apiKey }) + + // Handle base URL - append /v1 if it's a plain URL without a path + let baseURL = this.options.openAiNativeBaseUrl + if (baseURL && !baseURL.includes("/v1")) { + // Check if it's a URL without any path (just protocol://host:port) + try { + const url = new URL(baseURL) + // If the pathname is just '/', append /v1 + if (url.pathname === "/") { + baseURL = baseURL.replace(/\/$/, "") + "/v1" + } + } catch { + // If URL parsing fails, leave it as is + } + } + + this.client = new OpenAI({ baseURL, apiKey }) } private normalizeGpt5Usage(usage: any, model: OpenAiNativeModel): ApiStreamUsageChunk | undefined {