diff --git a/src/Modules/TeamUp.Modules.Integrations/Ai/ModelClients.cs b/src/Modules/TeamUp.Modules.Integrations/Ai/ModelClients.cs index c312474..c0a765d 100644 --- a/src/Modules/TeamUp.Modules.Integrations/Ai/ModelClients.cs +++ b/src/Modules/TeamUp.Modules.Integrations/Ai/ModelClients.cs @@ -50,8 +50,7 @@ internal sealed class OpenAiCompatibleModelClient(HttpClient http) : IModelClien var stopwatch = Stopwatch.StartNew(); try { - var baseUrl = (request.Endpoint ?? "https://api.openai.com").TrimEnd('/'); - using var message = new HttpRequestMessage(HttpMethod.Post, $"{baseUrl}/v1/chat/completions"); + using var message = new HttpRequestMessage(HttpMethod.Post, ResolveChatUrl(request.Endpoint)); if (!string.IsNullOrEmpty(request.ApiKey)) { message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", request.ApiKey); @@ -110,6 +109,24 @@ internal sealed class OpenAiCompatibleModelClient(HttpClient http) : IModelClien } } + /// + /// Resolve the chat-completions URL from a BYOK endpoint. Accepts a base URL (we append the path), + /// a base already ending in /v1, or the full …/chat/completions URL pasted as-is — so + /// a user who enters the complete gateway URL doesn't get a doubled path. + /// + private static string ResolveChatUrl(string? endpoint) + { + var url = (endpoint ?? "https://api.openai.com").Trim().TrimEnd('/'); + if (url.Contains("/chat/completions", StringComparison.OrdinalIgnoreCase)) + { + return url; + } + + return url.EndsWith("/v1", StringComparison.OrdinalIgnoreCase) + ? $"{url}/chat/completions" + : $"{url}/v1/chat/completions"; + } + private static object[] BuildMessages(ModelRequest request) { if (request.Messages is not { Count: > 0 })