Skip to content

fix(gateway): forward reasoning in chat completions (#37044) - #39006

Open
rodboev wants to merge 1 commit into
NousResearch:mainfrom
rodboev:pr/gateway-reasoning-in-oai-compat
Open

fix(gateway): forward reasoning in chat completions (#37044)#39006
rodboev wants to merge 1 commit into
NousResearch:mainfrom
rodboev:pr/gateway-reasoning-in-oai-compat

Conversation

@rodboev

@rodboev rodboev commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Summary

/v1/chat/completions exposed assistant text but dropped reasoning generated by the provider. The streaming route had no path from AIAgent.reasoning_callback into its SSE writer, and the non-streaming message discarded the completed turn's last_reasoning value. Using the derived reasoning.available tool-progress event would have mislabeled final answer text and capped the output.

This rebuild forwards the existing provider reasoning callback through the API adapter and emits its deltas as OpenAI-compatible delta.reasoning_content. The callback uses current main's ThreadSafeAsyncQueue and shared _sse_frame() path. Non-streaming responses include the current turn's reasoning_content under the same display.show_reasoning policy. The display policy defaults to hidden and applies the existing API-server override precedence.

Fixes #37044

Changes

  • gateway/platforms/api_server.py: pass provider reasoning through _run_agent() and _create_agent(), feed tagged deltas through ThreadSafeAsyncQueue, serialize them with _sse_frame(), and include non-empty last_reasoning in non-streaming messages when display is enabled.
  • tests/gateway/test_api_server.py: cover callback construction and forwarding, full reasoning payloads, reasoning-only output, empty and disabled reasoning, tool lifecycle coexistence, and non-streaming response shapes.

Validation

Scenario Behavior
Streaming provider reasoning with display enabled Preserved byte-for-byte in delta.reasoning_content; assistant text remains in delta.content.
Reasoning-only output Emits reasoning and normal SSE completion without fabricated assistant content.
Empty reasoning or display disabled Omits reasoning_content while answer, tool lifecycle, and completion output continue.
Structured tool lifecycle Retains one correlated running and completed event without using the tool-progress reasoning preview.
Non-streaming response Adds current-turn message.reasoning_content only when non-empty and display is enabled.
/v1/responses, /v1/runs, session chat, messaging Retain their existing endpoint-specific reasoning and event contracts.

Test plan

  • bash scripts/run_tests.sh tests/gateway/test_api_server.py -k "reasoning or run_agent or stream_includes_tool_progress or stream_emits_tool_lifecycle or stream_tool_lifecycle" -v --timeout=0 — 15 passed.
  • bash scripts/run_tests.sh tests/gateway/test_api_server.py -k "stream_includes_tool_progress or stream_emits_tool_lifecycle or stream_tool_lifecycle_skips_internal or stream_cancelled_persists_incomplete_snapshot or stream_client_disconnect_persists_incomplete_snapshot" -v --timeout=0 — 5 passed.
  • ruff check gateway/platforms/api_server.py tests/gateway/test_api_server.py — passed.

Not in scope

/v1/responses, /v1/runs, native session chat, and messaging platforms keep their established response formats and callback contracts.

@alt-glitch alt-glitch added type/feature New feature or request comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have labels Jun 4, 2026
@rodboev
rodboev force-pushed the pr/gateway-reasoning-in-oai-compat branch 3 times, most recently from 672f9d0 to d6e24d6 Compare July 1, 2026 14:56

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

Thanks for covering the non-streaming response shape; current main still drops last_reasoning from /v1/chat/completions at gateway/platforms/api_server.py:2417-2420.

Problems

  • The streaming branch added at gateway/platforms/api_server.py:2027 uses tool_progress_callback and reasoning.available. On current main, that event is derived from stripped assistant_message.content and capped at 500 characters (agent/conversation_loop.py:4334-4350), not from provider reasoning. Native reasoning deltas use reasoning_callback (run_agent.py:4745-4752; agent/chat_completion_helpers.py:2057-2065). This can label answer text as delta.reasoning_content and misses structured/reasoning-only outputs.
  • The new streaming test manually fires reasoning.available, so it does not cover the production reasoning callback chain.

Suggested changes

  • Thread reasoning_callback through _run_agent and _create_agent, then queue and serialize its deltas as delta.reasoning_content.
  • Update the stream test to invoke that callback and cover structured/reasoning-only output.

Automated hermes-sweeper review.

Comment thread gateway/platforms/api_server.py
@teknium1 teknium1 added sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 14, 2026
@rodboev

rodboev commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Good catch. The streaming path was treating reasoning.available as provider reasoning even though current main synthesizes that event from final assistant text.

I removed that source from the SSE path and threaded a dedicated reasoning_callback through APIServerAdapter._run_agent() and _create_agent() into AIAgent. Streaming responses now forward provider reasoning through that callback as delta.reasoning_content, byte-for-byte, while assistant content stays in delta.content. Non-streaming reasoning_content still comes from last_reasoning and stays gated by display.show_reasoning.

The new regression enters _run_agent(), reaches _create_agent(), invokes the installed provider callback, and verifies that reasoning-only and longer-than-500-character output survives unchanged. /v1/runs, /v1/responses, and the structured tool lifecycle callbacks are unchanged.

@rodboev
rodboev force-pushed the pr/gateway-reasoning-in-oai-compat branch from d1945be to 680e88a Compare July 14, 2026 02:35
@rodboev
rodboev force-pushed the pr/gateway-reasoning-in-oai-compat branch from 680e88a to a2806be Compare July 31, 2026 22:38
@rodboev rodboev changed the title feat(gateway): expose reasoning/thinking blocks in /v1/chat/completions responses (#37044) fix(gateway): forward reasoning in chat completions (#37044) Jul 31, 2026
@logabell

Copy link
Copy Markdown

Just want to comment here that this would be a much appreciated feature for my integration with OpenWebUI

@rodboev
rodboev force-pushed the pr/gateway-reasoning-in-oai-compat branch from a2806be to c290ec8 Compare August 13, 2026 22:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: API server gateway does not expose model reasoning/thinking blocks in /v1/chat/completions responses

4 participants