Skip to content

fix(openai): honor Gemini RetryInfo retryDelay on 429/503 retries - #271

Merged
plombeer31 merged 1 commit into
mainfrom
fix/issue-106-openai-retry-handling-ignores
Aug 31, 2026
Merged

fix(openai): honor Gemini RetryInfo retryDelay on 429/503 retries#271
plombeer31 merged 1 commit into
mainfrom
fix/issue-106-openai-retry-handling-ignores

Conversation

@plombeer31

@plombeer31 plombeer31 commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

What was broken

Gemini's OpenAI-compatible endpoint advertises its cooldown only as structured error JSON — a google.rpc.RetryInfo entry in error.details[] whose retryDelay is a protobuf Duration string ("39s", "1.5s") — and sends no Retry-After header. httpErrorFromResponse in src/llm/provider/openai/openai-http.ts derived retryAfterMs only from the header, so resolveWaitMs never saw the provider cooldown and throttled requests exhausted all three attempts on the 150/300 ms exponential backoff before the advertised cooldown elapsed.

The fix

When the retry-after header is absent on a 429/503, httpErrorFromResponse now falls back to parseRetryInfoDelayMs(), which JSON-parses the already-read body, walks error.details[] for a string retryDelay matching ^(\d+(?:\.\d+)?)s$, and returns milliseconds. The value flows through the existing retryAfterMs field, so nothing else changes:

  • a valid Retry-After header still takes precedence;
  • resolveWaitMs still caps the wait at OPENAI_RETRY_AFTER_CAP_MS (5 s), so a "39s" cooldown cannot stall an interactive turn or sleep unboundedly;
  • the cancellation-aware sleep() still lets a caller abort interrupt the wait;
  • malformed, negative, or non-string durations are ignored and the plain backoff applies;
  • deterministic 4xx behavior and the metadata gate (429/503 only) are untouched.

Test evidence

New fake-timer tests (pinned Math.random zeroes the jitter; no wall-clock delay) in src/llm/provider/openai/openai-http.test.ts cover every acceptance criterion: headerless 429 with "1.5s" and 503 with "2s" control the retry timing, "not-a-duration" / "-1s" / non-string values fall back to the 150 ms backoff, a valid header beats the structured delay, "39s" is capped at 5 s, a 500 carrying RetryInfo-shaped JSON ignores it, and caller abort interrupts the pending wait.

  • npx vitest run src/llm/provider/openai — 95 passed (9 files)
  • with the source fix stashed, the three behavior-bearing new tests fail (429/503 honoring + cap), confirming they gate the fix
  • npm run lint (tsc --noEmit) — clean
  • npm run build — clean

Note: open PR #267 addresses the same issue with an equivalent approach; this PR was produced from an independent verification pass against v0.4.2 main (4caff55) and adds a regression test that the 429/503-only gate holds. Maintainers can merge either.

Fixes #106

Gemini's OpenAI-compatible endpoint advertises its cooldown only as
google.rpc.RetryInfo inside the error JSON (error.details[].retryDelay,
a protobuf Duration like "39s") and sends no retry-after header. The
retry loop derived retryAfterMs solely from the header, so throttled
requests burned all three attempts on the 150/300ms backoff before the
provider's cooldown had elapsed.

httpErrorFromResponse now falls back to parsing that structured delay
from the already-read body on 429/503 when the header is absent. The
value flows through the existing retryAfterMs field, so header
precedence, the 5s interactive cap in resolveWaitMs, and the
cancellation-aware sleep all apply unchanged; malformed or negative
durations are ignored. Fake-timer tests cover each acceptance criterion
of #106.

Fixes #106

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@plombeer31
plombeer31 merged commit 6731c29 into main Aug 31, 2026
2 checks passed
plombeer31 pushed a commit that referenced this pull request Aug 31, 2026
…6-08-31

# Conflicts:
#	src/agent/step-executor.ts
#	src/config/config-schema.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenAI retry handling ignores Gemini error.details[].retryDelay metadata

1 participant