using System.Net.Http.Headers; using System.Text; using System.Text.Json; using Microsoft.Extensions.Options; namespace MealPlan.Api.Services.AI; public class OpenAiChatService : IOpenAiChatService { private readonly HttpClient _http; private readonly OpenAiOptions _options; private readonly ILogger _logger; public OpenAiChatService(HttpClient http, IOptions options, ILogger logger) { _http = http; _options = options.Value; _logger = logger; } public bool IsConfigured => _options.IsConfigured; public async Task CompleteJsonAsync( string systemPrompt, string userPrompt, OpenAiUseCase useCase = OpenAiUseCase.Default, CancellationToken ct = default) { if (!IsConfigured) { throw new InvalidOperationException( "OpenAI is not configured. Set OpenAI__ApiKey in appsettings.Local.json."); } var model = _options.ResolveModel(useCase); var payload = new { model, max_tokens = _options.MaxTokens, response_format = new { type = "json_object" }, messages = new[] { new { role = "system", content = systemPrompt }, new { role = "user", content = userPrompt }, }, }; using var request = new HttpRequestMessage(HttpMethod.Post, "https://api.openai.com/v1/chat/completions"); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _options.ApiKey); request.Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"); using var response = await _http.SendAsync(request, ct); var body = await response.Content.ReadAsStringAsync(ct); if (!response.IsSuccessStatusCode) { _logger.LogWarning("OpenAI API error {Status}: {Body}", response.StatusCode, body); throw new InvalidOperationException("OpenAI request failed. Check API key and quota."); } using var doc = JsonDocument.Parse(body); var content = doc.RootElement .GetProperty("choices")[0] .GetProperty("message") .GetProperty("content") .GetString() ?? "{}"; return StripMarkdownFence(content); } private static string StripMarkdownFence(string content) { var trimmed = content.Trim(); if (!trimmed.StartsWith("```", StringComparison.Ordinal)) return trimmed; var firstNewline = trimmed.IndexOf('\n'); if (firstNewline < 0) return trimmed; var endFence = trimmed.LastIndexOf("```", StringComparison.Ordinal); if (endFence <= firstNewline) return trimmed; return trimmed[(firstNewline + 1)..endFence].Trim(); } }