Avoid retrying exhausted OpenAI quota

This commit is contained in:
Nick Beckley 2026-07-25 21:01:25 +00:00
parent 0f83f739c5
commit 24bafca765
2 changed files with 18 additions and 0 deletions

View File

@ -39,6 +39,7 @@ var tests = new (string Name, Action Test)[]
("Illustration starter generation plan skips successful items", IllustrationStarterGenerationPlanSkipsSuccessfulItems),
("Story Intelligence prototype uses starter illustration codes", StoryIntelligencePrototypeUsesStarterIllustrationCodes),
("Story Intelligence simulation mode remains available", StoryIntelligenceSimulationModeRemainsAvailable),
("Story Intelligence does not retry exhausted billing quota", StoryIntelligenceDoesNotRetryExhaustedBillingQuota),
("Story Intelligence visualisation contract omits raw manuscript text", StoryIntelligenceVisualisationContractOmitsRawManuscriptText),
("Illustration bulk retry result reports counts", IllustrationBulkRetryResultReportsCounts),
("Illustration bulk retry skips blocking duplicate stable codes", IllustrationBulkRetrySkipsBlockingDuplicateStableCodes),
@ -698,6 +699,14 @@ static void StoryIntelligenceSimulationModeRemainsAvailable()
Assert(prototype.Scenes.Any(scene => scene.PovCharacterId == "rosie"), "Simulation should still exercise POV transitions.");
}
static void StoryIntelligenceDoesNotRetryExhaustedBillingQuota()
{
var client = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine/Services/StoryIntelligenceOpenAIInfrastructure.cs"));
Assert(client.Contains("IsInsufficientQuota(responseText)", StringComparison.Ordinal), "OpenAI insufficient_quota responses must be detected before retrying.");
Assert(client.Contains("request was not retried", StringComparison.Ordinal), "OpenAI insufficient_quota responses should fail clearly without retry churn.");
}
static void StoryIntelligenceVisualisationContractOmitsRawManuscriptText()
{
var model = new StoryIntelligenceExperiencePrototypeViewModel

View File

@ -279,6 +279,11 @@ public sealed class StoryIntelligenceClient(
return (responseText, retryCount);
}
if (IsInsufficientQuota(responseText))
{
throw new InvalidOperationException($"OpenAI billing quota exhausted; request was not retried: {TrimForLog(responseText)}");
}
if (!ShouldRetry(response.StatusCode) || attempt == maxAttempts)
{
throw new InvalidOperationException($"OpenAI request failed with {(int)response.StatusCode} {response.StatusCode}: {TrimForLog(responseText)}");
@ -318,6 +323,10 @@ public sealed class StoryIntelligenceClient(
or HttpStatusCode.ServiceUnavailable
or HttpStatusCode.GatewayTimeout;
internal static bool IsInsufficientQuota(string responseText)
=> responseText.Contains("\"insufficient_quota\"", StringComparison.OrdinalIgnoreCase)
|| responseText.Contains("exceeded your current quota", StringComparison.OrdinalIgnoreCase);
private static Task DelayForRetryAsync(int attempt, CancellationToken cancellationToken)
=> Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt - 1)), cancellationToken);