Skip to content

fix(providers/openai): accept streaming chunks with both reasoning fields#8715

Merged
lifeizhou-ap merged 2 commits into
aaif-goose:mainfrom
capttrousers:fix/openai-streaming-duplicate-reasoning-field
Apr 22, 2026
Merged

fix(providers/openai): accept streaming chunks with both reasoning fields#8715
lifeizhou-ap merged 2 commits into
aaif-goose:mainfrom
capttrousers:fix/openai-streaming-duplicate-reasoning-field

Conversation

@capttrousers
Copy link
Copy Markdown
Contributor

@capttrousers capttrousers commented Apr 21, 2026

Summary

Some OpenAI-compatible servers (notably vLLM serving gpt-oss-120b, see vllm-project/vllm#27755) emit both reasoning and reasoning_content in the same chat.completion.chunk delta, usually mirroring each other:

{"choices":[{"delta":{"reasoning":"","reasoning_content":""}}]}

The streaming Delta struct used #[serde(alias = "reasoning")] on reasoning_content, so serde treated the two keys as duplicates of the same field and failed the whole stream parse with:

Request failed: Failed to parse streaming chunk:
  duplicate field `reasoning_content`

This splits reasoning and reasoning_content into two separate Option<String> fields on Delta and reads them through a new reasoning_text() helper that prefers reasoning_content (DeepSeek/OpenRouter convention), falls back to reasoning (vLLM), and skips empty values. The non-streaming response_to_message path already tolerated both fields via a manual .get().or_else() fallback — this brings the streaming path to parity.

The two commits are structured for TDD: the first adds tests (one of which fails on that commit with the duplicate-field error); the second is the fix that flips it to green.

Testing

  • cargo test -p goose --lib providers::formats::openai — all tests pass after the fix; the new test_streaming_chunk_with_both_reasoning_fields fails on the test-only commit as designed.
  • cargo fmt
  • cargo clippy --all-targets -- -D warnings
  • Manual: configured goose CLI against a local vLLM-hosted gpt-oss-120b endpoint and confirmed chat + tool-call streaming no longer errors.

Related Issues

No open goose issue — discovered while configuring goose against a local vLLM gpt-oss-120b endpoint. Upstream vLLM context: vllm-project/vllm#27755.

…parse error

vLLM/SGLang-style OpenAI-compatible servers (e.g. gpt-oss-120b) emit
both `reasoning` and `reasoning_content` in the same chat.completion.chunk
delta. The streaming Delta struct uses `#[serde(alias = "reasoning")]`
on `reasoning_content`, so serde treats the two keys as duplicates and
the whole stream parse fails with:

    duplicate field `reasoning_content`

Add three tests:
- non-streaming response with both fields (already works via the manual
  `.get().or_else()` fallback in response_to_message)
- streaming chunk with only `reasoning_content` (guardrail)
- streaming chunk with both `reasoning` and `reasoning_content`
  (currently fails; the fix in the next commit flips it green)

Signed-off-by: Sam Osborn <samosborn88@gmail.com>
…elds

Some OpenAI-compatible servers (vLLM serving gpt-oss, see
vllm-project/vllm#27755) emit both
`reasoning` and `reasoning_content` in the same chat.completion.chunk
delta, usually mirroring each other. The previous `Delta` struct used
`#[serde(alias = "reasoning")]` on `reasoning_content`, which made serde
treat the two keys as duplicates of the same field and fail the whole
stream parse with:

    duplicate field `reasoning_content`

Split `reasoning` and `reasoning_content` into separate optional fields
and read through a `reasoning_text()` helper that prefers
`reasoning_content` (the DeepSeek/OpenRouter convention), falls back to
`reasoning` (vLLM), and skips empty values so empty-delta chunks don't
emit empty thinking content. This matches the tolerance already present
in the non-streaming `response_to_message` path.

Flips the failing test from the previous commit to green.

Signed-off-by: Sam Osborn <samosborn88@gmail.com>
@capttrousers capttrousers force-pushed the fix/openai-streaming-duplicate-reasoning-field branch from 3894455 to d42929e Compare April 21, 2026 02:44
@capttrousers capttrousers marked this pull request as ready for review April 21, 2026 02:59
Copy link
Copy Markdown
Collaborator

@lifeizhou-ap lifeizhou-ap left a comment

Choose a reason for hiding this comment

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

Thank you for the contribution!

@lifeizhou-ap lifeizhou-ap added this pull request to the merge queue Apr 22, 2026
Merged via the queue into aaif-goose:main with commit 015b0d9 Apr 22, 2026
20 checks passed
lifeizhou-ap added a commit that referenced this pull request Apr 22, 2026
* main: (41 commits)
  removed the specific code owner for documentation change (#8749)
  fix(providers): handle missing delta field in streaming chunks (#8700)
  refactor(providers): extract http_status module and rename handle_status_openai_compat (#8620)
  fix(providers/openai): accept streaming chunks with both reasoning fields (#8715)
  feat: associate threads with projects (#8745)
  upgrade goose sdk and tui to be compatible with the latest agentclientprotocol/sdk package (#8667)
  feat: extend goose2 context window ux with auto-compaction (#8721)
  improve goose2 agent management flows (#8737)
  alexhancock/tui-improvements (#8736)
  fix: add strict:false to Responses API tools and gpt-5.4 to known models (#8636)
  persist and reliably apply chat model selection (#8734)
  merge goose-acp crate into goose (#8726)
  docs: AGENTS.md section on goose2 desktop backend architecture (#8732)
  feat: goose2 message bubble + action tray (#8720)
  consolidate provider ACP methods onto inventory (#8710)
  ci: declare and enforce MSRV of 1.91.1 (#8670)
  fix(ui): correct grammar in apps view description (#8668) (#8679)
  Stop load openai fast model for openapi compatible custom endpoint (#8644)
  feat(hooks): add Husky git hooks for ui/goose2 (#8577)
  fix: links in chat could not be opened (#8544)
  ...
lifeizhou-ap added a commit that referenced this pull request Apr 23, 2026
* main: (34 commits)
  fix(goose-server): cache TLS cert to disk to avoid slow startup on first launch (#8174)
  feat: add Exa AI-powered search tool (#8487)
  fix: preprompt would show after loading session (#8744)
  commands to acp+ migration: extensions management (#8733)
  feat: desktop notification when goose finishes a task (#8647)
  harden code review skill for async state and default-resolution bugs (#8740)
  Feature/at agent mention (#8571)
  fix: removed hardcoded dependency of goose-acp-macro (#8753)
  perf: split agent setup into staged phases to reduce startup blocking (#8746)
  Add /skills command (#8600)
  Replace deprecated Claude ACP package links (#8625)
  removed the specific code owner for documentation change (#8749)
  fix(providers): handle missing delta field in streaming chunks (#8700)
  refactor(providers): extract http_status module and rename handle_status_openai_compat (#8620)
  fix(providers/openai): accept streaming chunks with both reasoning fields (#8715)
  feat: associate threads with projects (#8745)
  upgrade goose sdk and tui to be compatible with the latest agentclientprotocol/sdk package (#8667)
  feat: extend goose2 context window ux with auto-compaction (#8721)
  improve goose2 agent management flows (#8737)
  alexhancock/tui-improvements (#8736)
  ...
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.

2 participants