using System.Net.Http.Json; using System.Text.Json; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using TarninaHub.Application.Matchmaking; namespace TarninaHub.Infrastructure.Llm; /// /// OpenAI-compatible chat completion provider. Interchangeable behind /// (ADR-0005). The API key is read from /// configuration/secrets and never logged. /// public sealed class OpenAiCompatibleLlmProvider : ILlmProvider { private readonly HttpClient _httpClient; private readonly LlmSettings _settings; private readonly ILogger _logger; public OpenAiCompatibleLlmProvider( HttpClient httpClient, IOptions settings, ILogger logger) { _httpClient = httpClient; _settings = settings.Value; _logger = logger; } public bool IsConfigured => _settings.IsConfigured; public async Task CompleteAsync( string systemPrompt, string userPrompt, CancellationToken cancellationToken) { if (!IsConfigured) { return null; } var endpoint = _settings.BaseUrl.TrimEnd('/') + "/chat/completions"; var payload = new { model = _settings.Model, max_tokens = _settings.MaxTokens, messages = new object[] { new { role = "system", content = systemPrompt }, new { role = "user", content = userPrompt }, }, }; try { using var request = new HttpRequestMessage(HttpMethod.Post, endpoint) { Content = JsonContent.Create(payload), }; request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue( "Bearer", _settings.ApiKey); using var response = await _httpClient.SendAsync(request, cancellationToken); if (!response.IsSuccessStatusCode) { _logger.LogWarning("LLM provider returned status {Status}.", (int)response.StatusCode); return null; } var document = await response.Content.ReadFromJsonAsync(cancellationToken); return document .GetProperty("choices")[0] .GetProperty("message") .GetProperty("content") .GetString(); } catch (Exception exception) when (exception is not OperationCanceledException) { _logger.LogWarning(exception, "LLM completion failed; falling back to template rationale."); return null; } } }