Fix rate limit errors in AI price check

Tier 1 Anthropic accounts are capped at 8,000 output tokens/minute on
Sonnet. 3 concurrent batches burst well past that, causing 429s.

- MaxConcurrentBatches: 3 → 1 (sequential prevents burst)
- Add retry: on rate_limit_error, wait 65s then retry up to 3 times
  so the per-minute window resets before the next attempt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-25 20:54:30 -04:00
parent 740238a939
commit 7407d1cd96
@@ -1,3 +1,4 @@
using System.Net.Http;
using System.Text;
using System.Text.Json;
using Anthropic.SDK;
@@ -20,8 +21,9 @@ public class AiCatalogPriceCheckService : IAiCatalogPriceCheckService
private readonly ILogger<AiCatalogPriceCheckService> _logger;
private const string Model = "claude-sonnet-4-6";
private const int BatchSize = 25; // 25 items × ~80 tokens (with word limits) ≈ 2000 output tokens, well within 8192
private const int MaxConcurrentBatches = 3; // cap parallel API calls to stay within rate limits
private const int BatchSize = 25;
private const int MaxConcurrentBatches = 1; // Tier 1 output limit is 8,000 TPM — sequential avoids bursting past it
private const int RateLimitRetrySeconds = 65; // wait just past the 60s window before retrying a 429
private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNameCaseInsensitive = true };
@@ -78,10 +80,30 @@ public class AiCatalogPriceCheckService : IAiCatalogPriceCheckService
return "[]";
}
private static async Task<MessageResponse> SendAsync(AnthropicClient client, MessageParameters parameters)
/// <summary>
/// Sends a message to Claude with up to 3 attempts. On a rate-limit 429, waits
/// RateLimitRetrySeconds before retrying so the per-minute token window can reset.
/// </summary>
private async Task<MessageResponse> SendAsync(AnthropicClient client, MessageParameters parameters)
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(90));
return await client.Messages.GetClaudeMessageAsync(parameters, cts.Token);
const int maxAttempts = 3;
for (var attempt = 1; attempt <= maxAttempts; attempt++)
{
try
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(90));
return await client.Messages.GetClaudeMessageAsync(parameters, cts.Token);
}
catch (HttpRequestException ex) when (attempt < maxAttempts && ex.Message.Contains("rate_limit_error"))
{
_logger.LogWarning("Rate limit hit (attempt {Attempt}/{Max}), waiting {Seconds}s before retry",
attempt, maxAttempts, RateLimitRetrySeconds);
await Task.Delay(TimeSpan.FromSeconds(RateLimitRetrySeconds));
}
}
// Final attempt — let any exception propagate to the batch error handler
using var finalCts = new CancellationTokenSource(TimeSpan.FromSeconds(90));
return await client.Messages.GetClaudeMessageAsync(parameters, finalCts.Token);
}
/// <inheritdoc/>