Skip to content

Commit 3c89c43

Browse files
committed
fix: append /v1 to OpenAI Compatible base URLs for llama.cpp compatibility
- Automatically append /v1 to base URLs that are plain URLs without a path - This fixes compatibility with llama.cpp servers that expose OpenAI-compatible endpoints - Added tests to verify the URL handling works correctly Fixes #7065
1 parent ca6f261 commit 3c89c43

File tree

2 files changed

+52
-1
lines changed

2 files changed

+52
-1
lines changed

src/api/providers/__tests__/openai-native.spec.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,41 @@ describe("OpenAiNativeHandler", () => {
9999
})
100100
expect(handlerWithoutKey).toBeInstanceOf(OpenAiNativeHandler)
101101
})
102+
103+
it("should append /v1 to base URL when it's a plain URL without path", () => {
104+
// Test with plain URL (like llama.cpp server)
105+
const handler1 = new OpenAiNativeHandler({
106+
apiModelId: "gpt-4.1",
107+
openAiNativeApiKey: "test-api-key",
108+
openAiNativeBaseUrl: "http://127.0.0.1:8080",
109+
})
110+
expect(handler1).toBeInstanceOf(OpenAiNativeHandler)
111+
// The client should have the /v1 appended internally
112+
113+
// Test with URL that already has /v1
114+
const handler2 = new OpenAiNativeHandler({
115+
apiModelId: "gpt-4.1",
116+
openAiNativeApiKey: "test-api-key",
117+
openAiNativeBaseUrl: "http://localhost:8080/v1",
118+
})
119+
expect(handler2).toBeInstanceOf(OpenAiNativeHandler)
120+
121+
// Test with URL that has a different path
122+
const handler3 = new OpenAiNativeHandler({
123+
apiModelId: "gpt-4.1",
124+
openAiNativeApiKey: "test-api-key",
125+
openAiNativeBaseUrl: "https://api.openai.com/v1",
126+
})
127+
expect(handler3).toBeInstanceOf(OpenAiNativeHandler)
128+
129+
// Test with URL with trailing slash
130+
const handler4 = new OpenAiNativeHandler({
131+
apiModelId: "gpt-4.1",
132+
openAiNativeApiKey: "test-api-key",
133+
openAiNativeBaseUrl: "http://localhost:11434/",
134+
})
135+
expect(handler4).toBeInstanceOf(OpenAiNativeHandler)
136+
})
102137
})
103138

104139
describe("createMessage", () => {

src/api/providers/openai-native.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,23 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
5757
this.options.enableGpt5ReasoningSummary = true
5858
}
5959
const apiKey = this.options.openAiNativeApiKey ?? "not-provided"
60-
this.client = new OpenAI({ baseURL: this.options.openAiNativeBaseUrl, apiKey })
60+
61+
// Handle base URL - append /v1 if it's a plain URL without a path
62+
let baseURL = this.options.openAiNativeBaseUrl
63+
if (baseURL && !baseURL.includes("/v1")) {
64+
// Check if it's a URL without any path (just protocol://host:port)
65+
try {
66+
const url = new URL(baseURL)
67+
// If the pathname is just '/', append /v1
68+
if (url.pathname === "/") {
69+
baseURL = baseURL.replace(/\/$/, "") + "/v1"
70+
}
71+
} catch {
72+
// If URL parsing fails, leave it as is
73+
}
74+
}
75+
76+
this.client = new OpenAI({ baseURL, apiKey })
6177
}
6278

6379
private normalizeGpt5Usage(usage: any, model: OpenAiNativeModel): ApiStreamUsageChunk | undefined {

0 commit comments

Comments
 (0)