(null);
+ const [showAdvanced, setShowAdvanced] = useState(false);
const apiTypeOptions = [
{ value: "chat", label: t("chatCompletions") },
@@ -804,6 +807,8 @@ function AddOpenAICompatibleModal({ isOpen, onClose, onCreated }) {
apiType: formData.apiType,
baseUrl: formData.baseUrl,
type: "openai-compatible",
+ chatPath: formData.chatPath || "",
+ modelsPath: formData.modelsPath || "",
}),
});
const data = await res.json();
@@ -814,9 +819,12 @@ function AddOpenAICompatibleModal({ isOpen, onClose, onCreated }) {
prefix: "",
apiType: "chat",
baseUrl: "https://api.openai.com/v1",
+ chatPath: "",
+ modelsPath: "",
});
setCheckKey("");
setValidationResult(null);
+ setShowAdvanced(false);
}
} catch (error) {
console.log("Error creating OpenAI Compatible node:", error);
@@ -835,6 +843,7 @@ function AddOpenAICompatibleModal({ isOpen, onClose, onCreated }) {
baseUrl: formData.baseUrl,
apiKey: checkKey,
type: "openai-compatible",
+ modelsPath: formData.modelsPath || "",
}),
});
const data = await res.json();
@@ -876,6 +885,39 @@ function AddOpenAICompatibleModal({ isOpen, onClose, onCreated }) {
placeholder={t("openaiBaseUrlPlaceholder")}
hint={t("compatibleBaseUrlHint", { type: t("openai") })}
/>
+
(null);
+ const [showAdvanced, setShowAdvanced] = useState(false);
useEffect(() => {
// Reset validation when modal opens
@@ -959,6 +1004,8 @@ function AddAnthropicCompatibleModal({ isOpen, onClose, onCreated }) {
prefix: formData.prefix,
baseUrl: formData.baseUrl,
type: "anthropic-compatible",
+ chatPath: formData.chatPath || "",
+ modelsPath: formData.modelsPath || "",
}),
});
const data = await res.json();
@@ -968,9 +1015,12 @@ function AddAnthropicCompatibleModal({ isOpen, onClose, onCreated }) {
name: "",
prefix: "",
baseUrl: "https://api.anthropic.com/v1",
+ chatPath: "",
+ modelsPath: "",
});
setCheckKey("");
setValidationResult(null);
+ setShowAdvanced(false);
}
} catch (error) {
console.log("Error creating Anthropic Compatible node:", error);
@@ -989,6 +1039,7 @@ function AddAnthropicCompatibleModal({ isOpen, onClose, onCreated }) {
baseUrl: formData.baseUrl,
apiKey: checkKey,
type: "anthropic-compatible",
+ modelsPath: formData.modelsPath || "",
}),
});
const data = await res.json();
@@ -1024,6 +1075,39 @@ function AddAnthropicCompatibleModal({ isOpen, onClose, onCreated }) {
placeholder={t("anthropicBaseUrlPlaceholder")}
hint={t("compatibleBaseUrlHint", { type: t("anthropic") })}
/>
+
+ {showAdvanced && (
+
+ setFormData({ ...formData, chatPath: e.target.value })}
+ placeholder="/messages"
+ hint={t("chatPathHint")}
+ />
+ setFormData({ ...formData, modelsPath: e.target.value })}
+ placeholder={t("modelsPathPlaceholder")}
+ hint={t("modelsPathHint")}
+ />
+
+ )}
{
const nodeType = value.type || "openai-compatible";
@@ -836,12 +838,15 @@ export const updateProviderNodeSchema = z.object({
prefix: z.string().trim().min(1, "Prefix is required"),
apiType: z.enum(["chat", "responses"]).optional(),
baseUrl: z.string().trim().min(1, "Base URL is required"),
+ chatPath: z.string().trim().startsWith("/").max(500).optional().or(z.literal("")),
+ modelsPath: z.string().trim().startsWith("/").max(500).optional().or(z.literal("")),
});
export const providerNodeValidateSchema = z.object({
baseUrl: z.string().trim().min(1, "Base URL and API key required"),
apiKey: z.string().trim().min(1, "Base URL and API key required"),
type: z.enum(["openai-compatible", "anthropic-compatible"]).optional(),
+ modelsPath: z.string().trim().startsWith("/").max(500).optional().or(z.literal("")),
});
export const updateProviderConnectionSchema = z
diff --git a/tests/unit/custom-endpoint-paths.test.mjs b/tests/unit/custom-endpoint-paths.test.mjs
new file mode 100644
index 0000000000..79c4ad1085
--- /dev/null
+++ b/tests/unit/custom-endpoint-paths.test.mjs
@@ -0,0 +1,140 @@
+import { describe, it } from "node:test";
+import assert from "node:assert/strict";
+
+// Inline buildUrl logic from DefaultExecutor for unit testing
+// (avoids importing ESM modules with complex dependency chains)
+
+function buildUrlOpenAI(provider, credentials) {
+ const psd = credentials?.providerSpecificData;
+ const baseUrl = psd?.baseUrl || "https://api.openai.com/v1";
+ const normalized = baseUrl.replace(/\/$/, "");
+ const customPath = typeof psd?.chatPath === "string" && psd.chatPath ? psd.chatPath : null;
+ if (customPath) return `${normalized}${customPath}`;
+ const path = provider.includes("responses") ? "/responses" : "/chat/completions";
+ return `${normalized}${path}`;
+}
+
+function buildUrlAnthropic(credentials) {
+ const psd = credentials?.providerSpecificData;
+ const baseUrl = psd?.baseUrl || "https://api.anthropic.com/v1";
+ const normalized = baseUrl.replace(/\/$/, "");
+ const customPath = typeof psd?.chatPath === "string" && psd.chatPath ? psd.chatPath : null;
+ return `${normalized}${customPath || "/messages"}`;
+}
+
+function buildModelsUrl(baseUrl, modelsPath) {
+ const normalized = baseUrl.replace(/\/$/, "");
+ return `${normalized}${modelsPath || "/models"}`;
+}
+
+describe("Custom Endpoint Paths", () => {
+ describe("OpenAI Compatible buildUrl", () => {
+ it("returns custom chatPath when provided", () => {
+ const url = buildUrlOpenAI("openai-compatible-chat-abc123", {
+ providerSpecificData: {
+ baseUrl: "https://api.epsiloncode.pl",
+ chatPath: "/v4/chat/completions",
+ },
+ });
+ assert.equal(url, "https://api.epsiloncode.pl/v4/chat/completions");
+ });
+
+ it("returns default /chat/completions without chatPath", () => {
+ const url = buildUrlOpenAI("openai-compatible-chat-abc123", {
+ providerSpecificData: {
+ baseUrl: "https://api.openai.com/v1",
+ },
+ });
+ assert.equal(url, "https://api.openai.com/v1/chat/completions");
+ });
+
+ it("returns /responses for responses provider without chatPath", () => {
+ const url = buildUrlOpenAI("openai-compatible-responses-abc123", {
+ providerSpecificData: {
+ baseUrl: "https://api.openai.com/v1",
+ },
+ });
+ assert.equal(url, "https://api.openai.com/v1/responses");
+ });
+
+ it("treats empty string chatPath as default", () => {
+ const url = buildUrlOpenAI("openai-compatible-chat-abc123", {
+ providerSpecificData: {
+ baseUrl: "https://api.example.com/v1",
+ chatPath: "",
+ },
+ });
+ assert.equal(url, "https://api.example.com/v1/chat/completions");
+ });
+
+ it("strips trailing slash from baseUrl", () => {
+ const url = buildUrlOpenAI("openai-compatible-chat-abc123", {
+ providerSpecificData: {
+ baseUrl: "https://api.example.com/v1/",
+ chatPath: "/v4/chat/completions",
+ },
+ });
+ assert.equal(url, "https://api.example.com/v1/v4/chat/completions");
+ });
+ });
+
+ describe("Anthropic Compatible buildUrl", () => {
+ it("returns custom chatPath when provided", () => {
+ const url = buildUrlAnthropic({
+ providerSpecificData: {
+ baseUrl: "https://proxy.example.com/v2",
+ chatPath: "/v4/messages",
+ },
+ });
+ assert.equal(url, "https://proxy.example.com/v2/v4/messages");
+ });
+
+ it("returns default /messages without chatPath", () => {
+ const url = buildUrlAnthropic({
+ providerSpecificData: {
+ baseUrl: "https://api.anthropic.com/v1",
+ },
+ });
+ assert.equal(url, "https://api.anthropic.com/v1/messages");
+ });
+
+ it("treats empty string chatPath as default", () => {
+ const url = buildUrlAnthropic({
+ providerSpecificData: {
+ baseUrl: "https://api.anthropic.com/v1",
+ chatPath: "",
+ },
+ });
+ assert.equal(url, "https://api.anthropic.com/v1/messages");
+ });
+ });
+
+ describe("Validate endpoint modelsPath", () => {
+ it("uses modelsPath when provided", () => {
+ const url = buildModelsUrl("https://api.example.com/v1", "/v4/models");
+ assert.equal(url, "https://api.example.com/v1/v4/models");
+ });
+
+ it("falls back to /models when modelsPath is empty", () => {
+ const url = buildModelsUrl("https://api.example.com/v1", "");
+ assert.equal(url, "https://api.example.com/v1/models");
+ });
+
+ it("falls back to /models when modelsPath is undefined", () => {
+ const url = buildModelsUrl("https://api.example.com/v1", undefined);
+ assert.equal(url, "https://api.example.com/v1/models");
+ });
+ });
+
+ describe("No credentials fallback", () => {
+ it("works with null credentials for openai-compatible", () => {
+ const url = buildUrlOpenAI("openai-compatible-chat-abc123", null);
+ assert.equal(url, "https://api.openai.com/v1/chat/completions");
+ });
+
+ it("works with null credentials for anthropic-compatible", () => {
+ const url = buildUrlAnthropic(null);
+ assert.equal(url, "https://api.anthropic.com/v1/messages");
+ });
+ });
+});