Skip to content

fix(agent): retry OpenRouter 429s at the fetch boundary - #667

Open
yzxcj797 wants to merge 1 commit into
langchain-ai:mainfrom
yzxcj797:fix/openrouter-429-retry-664
Open

fix(agent): retry OpenRouter 429s at the fetch boundary#667
yzxcj797 wants to merge 1 commit into
langchain-ai:mainfrom
yzxcj797:fix/openrouter-429-retry-664

Conversation

@yzxcj797

Copy link
Copy Markdown

Summary

Fixes #664.

An OpenRouter free-tier 429 (no Retry-After) aborted the whole run on first occurrence: LangChain's AsyncCaller classifies a Retry-After-less 429 as non-retryable capacity and throws immediately, so OPENWIKI_PROVIDER_RETRY_ATTEMPTS was never spent. #461's createProviderRetryFetch can't help here — it rides the OpenAI SDK configuration option, which ChatOpenRouter doesn't accept — and its flat 1 s fallback couldn't ride out a per-minute window regardless.

What this does

Retries at the fetch boundary installOpenRouterDebugFetch already owns (it wraps every OpenRouter chat-completions fetch):

  • Delay hints, in priority order: Retry-After (seconds or HTTP-date) → X-RateLimit-Reset (unix-ms epoch, with seconds-magnitude detection for providers that send seconds, plus a 250 ms margin so the retry lands after the window actually resets) → exponential backoff (1 s doubling, capped at 30 s). Header-derived waits are defensively capped at 5 minutes.
  • Budget: the existing OPENWIKI_PROVIDER_RETRY_ATTEMPTS (default 3), resolved tolerantly at install time so an invalid env value still surfaces through run-config validation with its config stage attribution.
  • Re-send safety: retries only run when the request is re-sendable — string/absent body in init, and input isn't a Request object carrying a body (a consumed stream body can't be re-sent).
  • Abort propagation: the wait rejects with the abort signal's reason, so cancellation stays prompt mid-backoff.
  • Diagnostics contract unchanged: the final non-ok response is still captured for attachOpenRouterDebugInfo (Error Diagnostics block), and each retry emits a debug line (openrouter.retry attempt=N/M delayMs=…).

Tests

test/agent/openrouter-retry.test.ts (vitest, follows gemini-retry.test.ts conventions):

  • end-to-end through the wrapper: a Retry-After-less 429 with X-RateLimit-Reset is retried and the wait honours the reset instant;
  • attempt-budget exhaustion: 1 + N fetch calls, final 429 returned to the caller with diagnostics captured;
  • non-OpenRouter URLs and non-resendable bodies pass through without retry;
  • a network error during retry keeps the fetchError diagnostics;
  • unit coverage of computeOpenRouterRetryDelayMs: Retry-After seconds and HTTP-date, ms- and seconds-epoch resets, exponential growth with caps.

OpenRouter's platform-side 429s (free-models-per-min) carry no
Retry-After, which the LangChain AsyncCaller classifies as
non-retryable capacity and turns into an immediate abort, so
OPENWIKI_PROVIDER_RETRY_ATTEMPTS never applied and ~50s of agent work
was discarded on the first 429 (langchain-ai#664). langchain-ai#461's createProviderRetryFetch
only reaches ChatOpenAI-backed transports -- ChatOpenRouter has no
configuration option -- and its flat 1s fallback cannot ride out a
per-minute window anyway.

Retry in installOpenRouterDebugFetch, which already wraps every
OpenRouter chat-completions fetch:

- honour Retry-After (seconds or HTTP-date), then OpenRouter's
  X-RateLimit-Reset epoch (unix-ms, seconds-magnitude detection, small
  margin), then exponential backoff (1s doubling, 30s cap); header-
  derived waits are defensively capped at 5 min;
- spend the existing OPENWIKI_PROVIDER_RETRY_ATTEMPTS budget;
- only retry when the request is re-sendable (string/absent body, not a
  Request carrying a body) and propagate abort signals through the wait;
- keep the diagnostics contract: the final non-ok response is captured
  for attachOpenRouterDebugInfo, retries emit debug lines.
@changeset-bot

changeset-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 6cdb7a1

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

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.

[Bug]: OpenRouter free-tier 429 without Retry-After aborts the run; OPENWIKI_PROVIDER_RETRY_ATTEMPTS never applies

1 participant