feat(reasoning): expose thinking/reasoning across API + control thinking.display - #48024
feat(reasoning): expose thinking/reasoning across API + control thinking.display#48024arminanton wants to merge 1 commit into
Conversation
…lines doc Council demands: - NousResearch#1 end-to-end replay: stacked 38/39 PRs on clean v0.17.0; the only 10 non-literal lines are my OWN intentional fixes (encoding-safe I/O NousResearch#50033/NousResearch#50064, public-value test NousResearch#50078) — replay tree is a SUPERSET of overlay intent, 0 lost. - NousResearch#3 independent spot-check via PLAIN git apply (not my scripts): NousResearch#48024/NousResearch#50146/NousResearch#50046 all apply clean exit 0. - NousResearch#4 the 3 deleted test_model_metadata lines documented findably here + in complete_diff_coverage.md (overlay's private-900K deletion, public PR keeps 272K).
…ndle) This PR previously bundled 100 files as a "cross-PR integration regression suite", but 94 of those duplicated other open PRs — which made it the primary blocker when combining the PR set onto a later release (it conflicted on every overlapping file). **Slimmed to the 4 files genuinely unique to this PR:** ``` hermes_cli/auth.py # copilot-opus-context auth path hermes_cli/runtime_provider.py # runtime provider resolution tests/agent/conftest.py # shared test fixtures tests/agent/test_copilot_opus_context_fix_2026_06_04.py # the regression test ``` The 94 duplicate files are owned by their topical feature PRs already (autopilot NousResearch#49917, reasoning NousResearch#48024, copilot identity NousResearch#50064, etc.). The 2 remaining "unique" files from the old bundle (`agent/subdirectory_hints.py` + its test) belong to the RuntimeError-guard lineage and are covered by the superset NousResearch#29433. Built on v0.17.0 (`2bd1977d8`); all 4 files compile; 0 private-provenance leaks. Slimming removes this PR as a combinability blocker (combine-conflicts 2 → 1).
teknium1
left a comment
There was a problem hiding this comment.
Thanks for consolidating the API reasoning work. The current API server still lacks this capability: /v1/chat/completions wires no reasoning callback at gateway/platforms/api_server.py:2311-2321, and /v1/responses does not wire one at gateway/platforms/api_server.py:3397-3409.
Problems
- The chat streaming branch must not source reasoning from
reasoning.available. Current main constructs that event fromassistant_message.contentinagent/conversation_loop.py:4334-4350, whereas structured reasoning deltas callreasoning_callbackthroughrun_agent.py:4745-4751fromagent/chat_completion_helpers.py:2354-2359and:2675-2679. The proposed path can emit answer text as reasoning and omit native thinking deltas. - The
reasoning_tokenschange is already on main inagent/usage_pricing.py:883-906, from3a122ba4a.
Suggested changes
- Route Chat Completions through
reasoning_callback, as the Responses path does, and test a structured callback invocation. - Salvage only the API/display portions after reconciling the moved API-server lifecycle code; omit the already-landed usage hunk.
Automated hermes-sweeper review.
| ephemeral_system_prompt=system_prompt, | ||
| session_id=session_id, | ||
| stream_delta_callback=_on_delta, | ||
| tool_progress_callback=_on_reasoning_progress if _show_reasoning else None, |
There was a problem hiding this comment.
Please use reasoning_callback here instead of tool_progress_callback. On current main, reasoning.available is derived from assistant_message.content (agent/conversation_loop.py:4334-4350), while native structured reasoning deltas invoke reasoning_callback (agent/chat_completion_helpers.py:2354-2359, :2675-2679). This would miss structured thinking and can serialize answer content as reasoning_content.
c5346ea to
55df9e9
Compare
|
Rebased onto current Structured reasoning, not Event shapes:
Gated by Tests: |
Expose model reasoning/thinking on /v1/chat/completions and /v1/responses, gated on display.platforms.api_server.show_reasoning (off by default = byte-identical wire). Sweeper fix: both endpoints source STRUCTURED reasoning via the agent's reasoning_callback (fired by run_agent._fire_reasoning_delta from native reasoning_content / thinking deltas), NOT the reasoning.available progress event. conversation_loop derives reasoning.available from assistant_message.content, so sourcing it would serialize answer text as reasoning and drop native thinking deltas. - chat/completions: wires reasoning_callback in both stream and non-stream. Stream emits delta.reasoning_content chunks; non-stream adds a reasoning_content sibling of content. - responses: wires reasoning_callback into the SSE writer, emitted as the spec reasoning event family (output_item.added -> reasoning_summary_part.added -> reasoning_summary_text.delta -> .done -> output_item.done); non-stream via _extract_output_items(include_reasoning). Input hardening skips echoed-back reasoning items (no empty-turn 400s). - gate _reasoning_exposure_enabled() fails closed (never 500). - reasoning_callback threaded through _create_agent / _run_agent. thinking.display control (agent/anthropic_adapter.py): _resolve_thinking_display reads reasoning_config["display"] (summarized|omitted); default summarized, unknown values fall back so a bad value can't make an invalid request. Dropped the usage_pricing reasoning_tokens hunk from the original PR: already on main (3a122ba). Addresses the inline comment (use reasoning_callback, not tool_progress_callback / reasoning.available). Tests: 9 thinking.display resolver + 17 api_server reasoning (gate, chat stream/non-stream, responses stream/non-stream, extraction, input hardening).
55df9e9 to
bf9ee52
Compare
What does this PR do?
Exposes model reasoning/thinking through the API server gateway on both
/v1/chat/completionsand/v1/responses, and adds control over Anthropic'sthinking.display. Reasoning already renders in the CLI and messaging paths; this brings the OpenAI-compatible API surface to parity so downstream clients (Open WebUI, etc.) can render the model's thinking.Everything is gated on the existing
display.show_reasoningsetting. With it off (the default), the wire output is byte-identical to today, so this is a safe, opt-in change.Related Issue
Closes #37044
Closes #7556
Closes #21655
Closes #18466
Closes #30449
Type of Change
Changes Made
/v1/chat/completions(gateway/platforms/api_server.py) — closes #37044, #7556, #30449reasoning_contentas a sibling ofcontentin the message object.delta.reasoning_contentchunks emitted beforedelta.content, the shape Open WebUI and other OpenAI-compatible frontends already parse.display.show_reasoning([Bug]: "display.show_reasoning" config option is not honored by the API server adapter #7556)./v1/responses(gateway/platforms/api_server.py) — closes #21655reasoning_callback, emitted as the spec reasoning event family (response.output_item.added→reasoning_summary_part.added→reasoning_summary_text.delta→.done→output_item.done).reasoning_callback, not thereasoning.availableprogress event. The latter carries the stripped assistant content, not the model's reasoning, so surfacing it would duplicate the answer inside the thinking block._extract_output_items(include_reasoning=...).input/conversation_history(avoids empty-turn 400s).thinking.displaycontrol (agent/anthropic_adapter.py)_resolve_thinking_display()reads an optionalreasoning_config["display"](summarized|omitted). Default stayssummarizedso Hermes always has reasoning to surface;omittedsuppresses the reasoning text on the wire (the model still thinks; the signature is preserved for multi-turn continuity). Unknown values fall back tosummarizedso a bad value can never produce an invalid request.reasoning_tokens(agent/usage_pricing.py) — closes #18466normalize_usagenow also readscompletion_tokens_details.reasoning_tokens(the chat/completions usage shape). It was always 0 in chat_completions mode because onlyoutput_tokens_detailswas read.How to Test
display.show_reasoning: truein~/.hermes/config.yaml(default isfalse)./v1/chat/completionsnon-streaming → response message containsreasoning_content./v1/chat/completionswithstream: true→delta.reasoning_contentchunks arrive beforedelta.content./v1/responseswithstream: true→ reasoning summary events stream before the answer.display.show_reasoning: false(default), confirm the wire output is unchanged from before this PR.thinking.display: passreasoning_config["display"] = "omitted"and confirm no reasoning text is returned while the answer is unaffected.Checklist
Code
feat(reasoning):)thinking.displayresolver tests; 12 usage_pricing tests)Documentation & Housekeeping
cli-config.yaml.exampleif I added/changed config keys — N/A (no new config keys;thinking.displayis a runtimereasoning_configfield, anddisplay.show_reasoningalready exists)CONTRIBUTING.md/AGENTS.mdif I changed architecture or workflows — N/AProvider coverage
last_reasoningis sourced from the provider-neutralmsg["reasoning"]field, so this works for vendor-direct Anthropic, Copilot-served Claude, and Copilot-served GPT with no per-provider special-casing.Relation to prior PRs
Synthesizes the approaches in #39006 (chat/completions) and #43644 (Responses API), reusing the
reasoning_callbackmechanism #43644 established. Supersedes the narrower #40005, which used a text-prepend plus a non-standard SSE event.