Skip to content

fix: make OpenRouter debug-fetch patch concurrency-safe - #416

Open
Greg Land (bikeusaland) wants to merge 9 commits into
langchain-ai:mainfrom
bikeusaland:fix/openrouter-fetch-concurrency
Open

fix: make OpenRouter debug-fetch patch concurrency-safe#416
Greg Land (bikeusaland) wants to merge 9 commits into
langchain-ai:mainfrom
bikeusaland:fix/openrouter-fetch-concurrency

Conversation

@bikeusaland

Copy link
Copy Markdown
Contributor

Summary

Fixes a concurrency hazard where the OpenRouter debug-fetch patch could leak permanently or be lost when two OpenWiki runs overlap in the same process (#411).

installOpenRouterDebugFetch swapped globalThis.fetch for the duration of a run and restored it blindly in a finally. With two overlapping runOpenWikiAgent calls — subagents, parallel tests, or a long-lived host process — the install/restore steps corrupted each other:

  1. Run A installs, capturing the real fetch as its original.
  2. Run B installs, capturing A's patched fetch as its "original".
  3. Run A restores the real fetch.
  4. Run B restores A's patched fetch — now leaked into the global forever.

Both runs also shared a single failure slot, so one run's captured error could overwrite another's.

Why not a per-model configuration.fetch hook

The issue suggested attaching diagnostics per-model like the Codex/Vertex clients do. That isn't available here: ChatOpenRouter (from @langchain/openrouter) extends BaseChatModel and calls globalThis.fetch directly — its constructor exposes no fetch/configuration field. A global wrapper is genuinely the only interception point, which is why the original code patched the global.

Fix

Make the global wrapper reference-counted:

  • Install the wrapper once, capturing the genuine original fetch.
  • Register a per-run failure sink so concurrent runs never share or clobber each other's captured failure. getLastFailure/clearLastFailure operate on the calling run's own sink, and debug events route to each run's own onEvent.
  • Restore the global only when the last run detaches, reverting to the true original — never to a wrapper installed by a concurrent run.
  • Redundant restore() calls on the same handle are idempotent (can't prematurely restore while another run is active).

The OpenRouterFetchCapture return contract (getLastFailure, clearLastFailure, restore) is unchanged, so callers are untouched.

Tests

New test/openrouter-debug-fetch.test.ts (4 tests): single-run restore returns the exact original; overlapping runs each capture their own 429 while restore stays reference-counted; non-OpenRouter requests pass through untouched; redundant restore is a no-op that doesn't disturb an active run.

Full suite green: typecheck, eslint ., prettier --check, and all tests pass.

Closes #411

🤖 Generated with Claude Code

Greg Land (bikeusaland) and others added 9 commits July 20, 2026 16:20
installOpenRouterDebugFetch swapped globalThis.fetch for the whole run and
restored it blindly in a finally. Two overlapping runOpenWikiAgent calls
(subagents, parallel tests, a long-lived host) corrupted each other: the second
install captured the first's patched fetch as its "original", and the first
restore clobbered the second's patch — so the wrapper leaked permanently or was
lost, and both runs shared a single failure slot.

ChatOpenRouter extends BaseChatModel and calls globalThis.fetch directly with no
injectable fetch (unlike the OpenAI/Codex/Vertex clients, which take a
configuration.fetch hook), so a global wrapper is the only interception point.
Make that wrapper reference-counted: install it once against the genuine
original fetch, register a per-run failure sink so concurrent runs never share
or clobber each other's captured failure (debug events still route to each run's
own options), and restore the global only when the last run detaches. Redundant
restore() calls are idempotent.

Exported for testing; new coverage pins the concurrency contract.

Fixes langchain-ai#411

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

Global fetch monkey-patch for OpenRouter diagnostics is not concurrency-safe

1 participant