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.");
}
}
}
}