From 0af6dc3fc2c5a92b3be856ebb735984115cd5b35 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Dec 2025 06:43:20 +0000 Subject: [PATCH] Fix Azure OpenAI endpoint double /v1 issue Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com> --- apps/dokploy/server/api/routers/ai.ts | 26 ++++++++++++++++--- .../server/src/utils/ai/select-ai-provider.ts | 22 ++++++++++++++-- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/apps/dokploy/server/api/routers/ai.ts b/apps/dokploy/server/api/routers/ai.ts index e03da905d..96ce5e721 100644 --- a/apps/dokploy/server/api/routers/ai.ts +++ b/apps/dokploy/server/api/routers/ai.ts @@ -58,23 +58,43 @@ export const aiRouter = createTRPCRouter({ const providerName = getProviderName(input.apiUrl); const headers = getProviderHeaders(input.apiUrl, input.apiKey); let response = null; + let apiUrl = input.apiUrl; + switch (providerName) { case "ollama": - response = await fetch(`${input.apiUrl}/api/tags`, { headers }); + response = await fetch(`${apiUrl}/api/tags`, { headers }); break; case "gemini": response = await fetch( - `${input.apiUrl}/models?key=${encodeURIComponent(input.apiKey)}`, + `${apiUrl}/models?key=${encodeURIComponent(input.apiKey)}`, { headers: {} }, ); break; + case "azure": + // Azure OpenAI uses deployments endpoint + // Remove trailing /openai/v1 or /v1 if present + apiUrl = apiUrl.replace(/\/openai\/v1\/?$/, ""); + apiUrl = apiUrl.replace(/\/v1\/?$/, ""); + apiUrl = apiUrl.replace(/\/$/, ""); + + if (!input.apiKey) + throw new TRPCError({ + code: "BAD_REQUEST", + message: "API key must contain at least 1 character(s)", + }); + // Azure uses deployments endpoint to list models + response = await fetch( + `${apiUrl}/openai/deployments?api-version=2023-05-15`, + { headers }, + ); + break; default: if (!input.apiKey) throw new TRPCError({ code: "BAD_REQUEST", message: "API key must contain at least 1 character(s)", }); - response = await fetch(`${input.apiUrl}/models`, { headers }); + response = await fetch(`${apiUrl}/models`, { headers }); } if (!response.ok) { diff --git a/packages/server/src/utils/ai/select-ai-provider.ts b/packages/server/src/utils/ai/select-ai-provider.ts index 1967ff834..b204564f7 100644 --- a/packages/server/src/utils/ai/select-ai-provider.ts +++ b/packages/server/src/utils/ai/select-ai-provider.ts @@ -29,11 +29,22 @@ export function selectAIProvider(config: { apiUrl: string; apiKey: string }) { apiKey: config.apiKey, baseURL: config.apiUrl, }); - case "azure": + case "azure": { + // Azure OpenAI endpoints should not include /openai/v1 or /v1 at the end + // The SDK handles the path construction internally + let azureBaseUrl = config.apiUrl; + // Remove trailing /openai/v1 if present + azureBaseUrl = azureBaseUrl.replace(/\/openai\/v1\/?$/, ""); + // Remove trailing /v1 if present + azureBaseUrl = azureBaseUrl.replace(/\/v1\/?$/, ""); + // Remove trailing slash + azureBaseUrl = azureBaseUrl.replace(/\/$/, ""); + return createAzure({ apiKey: config.apiKey, - baseURL: config.apiUrl, + baseURL: azureBaseUrl, }); + } case "anthropic": return createAnthropic({ apiKey: config.apiKey, @@ -91,6 +102,13 @@ export const getProviderHeaders = ( apiUrl: string, apiKey: string, ): Record => { + // Azure OpenAI + if (apiUrl.includes("azure.com")) { + return { + "api-key": apiKey, + }; + } + // Anthropic if (apiUrl.includes("anthropic")) { return {