using System.Text; using System.Text.Json.Nodes; using CareFix.Api.Infrastructure; using CareFix.Api.Options; using Microsoft.Extensions.Options; namespace CareFix.Api.Ai; /// Thin client for the Anthropic Messages API with tool use and retries. public sealed class ClaudeClient(IHttpClientFactory http, IOptions options, AiUsageService usage, ILogger log) { public const string HttpClientName = "claude"; public async Task CreateMessageAsync(JsonArray system, JsonArray? tools, JsonArray messages, CancellationToken ct, int? maxTokens = null, UsageContext? usageContext = null) { var o = options.Value.Claude; if (string.IsNullOrWhiteSpace(o.ApiKey)) throw new AppException(500, "The Claude API key is not configured (CareFix:Claude:ApiKey)."); var req0 = new JsonObject { ["model"] = o.Model, ["max_tokens"] = maxTokens ?? o.MaxTokens, ["system"] = system.DeepClone(), ["messages"] = messages.DeepClone(), }; if (tools is { Count: > 0 }) req0["tools"] = tools.DeepClone(); var body = req0.ToJsonString(); for (var attempt = 1; ; attempt++) { using var req = new HttpRequestMessage(HttpMethod.Post, $"{o.BaseUrl.TrimEnd('/')}/v1/messages") { Content = new StringContent(body, Encoding.UTF8, "application/json"), }; req.Headers.Add("x-api-key", o.ApiKey); req.Headers.Add("anthropic-version", "2023-06-01"); var client = http.CreateClient(HttpClientName); HttpResponseMessage resp; try { resp = await client.SendAsync(req, ct); } catch (HttpRequestException ex) when (attempt < 3) { log.LogWarning(ex, "Claude request failed, retrying (attempt {Attempt})", attempt); await Task.Delay(TimeSpan.FromSeconds(2 * attempt), ct); continue; } using (resp) { var text = await resp.Content.ReadAsStringAsync(ct); if (resp.IsSuccessStatusCode) { var parsed = JsonNode.Parse(text)!.AsObject(); await usage.RecordAsync(o.Model, parsed["usage"], usageContext, ct); return parsed; } var code = (int)resp.StatusCode; if ((code is 429 or 529 || code >= 500) && attempt < 3) { log.LogWarning("Claude returned {Code}, retrying (attempt {Attempt})", code, attempt); await Task.Delay(TimeSpan.FromSeconds(3 * attempt), ct); continue; } log.LogError("Claude error {Code}: {Body}", code, text); throw new AppException(502, $"The AI service returned an error ({code}). Try again in a minute."); } } } }