Skip to content

fix(llm): stop consuming the source iterable when the stream consumer cancels - #726

Open
Matthew-Selvam wants to merge 3 commits into
libredb:mainfrom
Matthew-Selvam:fix/gemini-stream-cancel
Open

fix(llm): stop consuming the source iterable when the stream consumer cancels#726
Matthew-Selvam wants to merge 3 commits into
libredb:mainfrom
Matthew-Selvam:fix/gemini-stream-cancel

Conversation

@Matthew-Selvam

Copy link
Copy Markdown
Contributor

Summary

streamFromAsyncIterable — the helper behind every Gemini streaming completion (GeminiProvider.createStreamFromResult) — kept pulling from the SDK iterable after the consumer cancelled the stream. Two consequences:

  1. It generated (and billed) a full completion nobody reads.
  2. The next controller.enqueue on the cancelled stream threw TypeError into the generic catch, surfacing the client-side abort as a spurious stream error.

Fix

  • The pull loop checks controller.desiredSize === null (the closed/cancelled/errored signal) before each transform/enqueue and exits quietly instead of draining the rest of the iterable.
  • A cancel racing the loop can still make an enqueue or the final close throw; the new isStreamCancelled helper recognizes that shape (null desiredSize, or a TypeError naming cancellation/closed/invalid-state — message text varies across runtimes) and treats it as a normal exit.
  • Real pipeline failures still controller.error(...) — the existing handles error in iterable test pins that.

Test plan

  • New: stops consuming the iterable when the consumer cancels — 100-item iterable, read one chunk, cancel, assert pulled < 5
  • New: direct isStreamCancelled suite covering all three branches (live controller + real error → false; TypeError naming cancel/closed/invalid state → true; null desiredSize → true)
  • Existing streaming suite (transform/skip/error/empty paths) unchanged

Found during a broader code review of the LLM layer; no issue existed yet for it.

… cancels

streamFromAsyncIterable kept pulling from the SDK iterable after the
consumer cancelled — generating (and billing) a full completion nobody
reads — until a post-cancel enqueue threw TypeError into the generic
catch and surfaced as a spurious stream error. Every aborted Gemini
completion took this path (the only consumer of the helper).

The pull loop now checks controller.desiredSize === null before each
transform/enqueue and exits quietly; a cancel racing the loop (the
enqueue/close throw) is recognized by isStreamCancelled and treated as a
normal exit. Real pipeline failures still error the stream.
…ch deterministically

Two CI failures in the new isStreamCancelled suite:

- The message regex used 'cancell' (double-L), which misses the American
  'canceled' spelling that runtimes actually emit — and that the test
  itself used. /cancel/ matches both.
- reader.cancel() does NOT null desiredSize in Bun's runtime (it stays 0),
  so the closed-state branch is now reached via controller.close(), which
  the spec defines as null. Also reformats the probe construction to
  satisfy Biome.
…nly null case

Per the WHATWG spec, desiredSize is null ONLY for an errored stream —
closed and cancelled streams keep a numeric desiredSize (Bun's runtime
confirmed: 0 after both close() and reader.cancel()). The in-loop
desiredSize === null bail-out was therefore unreachable in production:
this code only ever reaches the errored state through controller.error()
AFTER the check. Dead code, and a 100%-coverage-gate failure.

The cancel signal is the enqueue/close throw itself: a cancelled stream
makes the next enqueue throw TypeError, which isStreamCancelled now
recognizes. The direct unit test reaches the null branch via
controller.error() — the one state where the spec makes it null — and the
import line is split for Biome's line width.
@codecov

codecov Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@cevheri cevheri added the loop:needs-moderator-action Flagged by the maintainer loop: suspicious content or a decision only a human can make label Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

loop:needs-moderator-action Flagged by the maintainer loop: suspicious content or a decision only a human can make

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants