Skip to content

fix: drain non sse stream reader - #3956

Merged
akshaydeo merged 1 commit into
devfrom
06-01-fix_drain_non_sse_stream_reader
Jun 2, 2026
Merged

fix: drain non sse stream reader#3956
akshaydeo merged 1 commit into
devfrom
06-01-fix_drain_non_sse_stream_reader

Conversation

@TejasGhatte

@TejasGhatte TejasGhatte commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Some OpenAI-compatible backends return valid SSE frames without a Content-Type: text/event-stream header. The previous DrainNonSSEStreamResponse helper would unconditionally drain and discard the response body in this case, causing the downstream SSE parser to receive an empty stream. This PR introduces a reader-preserving variant that peeks at the first bytes of the stream to detect SSE field prefixes (data:, event:, id:, retry:, :, or leading newlines) before deciding whether to drain or pass the reader through intact.

Changes

  • Introduced DrainNonSSEStreamReader(resp, reader) which accepts an io.Reader (e.g. a decompressed stream) and returns a potentially buffered reader alongside a drained boolean, preserving the stream when it looks like SSE even if the content type header is absent.
  • DrainNonSSEStreamResponse is retained as a thin wrapper delegating to DrainNonSSEStreamReader for backward compatibility.
  • All streaming handlers in the OpenAI provider (text completion, chat completion, responses, speech, transcription, image generation, image edit) now use DrainNonSSEStreamReader and reassign the reader from its return value so the buffered peek bytes are not lost.
  • Added looksLikeSSEPrefix to perform a case-sensitive, 16-byte peek-based heuristic for SSE field prefixes.
  • Added tests covering: SSE without content type remains readable, gzip-compressed SSE without content type remains readable, JSON without content type is drained, and uppercase SSE-like prefixes are treated as non-SSE.

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Chore/CI

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Providers/Integrations
  • Plugins
  • UI (React)
  • Docs

How to test

go test ./core/providers/utils/... -run TestDrainNonSSEStreamReader
go test ./...

Expected: all new TestDrainNonSSEStreamReader_* tests pass, and existing streaming tests remain green. To validate end-to-end, route a streaming request through a backend that returns SSE without Content-Type: text/event-stream and confirm the response is streamed correctly rather than returning an error.

Breaking changes

  • Yes
  • No

Related issues

Security considerations

None. The peek reads at most 16 bytes from the stream and does not log or expose any content.

Checklist

  • I read docs/contributing/README.md and followed the guidelines
  • I added/updated tests where appropriate
  • I updated documentation where needed
  • I verified builds succeed (Go and UI)
  • I verified the CI pipeline passes locally if applicable

Summary by CodeRabbit

  • Bug Fixes

    • Improved streaming across text, chat, responses, speech, transcription, image generation and edit flows to detect SSE-like streams, avoid prematurely draining them, and prevent stream hangs or unexpected termination.
  • Tests

    • Added and updated unit tests for SSE detection, compressed-stream handling, fragmented/tiny-prefix delivery, and correct draining behavior for non-SSE payloads.

Copy link
Copy Markdown
Collaborator Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduce DrainNonSSEStreamReader to peek and preserve SSE-like streams when Content-Type is missing/incorrect, replace prior response-only draining with reader-aware draining across seven OpenAI streaming goroutines, and add unit tests covering plain, gzip, short-read SSE detection and non-SSE draining.

Changes

SSE Stream Detection Refactoring

Layer / File(s) Summary
Buffered SSE detection utility
core/providers/utils/utils.go
Add DrainNonSSEStreamReader(resp, reader) and SSE-prefix helpers to peek initial bytes and decide whether to preserve the reader or drain to io.Discard. Includes bufio import.
Unit tests for detection
core/providers/utils/utils_test.go
Add tests verifying SSE-like payloads (plain, gzip, short-read, live-stream) without Content-Type are preserved, JSON-like payloads are drained, uppercase DATA: is drained, and include shortReadReader and timing tests.
OpenAI streaming endpoint integration
core/providers/openai/openai.go
Update text completion, chat completion, responses, speech, transcription, image generation, and image edit streaming goroutines to call DrainNonSSEStreamReader(resp, reader) and branch on the returned drained flag instead of DrainNonSSEStreamResponse.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • danpiths
  • akshaydeo

Poem

A rabbit peeks where streams begin,
I nibble prefixes, keep what's thin.
When "data:" whispers on the line,
I hold the thread and let it shine.
Hooray for streaming—🐇🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: drain non sse stream reader' clearly and concisely describes the main bug fix, matching the core change of updating non-SSE stream handling logic.
Description check ✅ Passed The PR description covers all required template sections: summary explaining the problem, detailed changes, type of change marked, affected areas selected, testing instructions provided, breaking changes addressed, and checklist items marked appropriately.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 06-01-fix_drain_non_sse_stream_reader

Comment @coderabbitai help to get the list of available commands and usage tips.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


tejas ghatte seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@TejasGhatte
TejasGhatte marked this pull request as ready for review June 1, 2026 14:41

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/providers/utils/utils.go`:
- Around line 1003-1005: DrainNonSSEStreamResponse currently discards the reader
returned by DrainNonSSEStreamReader, which lets Peek(16) buffer bytes in a
throwaway bufio.Reader and causes subsequent reads from resp.BodyStream() to
miss those bytes; instead call DrainNonSSEStreamReader(resp, resp.BodyStream()),
capture the returned io.Reader (the possibly wrapped/buffered reader) and
restore it onto the response (e.g. via resp.SetBodyStream(returnedReader, -1) or
the equivalent in fasthttp) before returning the drained bool so callers keep
the full stream.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4bbb409a-ec9b-4b1f-9101-3dc6197562aa

📥 Commits

Reviewing files that changed from the base of the PR and between d4c96b8 and 435e381.

📒 Files selected for processing (3)
  • core/providers/openai/openai.go
  • core/providers/utils/utils.go
  • core/providers/utils/utils_test.go

Comment thread core/providers/utils/utils.go Outdated
@greptile-apps

greptile-apps Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 5/5

Safe to merge; the core logic is well-tested, all seven streaming handlers have been updated consistently, and no pre-existing callers of the removed function remain.

The change is narrowly scoped: a new peek-based heuristic replaces an unconditional drain, and the returned reader is correctly reassigned in every handler. The partial-prefix match behaviour is intentional and pinned by dedicated tests. No regressions to non-SSE paths were found, and the idle-timeout / decompression / cancellation chain interacts with the new bufio wrapper correctly on both the SSE and drain code paths.

core/providers/utils/utils.go — the peekHasPrefix partial-match design is intentional but undocumented in the code itself; worth a short inline comment to prevent future misreads.

Important Files Changed

Filename Overview
core/providers/utils/utils.go Replaces DrainNonSSEStreamResponse with DrainNonSSEStreamReader; introduces hasSSEPrefix and peekHasPrefix for a non-blocking 1-byte peek heuristic. Logic is intentional but partial-prefix matching (when fewer bytes than the field name are buffered) can produce false positives for non-SSE bodies starting with d/e/i/r.
core/providers/openai/openai.go All seven streaming handlers now use DrainNonSSEStreamReader and reassign the returned reader, ensuring the peek bytes buffered during SSE detection are preserved for the downstream SSE scanner. Mechanically straightforward; no logic regressions visible.
core/providers/utils/utils_test.go Replaces two simple SSE/non-SSE drain tests with a comprehensive suite: SSE without content-type, gzip-compressed SSE, JSON drain, uppercase rejection, short-read delivery, fragmented single-byte prefix, and an end-to-end Codex reproduction test. Coverage is thorough.

Reviews (6): Last reviewed commit: "fix: drain non sse stream reader" | Re-trigger Greptile

Comment thread core/providers/utils/utils.go Outdated
Comment thread core/providers/utils/utils.go Outdated
@TejasGhatte
TejasGhatte force-pushed the 06-01-fix_drain_non_sse_stream_reader branch from 435e381 to 909f828 Compare June 1, 2026 15:03

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/providers/utils/utils.go`:
- Around line 1008-1016: The probe in DrainNonSSEStreamReader currently does a
single reader.Read into a 16-byte slice which can misclassify SSE when readers
return short reads; change the logic to wrap the incoming io.Reader in a
bufio.Reader, use bufio.Reader.Peek(16) (handling smaller available bytes
without consuming) to check looksLikeSSEPrefix, and if it looks like SSE return
the bufio.Reader and false, otherwise drain the bufio.Reader (io.Copy to
io.Discard) and return nil,true; update references to the original reader
variable to use the buffered reader and keep uses of looksLikeSSEPrefix
unchanged. Also add a unit test that supplies an io.Reader implementation that
deliberately returns short reads (n < len and err == nil) to ensure the SSE
prefix split across reads is detected correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 268f1b8a-2d34-4d5f-9708-690d07106f35

📥 Commits

Reviewing files that changed from the base of the PR and between 435e381 and 909f828.

📒 Files selected for processing (3)
  • core/providers/openai/openai.go
  • core/providers/utils/utils.go
  • core/providers/utils/utils_test.go

Comment thread core/providers/utils/utils.go Outdated
@TejasGhatte
TejasGhatte force-pushed the 06-01-fix_drain_non_sse_stream_reader branch from 909f828 to 89c2a42 Compare June 1, 2026 15:12

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/providers/utils/utils.go`:
- Around line 1009-1015: The current DrainNonSSEStreamReader uses
bufio.Reader.Peek(16) which can block; change DrainNonSSEStreamReader to probe
incrementally (e.g., Peek(1) first), and only call Peek(n) for longer tokens
when the first byte matches SSE candidate prefixes (':' or 'i' etc.), using
looksLikeSSEPrefix or similar logic to decide when to peek further and avoid a
fixed Peek(16); after detecting non-SSE, drain the rest without blocking. Also
add a regression test that feeds the reader small fragments like ":\n\n" or
"id:" with a pause before 16 bytes are available and assert
DrainNonSSEStreamReader returns promptly and the underlying stream remains
readable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3378b7dd-b792-4288-b66c-0d7f8262f7f5

📥 Commits

Reviewing files that changed from the base of the PR and between 909f828 and 89c2a42.

📒 Files selected for processing (3)
  • core/providers/openai/openai.go
  • core/providers/utils/utils.go
  • core/providers/utils/utils_test.go

Comment thread core/providers/utils/utils.go Outdated
@TejasGhatte
TejasGhatte force-pushed the 06-01-fix_drain_non_sse_stream_reader branch from 89c2a42 to 1f19cc0 Compare June 1, 2026 16:03

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/providers/utils/utils.go`:
- Around line 997-1016: Reintroduce the exported compatibility wrapper
DrainNonSSEStreamResponse that preserves the original public API and simply
forwards to the new helper DrainNonSSEStreamReader; implement
DrainNonSSEStreamResponse with the original signature, call
DrainNonSSEStreamReader internally, and translate/return values exactly as
before (ensuring nil/reader behavior is preserved), so callers of
DrainNonSSEStreamResponse keep working; keep existing helpers like hasSSEPrefix
and sseInitialBufSize unchanged.
- Around line 1018-1041: The peekHasPrefix implementation can block because
bufio.Reader.Peek(len(prefix)) waits for all bytes; update peekHasPrefix to use
reader.Buffered() to only check min(n, len(prefix)) bytes and compare peeked
bytes to prefix[:n] so fragmented SSE names like "d" vs "data:" don't stall
hasSSEPrefix/DrainNonSSEStreamReader; adjust hasSSEPrefix to rely on this
non-blocking probe for cases 'd','e','i','r' and add a regression test that uses
io.Pipe to write only the first byte of "data:" and "event:" then assert
DrainNonSSEStreamReader returns promptly without draining the rest.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5763115f-3ce7-4761-8171-81d72669099c

📥 Commits

Reviewing files that changed from the base of the PR and between 89c2a42 and 1f19cc0.

📒 Files selected for processing (3)
  • core/providers/openai/openai.go
  • core/providers/utils/utils.go
  • core/providers/utils/utils_test.go

Comment thread core/providers/utils/utils.go
Comment thread core/providers/utils/utils.go Outdated
@TejasGhatte
TejasGhatte force-pushed the 06-01-fix_drain_non_sse_stream_reader branch from 1f19cc0 to fe25cc8 Compare June 1, 2026 16:20

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/providers/utils/utils_test.go`:
- Around line 824-893: The 200ms deadline in
TestDrainNonSSEStreamReader_TinyOpenSSEPrefixReturnsPromptly is too tight and
can flake on CI; update the test to use a more forgiving timeout (e.g., replace
time.After(200 * time.Millisecond) with a larger constant such as time.After(2 *
time.Second)) or, preferably, convert the timing assertion to a handshake-based
synchronization using the existing writeErr or a dedicated done channel so the
goroutine signals readiness deterministically; apply the same change to the
other promptness test referencing DrainNonSSEStreamReader to avoid flaky
failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 44769f20-592a-4d8d-bdc6-9f00ed27623d

📥 Commits

Reviewing files that changed from the base of the PR and between 1f19cc0 and fe25cc8.

📒 Files selected for processing (3)
  • core/providers/openai/openai.go
  • core/providers/utils/utils.go
  • core/providers/utils/utils_test.go

Comment thread core/providers/utils/utils_test.go
@TejasGhatte
TejasGhatte force-pushed the 06-01-fix_drain_non_sse_stream_reader branch from fe25cc8 to b3d88dc Compare June 2, 2026 06:30

akshaydeo commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Merge activity

  • Jun 2, 7:03 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Jun 2, 7:03 AM UTC: @akshaydeo merged this pull request with Graphite.

@akshaydeo
akshaydeo merged commit c0f8a28 into dev Jun 2, 2026
14 of 15 checks passed
@akshaydeo
akshaydeo deleted the 06-01-fix_drain_non_sse_stream_reader branch June 2, 2026 07:03
akshaydeo pushed a commit that referenced this pull request Jun 2, 2026
## Summary

Some OpenAI-compatible backends return valid SSE frames without a `Content-Type: text/event-stream` header. The previous `DrainNonSSEStreamResponse` helper would unconditionally drain and discard the response body in this case, causing the downstream SSE parser to receive an empty stream. This PR introduces a reader-preserving variant that peeks at the first bytes of the stream to detect SSE field prefixes (`data:`, `event:`, `id:`, `retry:`, `:`, or leading newlines) before deciding whether to drain or pass the reader through intact.

## Changes

- Introduced `DrainNonSSEStreamReader(resp, reader)` which accepts an `io.Reader` (e.g. a decompressed stream) and returns a potentially buffered reader alongside a `drained` boolean, preserving the stream when it looks like SSE even if the content type header is absent.
- `DrainNonSSEStreamResponse` is retained as a thin wrapper delegating to `DrainNonSSEStreamReader` for backward compatibility.
- All streaming handlers in the OpenAI provider (`text completion`, `chat completion`, `responses`, `speech`, `transcription`, `image generation`, `image edit`) now use `DrainNonSSEStreamReader` and reassign the reader from its return value so the buffered peek bytes are not lost.
- Added `looksLikeSSEPrefix` to perform a case-sensitive, 16-byte peek-based heuristic for SSE field prefixes.
- Added tests covering: SSE without content type remains readable, gzip-compressed SSE without content type remains readable, JSON without content type is drained, and uppercase SSE-like prefixes are treated as non-SSE.

## Type of change

- [x] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [ ] Transports (HTTP)
- [x] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./core/providers/utils/... -run TestDrainNonSSEStreamReader
go test ./...
```

Expected: all new `TestDrainNonSSEStreamReader_*` tests pass, and existing streaming tests remain green. To validate end-to-end, route a streaming request through a backend that returns SSE without `Content-Type: text/event-stream` and confirm the response is streamed correctly rather than returning an error.

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

## Security considerations

None. The peek reads at most 16 bytes from the stream and does not log or expose any content.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **Bug Fixes**
  * Improved streaming across text, chat, responses, speech, transcription, image generation and edit flows to detect SSE-like streams, avoid prematurely draining them, and prevent stream hangs or unexpected termination.

* **Tests**
  * Added and updated unit tests for SSE detection, compressed-stream handling, fragmented/tiny-prefix delivery, and correct draining behavior for non-SSE payloads.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
@coderabbitai coderabbitai Bot mentioned this pull request Jun 2, 2026
18 tasks
akshaydeo pushed a commit that referenced this pull request Jun 4, 2026
## Summary

Some OpenAI-compatible backends return valid SSE frames without a `Content-Type: text/event-stream` header. The previous `DrainNonSSEStreamResponse` helper would unconditionally drain and discard the response body in this case, causing the downstream SSE parser to receive an empty stream. This PR introduces a reader-preserving variant that peeks at the first bytes of the stream to detect SSE field prefixes (`data:`, `event:`, `id:`, `retry:`, `:`, or leading newlines) before deciding whether to drain or pass the reader through intact.

## Changes

- Introduced `DrainNonSSEStreamReader(resp, reader)` which accepts an `io.Reader` (e.g. a decompressed stream) and returns a potentially buffered reader alongside a `drained` boolean, preserving the stream when it looks like SSE even if the content type header is absent.
- `DrainNonSSEStreamResponse` is retained as a thin wrapper delegating to `DrainNonSSEStreamReader` for backward compatibility.
- All streaming handlers in the OpenAI provider (`text completion`, `chat completion`, `responses`, `speech`, `transcription`, `image generation`, `image edit`) now use `DrainNonSSEStreamReader` and reassign the reader from its return value so the buffered peek bytes are not lost.
- Added `looksLikeSSEPrefix` to perform a case-sensitive, 16-byte peek-based heuristic for SSE field prefixes.
- Added tests covering: SSE without content type remains readable, gzip-compressed SSE without content type remains readable, JSON without content type is drained, and uppercase SSE-like prefixes are treated as non-SSE.

## Type of change

- [x] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [ ] Transports (HTTP)
- [x] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./core/providers/utils/... -run TestDrainNonSSEStreamReader
go test ./...
```

Expected: all new `TestDrainNonSSEStreamReader_*` tests pass, and existing streaming tests remain green. To validate end-to-end, route a streaming request through a backend that returns SSE without `Content-Type: text/event-stream` and confirm the response is streamed correctly rather than returning an error.

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

## Security considerations

None. The peek reads at most 16 bytes from the stream and does not log or expose any content.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **Bug Fixes**
  * Improved streaming across text, chat, responses, speech, transcription, image generation and edit flows to detect SSE-like streams, avoid prematurely draining them, and prevent stream hangs or unexpected termination.

* **Tests**
  * Added and updated unit tests for SSE detection, compressed-stream handling, fragmented/tiny-prefix delivery, and correct draining behavior for non-SSE payloads.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
@akshaydeo akshaydeo mentioned this pull request Jun 5, 2026
18 tasks
akshaydeo added a commit that referenced this pull request Jun 6, 2026
## Summary

This PR bumps the Go toolchain version from `1.26.3` to `1.26.4` across all modules and CI workflows, and cuts a new release (`core` v1.5.17, `framework` v1.3.17, `transports` v1.5.9, `plugins/compat` v0.1.16, `plugins/governance` v1.5.17, and associated plugin versions) incorporating a large batch of features and fixes accumulated since the previous release.

## Changes

- **Go 1.26.4** — Updated `go-version` in all GitHub Actions workflows (`e2e-tests`, `helm-release`, `pr-tests`, `release-cli`, `release-pipeline`, `snyk`) and all `go.mod` files (core, framework, transports, cli, all plugins, examples, and test modules).
- **Core (v1.5.17)** — OpenAI compaction support, multi-customer logs and usage tracking, multiple team/business unit support, `request_headers` wildcard pattern capture for OTel and Maxim plugins, xAI `x_search` tool, fetch URL validation with SSRF hardening, `file://` pricing URL scheme, virtual key provider fan-out filtering, and a broad set of fixes including Anthropic prompt cache key, empty thinking block stripping, OpenAI stream usage event cleanup, Gemini numeric schema constraints, stale connection retries, Azure Claude diagnostic strip, and passthrough budget handling.
- **Framework (v1.3.17)** — Scope-aware budgets and limits wired from model configs, provider-level governance, multiple customer budget support with `calendar_aligned` windows, paginated virtual key fetch, `config.json` source-of-truth flow, FTS index cap reduction, sync worker drift fix, cascade deletes for model configs, and high-scale virtual key flow improvements.
- **Transports (v1.5.9)** — Full changelog covering all of the above plus UI improvements (log navigation, customer detail sheet, `BudgetDisplay` component, inline loading shell, materialized view alias), SCIM provisioning fields, Helm/config schema additions (`roles`, `per_user_oauth`), client IP resolution from forwarded headers, and dependency upgrades (`recharts` to 3.8.1, `golang.org/x` CVE remediation).
- **Plugins** — `governance` v1.5.17 adds team budget/rate-limit exporters, ghost node reconciliation fix, and VK double usage counting fix; `logging` v1.5.17 adds wildcard header capture and file attachment rendering; `otel` v1.2.17 adds `disable_content_logging` and multiple collectors support; `maxim` v1.6.17 adds `request_headers` wildcard capture; `compat` v0.1.16 fixes `max_tokens` preservation during param filtering.

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [x] Chore/CI

## Affected areas

- [x] Core (Go)
- [x] Transports (HTTP)
- [x] Providers/Integrations
- [x] Plugins
- [x] UI (React)
- [ ] Docs

## How to test

```sh
# Verify Go version
go version  # should report go1.26.4

# Run core tests
cd core && go test ./...

# Run framework tests
cd framework && go test ./...

# Run transports tests
cd transports && go test ./...

# Run plugin tests
cd plugins/governance && go test ./...
cd plugins/logging && go test ./...
cd plugins/otel && go test ./...

# UI
cd ui
pnpm i
pnpm build
pnpm test
```

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

#4053, #4066, #4041, #4012, #3976, #3947, #3991, #4045, #3957, #3938, #3937, #3939, #3981, #3998, #3997, #4092, #4091, #4079, #4080, #4086, #3929, #3994, #4028, #3970, #3919, #3861, #3664, #3999, #4088, #4070, #4051, #4043, #4057, #4023, #3941, #3955, #4024, #3956, #3967, #3925, #3992, #3900

## Security considerations

- Fetch URL validation hardened against SSRF by tightening IP checks for private networks and link-local addresses (#4092, #3947, #3991).
- Transitive `golang.org/x` dependencies (crypto, net, sys, text) bumped to address Docker Scout CVEs (#3900).

## Checklist

- [x] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [x] I verified the CI pipeline passes locally if applicable

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * OpenAI compaction, multi-customer/team logstore support, request-header wildcard capture, enhanced governance (provider-level & scope-aware limits), disable-content-logging option, support for multiple OpenTelemetry collectors, SSRF hardening and URL validation.

* **Chores**
  * Bumped Go toolchain across modules and updated component/plugin version releases.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
@akshaydeo akshaydeo mentioned this pull request Jun 7, 2026
akshaydeo pushed a commit that referenced this pull request Jun 7, 2026
## Summary

Some OpenAI-compatible backends return valid SSE frames without a `Content-Type: text/event-stream` header. The previous `DrainNonSSEStreamResponse` helper would unconditionally drain and discard the response body in this case, causing the downstream SSE parser to receive an empty stream. This PR introduces a reader-preserving variant that peeks at the first bytes of the stream to detect SSE field prefixes (`data:`, `event:`, `id:`, `retry:`, `:`, or leading newlines) before deciding whether to drain or pass the reader through intact.

## Changes

- Introduced `DrainNonSSEStreamReader(resp, reader)` which accepts an `io.Reader` (e.g. a decompressed stream) and returns a potentially buffered reader alongside a `drained` boolean, preserving the stream when it looks like SSE even if the content type header is absent.
- `DrainNonSSEStreamResponse` is retained as a thin wrapper delegating to `DrainNonSSEStreamReader` for backward compatibility.
- All streaming handlers in the OpenAI provider (`text completion`, `chat completion`, `responses`, `speech`, `transcription`, `image generation`, `image edit`) now use `DrainNonSSEStreamReader` and reassign the reader from its return value so the buffered peek bytes are not lost.
- Added `looksLikeSSEPrefix` to perform a case-sensitive, 16-byte peek-based heuristic for SSE field prefixes.
- Added tests covering: SSE without content type remains readable, gzip-compressed SSE without content type remains readable, JSON without content type is drained, and uppercase SSE-like prefixes are treated as non-SSE.

## Type of change

- [x] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [ ] Transports (HTTP)
- [x] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./core/providers/utils/... -run TestDrainNonSSEStreamReader
go test ./...
```

Expected: all new `TestDrainNonSSEStreamReader_*` tests pass, and existing streaming tests remain green. To validate end-to-end, route a streaming request through a backend that returns SSE without `Content-Type: text/event-stream` and confirm the response is streamed correctly rather than returning an error.

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

## Security considerations

None. The peek reads at most 16 bytes from the stream and does not log or expose any content.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **Bug Fixes**
  * Improved streaming across text, chat, responses, speech, transcription, image generation and edit flows to detect SSE-like streams, avoid prematurely draining them, and prevent stream hangs or unexpected termination.

* **Tests**
  * Added and updated unit tests for SSE detection, compressed-stream handling, fragmented/tiny-prefix delivery, and correct draining behavior for non-SSE payloads.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
akshaydeo added a commit that referenced this pull request Jun 7, 2026
## ✨ Features

- **OpenAI Compaction** — Added OpenAI conversation compaction support
across core, framework, logging, and the API surface (#4053)
- **Multi-Customer & Org Hierarchy** — Logs and usage tracking now
support multiple customers, teams, and business units, including
business unit CRUD, team assignment, and governance endpoints in the
OpenAPI spec (#4066, #4041, #4082)
- **Provider-Level Governance** — Budgets & limits are now scope-aware
and can be applied at the virtual-key top level and per provider, wired
from the model configs table, with UI filters for scope and providers
(#3938, #3937, #3939, #3981, #3962)
- **Customer Budgets** — Customers support multiple budgets and
`calendar_aligned` budget windows (#3998, #3997)
- **Virtual Key Attribution & Controls** — Added a `created_by` user
attribution column and a `blacklisted_models` column for virtual key
provider configs (#3672, #3653)
- **Request Header Capture** — OTel and Maxim observability plugins
capture `request_headers` by pattern, with wildcard support (e.g.
`x-custom-*`); logging gained the same wildcard header capture (#4012,
#3958)
- **OTel Content Controls & Collectors** — New `disable_content_logging`
option drops message/tool content from exported spans, plus support for
multiple OTel collectors (#4064, #3894)
- **xAI x_search** — Added xAI `x_search` tool support (#3976)
- **URL Validation** — Added fetch URL validation with private-network
configuration and link-local blocking (#3947, #3991)
- **File Scheme Pricing URLs** — Pricing source URLs now accept the
`file://` scheme for air-gapped and self-hosted deployments (#4045)
- **Paginated Virtual Keys** — Virtual key fetching is paginated to
handle deployments with very large numbers of keys (#3957)
- **Client IP Resolution** — Resolve client IP from
`X-Forwarded-For`/`X-Real-IP` headers
- **SCIM Provisioning** — Added `attributeType`/`attributeValue` SCIM
provisioning fields
- **Helm/Config Schema** — Added `roles` RBAC governance config and
`per_user_oauth` MCP auth to the Helm chart and config schema (#4004,
#4009)
- **Log Navigation UI** — Added a "View logs" menu item to customer,
team, and virtual key tables, clickable links in log detail views, a
customer detail sheet, and a reusable `BudgetDisplay` component (#4073,
#4054, #4026, #4055)
- **Faster First Paint** — Added an inline loading shell to `#root`
before React mounts (#4063)
- **Materialized View Alias** — Added an `alias` column to the
materialized view with filter support (#4078)

## 🐞 Fixed

- **Fetch URL IP Checks** — Hardened fetch URL IP checks against SSRF
(#4092)
- **Mantle Model Matching** — Broadened Mantle model matching to all
`gpt` variants (#4091)
- **Empty Thinking Blocks** — Strip thinking blocks when the signature
is empty (#4079)
- **OpenAI Stream Usage** — Removed usage from the `responses.created`
event in the OpenAI stream (#4080)
- **Prompt Cache Key** — Set the prompt cache key from the Anthropic
integration (#4086)
- **Upstream Failure Status** — Map upstream connection failures to 502
instead of 400 (#3929) (thanks
[@chris-colinsky](https://github.com/chris-colinsky)!)
- **Gemini Schema Constraints** — Accept numeric schema integer
constraints for Gemini (#3994) (thanks
[@yanhao98](https://github.com/yanhao98)!)
- **Files Provider Param** — Accept the `?provider=` query param on `GET
/v1/files` (#3971) (thanks [@alexef](https://github.com/alexef)!)
- **Optional Batch Model** — Made the `model` field optional on `POST
/v1/batches` (#3973) (thanks [@alexef](https://github.com/alexef)!)
- **Helm Azure Config** — Added missing `azure_key_config` fields to the
Helm schema (#3996) (thanks
[@axelray-dev](https://github.com/axelray-dev)!)
- **Text Completion Chunk Model** — Added the missing `Model` field to
`TextCompletionChunkResponse` (#3970) (thanks
[@kuishou68](https://github.com/kuishou68)!)
- **MCP Inline stdio Env** — MCP stdio server configs accept inline
environment variable assignments (#3861) (thanks
[@Shushmitaaaa](https://github.com/Shushmitaaaa)!)
- **Orphaned Tool Results** — Orphaned tool results in the OpenAI to
Anthropic conversion flow are no longer rejected by the Anthropic API
(#3919)
- **Node Usage Reconciliation** — Added a monotonic `inc_number` log
cursor so node usage reconciliation does not skip late async log writes
(#3664)
- **Bedrock Output Assessments** — Corrected the type of
`outputAssessments` in Bedrock responses (#4028)
- **Model Pool Pricing Reloads** — Preserve non-pricing model pool
entries across pricing reloads (#3999)
- **Ghost Node Reconciliation** — Replicate the VK hierarchy flow for
ghost node reconciliation (#4088)
- **VK Double Usage Counting** — Fixed double usage counting when
creating a virtual key (#4070)
- **Model Config Lifecycle** — Cascade deletes for model configs and
removal of stale in-memory model configs (#4051, #4043)
- **FTS Index Cap** — Reduced the FTS index `left()` cap from 800k to
250k chars to stay within the tsvector limit (#4057)
- **Sync Worker Drift** — Reduced the sync worker ticker period to 5m to
prevent threshold drift (#4023)
- **Passthrough** — Fixed passthrough budgets, gated passthrough models
per VK, model extraction for Azure passthrough, and restricted
fallbacks/provider selection to the VK boundary (#3941, #3988, #3983,
#3924)
- **Provider Response Headers** — Strip provider response headers and
add a content-type filter (#3955, #4024)
- **Stream Handling** — Drain non-SSE stream readers and retry stale
connections (#3956, #3967)
- **Azure Claude** — Strip Azure diagnostic property for Claude models
(#3925)
- **Compat max_tokens** — Preserve chat `max_tokens` during param
filtering (#3992)
- **Raw Request Flag** — Removed the raw request flag from providers
that don't support it (#4058)
- **UI Fixes** — Standardized page container layout, virtual key model
configs UI, and dashboard chart tooltips (#4046, #4052, #4044)

## 🔧 Maintenance

- **Dependency Upgrades** — Bumped transitive `golang.org/x`
dependencies (crypto, net, sys, text) for Docker Scout CVE remediation
and `recharts` to 3.8.1; cascaded version bumps across all modules
(#3900, #4003)
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.

3 participants