Skip to content

Terminate OpenAI streams when response.failed arrives - #6920

Merged
tim-smart merged 2 commits into
mainfrom
audit/repro-split-adapters-ai-6-openai-failed-stream-nontermination
Aug 3, 2026
Merged

Terminate OpenAI streams when response.failed arrives#6920
tim-smart merged 2 commits into
mainfrom
audit/repro-split-adapters-ai-6-openai-failed-stream-nontermination

Conversation

@fubhy

@fubhy fubhy commented Aug 3, 2026

Copy link
Copy Markdown
Member

Summary

Valid failed Responses API streams can wait indefinitely, and WebSocket streams can retain their semaphore after the terminal failure event.

Important

This PR starts with focused failing reproduction tests. Add the implementation fix to this same branch; CI is expected to fail until that fix is included.

response.failed does not terminate OpenAI streams

Module: ai/openai/OpenAiClient
Audit ID: adapters-ai-6-openai-failed-stream-nontermination
Severity / confidence: high / high

What happens

Valid failed Responses API streams can wait indefinitely, and WebSocket streams can retain their semaphore after the terminal failure event.

Why it happens

Both the HTTP SSE and WebSocket terminal predicates omit response.failed.

Expected behavior

response.failed is a terminal Responses API event alongside response.completed and response.incomplete.

Relevant implementation

These links and excerpts are pinned to audit base c9b56ab507f224426ee8388dc450da447ec4715f.

View problematic code at packages/ai/openai/src/OpenAiClient.ts:268-275
      const stream = response.stream.pipe(
        Stream.decodeText(),
        Stream.pipeThroughChannel(Sse.decodeDataSchema(OpenAiSchema.ResponseStreamEvent)),
        Stream.takeUntil((event) =>
          event.data.type === "response.completed" ||
          event.data.type === "response.incomplete"
        ),
        Stream.map((event) => event.data),

View exact lines on GitHub

View problematic code at packages/ai/openai/src/OpenAiClient.ts:638-643
        return Stream.fromQueue(incoming).pipe(
          Stream.takeUntil((e) => {
            done = e.type === "response.completed" || e.type === "response.incomplete"
            return done
          })
        )

View exact lines on GitHub

Reproduction

pnpm test --run packages/ai/openai/test/OpenAiFailedStreamNonterminationAudit.test.ts

Observed failure: The response.failed stream timed out instead of completing.

Implementation handoff

The initial reproduction tests on this branch are the regression specification for the implementation fix that should follow in this PR.

  1. Start with the pinned implementation excerpts and the Why it happens analysis above.
  2. Change the implementation so it satisfies the stated Expected behavior; do not weaken or remove the reproduction assertions.
  3. Run the focused reproduction command(s) and confirm the observed failures become passing tests:
pnpm test --run packages/ai/openai/test/OpenAiFailedStreamNonterminationAudit.test.ts
  1. Run the affected package's existing tests, then the repository lint and type checks before requesting review.

Audit provenance

  • Audit base: c9b56ab507f224426ee8388dc450da447ec4715f
  • Reproduction base: 8f9499f562729f5f7b08d8bcc4db86b4aeff8a21
  • Findings: adapters-ai-6-openai-failed-stream-nontermination
  • Initial patch: focused reproduction tests; implementation fix pending

Closes EFF-339

@fubhy fubhy added the audit Findings originating from the Effect runtime correctness audit label Aug 3, 2026
@github-project-automation github-project-automation Bot moved this to Discussion Ongoing in PR Backlog Aug 3, 2026
@changeset-bot

changeset-bot Bot commented Aug 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 383fdc3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 30 packages
Name Type
@effect/ai-openai Patch
effect Patch
@effect/opentelemetry Patch
@effect/platform-browser Patch
@effect/platform-bun Patch
@effect/platform-deno Patch
@effect/platform-node-shared Patch
@effect/platform-node Patch
@effect/vitest Patch
@effect/ai-anthropic Patch
@effect/ai-openai-compat Patch
@effect/ai-openrouter Patch
@effect/atom-react Patch
@effect/atom-solid Patch
@effect/atom-vue Patch
@effect/sql-clickhouse Patch
@effect/sql-d1 Patch
@effect/sql-libsql Patch
@effect/sql-mssql Patch
@effect/sql-mysql2 Patch
@effect/sql-pg Patch
@effect/sql-pglite Patch
@effect/sql-sqlite-bun Patch
@effect/sql-sqlite-do Patch
@effect/sql-sqlite-node Patch
@effect/sql-sqlite-react-native Patch
@effect/sql-sqlite-wasm Patch
@effect/docgen Patch
@effect/doctest Patch
@effect/openapi-generator Patch

Not sure what this means? Click here to learn what changesets are.

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

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The reproduction test is well-constructed, but the implementation fix it's meant to validate is missing from this PR. Both terminal predicates need updating, and the WebSocket path needs a matching test.

Reviewed changes

A new reproduction test for the bug where response.failed SSE events do not terminate an OpenAI Responses API stream.

  • OpenAiFailedStreamNonterminationAudit.test.ts: One test that sends a response.failed SSE event into createResponseStream and asserts the stream terminates rather than hanging indefinitely.

⚠️ Implementation fix missing

The terminal predicates that cause this bug are still unchanged:

  • OpenAiClient.ts:271-274 (HTTP SSE): Stream.takeUntil checks only response.completed and response.incompleteresponse.failed is absent.
  • OpenAiClient.ts:638-643 (WebSocket): Same omission — the done flag is only set for response.completed and response.incomplete.

Both need e.type === "response.failed" added to their predicates.

Technical details
# Add `response.failed` to both terminal predicates

## Affected sites
- `packages/ai/openai/src/OpenAiClient.ts:271` — SSE `takeUntil` predicate
- `packages/ai/openai/src/OpenAiClient.ts:640` — WebSocket `takeUntil` predicate

## Required outcome
Both terminal predicates must treat `response.failed` as a terminal event alongside `response.completed` and `response.incomplete`, so the stream completes promptly when that event arrives.

## Suggested approach
Change `event.data.type === "response.completed" || event.data.type === "response.incomplete"` to include `event.data.type === "response.failed"` (and the equivalent in the WebSocket path).

⚠️ Missing WebSocket test

The PR description identifies this bug in both HTTP SSE and WebSocket streams (OpenAiClient.ts:638-643), but the reproduction test only covers the SSE path. A corresponding WebSocket test should be added.

ℹ️ Nitpicks

  • it.live is unique in this codebase (all existing AI provider tests use it.effect). It's the right tool here — Effect.timeoutOption needs a real clock — but worth calling out. An alternative is to use TestClock and explicitly advance it in a it.effect test, which would keep runner isolation but is arguably more verbose.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) (Kimi K2 not used — the program covers this model; add its provider key to run your pick) | 𝕏

Comment thread packages/ai/openai/test/OpenAiFailedStreamNonterminationAudit.test.ts Outdated
@tim-smart
tim-smart enabled auto-merge (squash) August 3, 2026 21:31

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes since the prior pullfrog review (14ffdf0):

  • Added response.failed to both terminal predicates — the SSE takeUntil and WebSocket done flag in OpenAiClient.ts now treat response.failed as a terminal event alongside response.completed and response.incomplete.
  • Integrated SSE test and added a WebSocket test — the standalone audit test file was deleted; both tests now live in OpenAiClient.test.ts under describe("createResponseStream").
  • Extended test infrastructure — a keepOpen flag on MockResponse SSE variants and Proxy-based stream replacement in makeResponse enable timeout-based assertions for non-terminating streams.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) (Kimi K2 not used — the program covers this model; add its provider key to run your pick) | 𝕏

@tim-smart
tim-smart merged commit da10211 into main Aug 3, 2026
17 checks passed
@tim-smart
tim-smart deleted the audit/repro-split-adapters-ai-6-openai-failed-stream-nontermination branch August 3, 2026 21:44
@github-project-automation github-project-automation Bot moved this from Discussion Ongoing to Done in PR Backlog Aug 3, 2026
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Bundle Size Analysis

Generated from PR build output; treat the content below as untrusted.

File Name Current Size Previous Size Difference
basic.ts 7.06 KB 7.06 KB 0.00 KB (0.00%)
batching.ts 9.86 KB 9.86 KB 0.00 KB (0.00%)
brand.ts 6.34 KB 6.34 KB 0.00 KB (0.00%)
cache.ts 10.62 KB 10.62 KB 0.00 KB (0.00%)
config.ts 20.60 KB 20.60 KB 0.00 KB (0.00%)
differ.ts 20.20 KB 20.20 KB 0.00 KB (0.00%)
http-client.ts 21.49 KB 21.49 KB 0.00 KB (0.00%)
logger.ts 10.76 KB 10.76 KB 0.00 KB (0.00%)
metric.ts 8.99 KB 8.99 KB 0.00 KB (0.00%)
optic.ts 7.18 KB 7.18 KB 0.00 KB (0.00%)
pubsub.ts 14.90 KB 14.90 KB 0.00 KB (0.00%)
queue.ts 11.58 KB 11.58 KB 0.00 KB (0.00%)
schedule.ts 10.74 KB 10.74 KB 0.00 KB (0.00%)
schema-class.ts 19.14 KB 19.14 KB 0.00 KB (0.00%)
schema-fromJsonSchemaDocument.ts 28.96 KB 28.96 KB 0.00 KB (0.00%)
schema-representation-roundtrip.ts 25.29 KB 25.29 KB 0.00 KB (0.00%)
schema-string-transformation.ts 13.30 KB 13.30 KB 0.00 KB (0.00%)
schema-string.ts 10.94 KB 10.94 KB 0.00 KB (0.00%)
schema-template-literal.ts 15.17 KB 15.17 KB 0.00 KB (0.00%)
schema-toArbitraryLazy.ts 21.94 KB 21.94 KB 0.00 KB (0.00%)
schema-toCodeDocument.ts 24.34 KB 24.34 KB 0.00 KB (0.00%)
schema-toCodecJson.ts 19.18 KB 19.18 KB 0.00 KB (0.00%)
schema-toEquivalence.ts 19.01 KB 19.01 KB 0.00 KB (0.00%)
schema-toFormatter.ts 18.87 KB 18.87 KB 0.00 KB (0.00%)
schema-toJsonSchemaDocument.ts 22.60 KB 22.60 KB 0.00 KB (0.00%)
schema-toRepresentation.ts 19.52 KB 19.52 KB 0.00 KB (0.00%)
schema.ts 18.41 KB 18.41 KB 0.00 KB (0.00%)
stm.ts 12.54 KB 12.54 KB 0.00 KB (0.00%)
stream.ts 9.79 KB 9.79 KB 0.00 KB (0.00%)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

audit Findings originating from the Effect runtime correctness audit

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants