using System.Net.Http.Headers; using System.Text.Json; using Toak.Api.Models; using Toak.Serialization; using Toak.Core; using Toak.Core.Interfaces; namespace Toak.Api; public class OpenAiCompatibleClient : ISpeechClient, ILlmClient { private readonly HttpClient _httpClient; private readonly string? _reasoningEffort; public OpenAiCompatibleClient(string apiKey, string baseUrl = "https://api.groq.com/openai/v1/", string? reasoningEffort = null) { _httpClient = new HttpClient(); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); _httpClient.BaseAddress = new Uri(baseUrl); _reasoningEffort = reasoningEffort == "none" ? null : reasoningEffort; } public async Task TranscribeAsync(string filePath, string language = "", string model = Constants.Defaults.WhisperModel) { // ... (TranscribeAsync content remains same except maybe some internal comments or contexts) using var content = new MultipartFormDataContent(); await using var fileStream = File.OpenRead(filePath); using var streamContent = new StreamContent(fileStream); streamContent.Headers.ContentType = new MediaTypeHeaderValue("audio/wav"); // or mpeg content.Add(streamContent, "file", Path.GetFileName(filePath)); var modelToUse = string.IsNullOrWhiteSpace(model) ? Constants.Defaults.WhisperModel : model; content.Add(new StringContent(modelToUse), "model"); if (!string.IsNullOrWhiteSpace(language)) { var firstLang = language.Split(',')[0].Trim(); content.Add(new StringContent(firstLang), "language"); } Logger.LogDebug($"Sending Whisper API request ({modelToUse})..."); var response = await _httpClient.PostAsync("audio/transcriptions", content); Logger.LogDebug($"Whisper API response status: {response.StatusCode}"); if (!response.IsSuccessStatusCode) { var error = await response.Content.ReadAsStringAsync(); throw new Exception($"Whisper API Error: {response.StatusCode} - {error}"); } var json = await response.Content.ReadAsStringAsync(); var result = JsonSerializer.Deserialize(json, AppJsonSerializerContext.Default.WhisperResponse); return result?.Text ?? string.Empty; } public async Task RefineTextAsync(string rawTranscript, string systemPrompt, string model = Constants.Defaults.LlmModel) { var requestBody = new OpenAiRequest { Model = string.IsNullOrWhiteSpace(model) ? Constants.Defaults.LlmModel : model, Temperature = 0.0, ReasoningEffort = _reasoningEffort, Messages = [ new OpenAiRequestMessage { Role = "system", Content = systemPrompt }, new OpenAiRequestMessage { Role = "user", Content = $"{rawTranscript}" } ] }; var jsonContent = new StringContent(JsonSerializer.Serialize(requestBody, AppJsonSerializerContext.Default.OpenAiRequest), System.Text.Encoding.UTF8, "application/json"); Logger.LogDebug($"Sending OpenAi API request (model: {requestBody.Model})..."); var response = await _httpClient.PostAsync("chat/completions", jsonContent); Logger.LogDebug($"OpenAi API response status: {response.StatusCode}"); if (!response.IsSuccessStatusCode) { var error = await response.Content.ReadAsStringAsync(); throw new Exception($"OpenAi API Error: {response.StatusCode} - {error}"); } var json = await response.Content.ReadAsStringAsync(); var result = JsonSerializer.Deserialize(json, AppJsonSerializerContext.Default.OpenAiResponse); return result?.Choices?.FirstOrDefault()?.Message?.Content ?? string.Empty; } public async IAsyncEnumerable RefineTextStreamAsync(string rawTranscript, string systemPrompt, string model = Constants.Defaults.LlmModel) { var requestBody = new OpenAiRequest { Model = string.IsNullOrWhiteSpace(model) ? Constants.Defaults.LlmModel : model, Temperature = 0.0, Stream = true, ReasoningEffort = _reasoningEffort, Messages = [ new OpenAiRequestMessage { Role = "system", Content = systemPrompt }, new OpenAiRequestMessage { Role = "user", Content = $"{rawTranscript}" } ] }; var jsonContent = new StringContent(JsonSerializer.Serialize(requestBody, AppJsonSerializerContext.Default.OpenAiRequest), System.Text.Encoding.UTF8, "application/json"); using var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); request.Content = jsonContent; request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream")); Logger.LogDebug($"Sending OpenAi Steam API request (model: {requestBody.Model})..."); using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); Logger.LogDebug($"OpenAi Stream API response status: {response.StatusCode}"); if (!response.IsSuccessStatusCode) { var error = await response.Content.ReadAsStringAsync(); throw new Exception($"OpenAi API Error: {response.StatusCode} - {error}"); } await using var stream = await response.Content.ReadAsStreamAsync(); using var reader = new StreamReader(stream); string? line; while ((line = await reader.ReadLineAsync()) != null) { if (string.IsNullOrWhiteSpace(line)) continue; if (line.StartsWith("data: ")) { var data = line.Substring("data: ".Length).Trim(); if (data == "[DONE]") break; var chunk = JsonSerializer.Deserialize(data, AppJsonSerializerContext.Default.OpenAiStreamResponse); var content = chunk?.Choices?.FirstOrDefault()?.Delta?.Content; if (!string.IsNullOrEmpty(content)) { yield return content; } } } } }