Skip to content

fix(retry): lowercase header keys and surface Gemini RetryInfo - #363

Open
someotherdustin wants to merge 2 commits into
charmbracelet:mainfrom
someotherdustin:fix/retry-after-bugs
Open

someotherdustin wants to merge 2 commits into
charmbracelet:mainfrom
someotherdustin:fix/retry-after-bugs

Conversation

@someotherdustin

Copy link
Copy Markdown

Fantasy's retry handling looks up response headers by exact lowercase key ("retry-after", "retry-after-ms"), but two provider error paths never produced those keys, so retry-after hints from Anthropic, OpenAI, and Gemini were silently ignored.

toHeaderMap never lowercased its own output

providers/anthropic/error.go and providers/openai/error.go both had a toHeaderMap that copied http.Header's canonical MIME casing (e.g. "Retry-After") into its output map, while lowercasing a throwaway copy of the input map instead. retry.go's exact-case lookups never matched, so the retry-after handling never engaged for these two providers.

Fixed by lowercasing the output keys directly.

Gemini's retry hint is structured, not a header

Gemini doesn't send a Retry-After header on 429s — the real hint is a google.rpc.RetryInfo detail on genai.APIError. providers/google/error.go's toProviderErr dropped it entirely and left ResponseHeaders nil.

Added retryHeadersFromDetails to find the RetryInfo detail, parse its retryDelay duration, and synthesize a lowercase retry-after header so retry.go's getRetryDelayInMs picks it up the same way it does for the other providers.

Testing

  • go build ./... and go vet ./... pass
  • go test ./providers/anthropic/... ./providers/openai/... ./providers/google/... pass, including new regression tests:
    • TestToHeaderMap_LowercasesKeys (anthropic, openai)
    • TestToProviderErr_SurfacesRetryInfoDelay, TestToProviderErr_NoRetryInfoLeavesHeadersNil (google)
  • golangci-lint run on the touched packages: 0 issues

New-feature discussion: not applicable (bug fix).

AI assistance: this patch and its tests were prepared with Claude Code.

🤖 Generated with Claude Code

https://claude.ai/code/session_0167DoqhYHRGBgqFUChZ9hPA

someotherdustin and others added 2 commits September 12, 2026 07:21
Gemini's real 429 retry hint is a structured RetryInfo detail on
genai.APIError, not a header, so toProviderErr previously dropped it
entirely and left ResponseHeaders nil. Parse it and synthesize a
lowercase retry-after header so retry.go's getRetryDelayInMs picks it
up like it does for every other provider.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GHeU92PFdTi47wa2zCMS1o
toHeaderMap kept http.Header's canonical MIME casing (e.g.
"Retry-After") in its output map while only lowercasing a throwaway
copy of the input, so retry.go's exact-case "retry-after"/
"retry-after-ms" lookups never matched. Lowercase the output keys
directly so the existing retry-after handling actually engages.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GHeU92PFdTi47wa2zCMS1o
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant