Skip to content

feat(reasoning): expose thinking/reasoning across API + control thinking.display - #48024

Open
arminanton wants to merge 1 commit into
NousResearch:mainfrom
arminanton:feat/expose-reasoning-api-server
Open

feat(reasoning): expose thinking/reasoning across API + control thinking.display#48024
arminanton wants to merge 1 commit into
NousResearch:mainfrom
arminanton:feat/expose-reasoning-api-server

Conversation

@arminanton

Copy link
Copy Markdown
Contributor

What does this PR do?

Exposes model reasoning/thinking through the API server gateway on both /v1/chat/completions and /v1/responses, and adds control over Anthropic's thinking.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_reasoning setting. 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

  • ✨ New feature (non-breaking change that adds functionality)

Changes Made

/v1/chat/completions (gateway/platforms/api_server.py) — closes #37044, #7556, #30449

/v1/responses (gateway/platforms/api_server.py) — closes #21655

  • Real token-by-token reasoning sourced from the agent's reasoning_callback, emitted as the spec reasoning event family (response.output_item.addedreasoning_summary_part.addedreasoning_summary_text.delta.doneoutput_item.done).
  • Sourced from reasoning_callback, not the reasoning.available progress event. The latter carries the stripped assistant content, not the model's reasoning, so surfacing it would duplicate the answer inside the thinking block.
  • Non-streaming via _extract_output_items(include_reasoning=...).
  • Input hardening: clients echo reasoning items back; they have no role/content, so they are skipped when rebuilding input/conversation_history (avoids empty-turn 400s).

thinking.display control (agent/anthropic_adapter.py)

  • New _resolve_thinking_display() reads an optional reasoning_config["display"] (summarized | omitted). Default stays summarized so Hermes always has reasoning to surface; omitted suppresses the reasoning text on the wire (the model still thinks; the signature is preserved for multi-turn continuity). Unknown values fall back to summarized so a bad value can never produce an invalid request.

reasoning_tokens (agent/usage_pricing.py) — closes #18466

  • normalize_usage now also reads completion_tokens_details.reasoning_tokens (the chat/completions usage shape). It was always 0 in chat_completions mode because only output_tokens_details was read.

How to Test

  1. Set display.show_reasoning: true in ~/.hermes/config.yaml (default is false).
  2. Start the API server and call a reasoning-capable model:
    • /v1/chat/completions non-streaming → response message contains reasoning_content.
    • /v1/chat/completions with stream: truedelta.reasoning_content chunks arrive before delta.content.
    • /v1/responses with stream: true → reasoning summary events stream before the answer.
  3. With display.show_reasoning: false (default), confirm the wire output is unchanged from before this PR.
  4. Anthropic thinking.display: pass reasoning_config["display"] = "omitted" and confirm no reasoning text is returned while the answer is unaffected.

Checklist

Code

Documentation & Housekeeping

  • I've updated relevant documentation (docstrings on the new resolver/extraction helpers) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A (no new config keys; thinking.display is a runtime reasoning_config field, and display.show_reasoning already exists)
  • I've updated CONTRIBUTING.md / AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact — N/A (pure Python, no OS-specific primitives; passes the Windows-footgun check)
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A

Provider coverage

last_reasoning is sourced from the provider-neutral msg["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_callback mechanism #43644 established. Supersedes the narrower #40005, which used a text-prepend plus a non-standard SSE event.

@alt-glitch alt-glitch added type/feature New feature or request comp/gateway Gateway runner, session dispatch, delivery comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint provider/anthropic Anthropic native Messages API P3 Low — cosmetic, nice to have labels Jun 17, 2026
@arminanton
arminanton marked this pull request as ready for review June 17, 2026 20:28
arminanton added a commit to arminanton/hermes-agent that referenced this pull request Jun 21, 2026
…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).
arminanton added a commit to arminanton/hermes-agent that referenced this pull request Jun 22, 2026
…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 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 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 from assistant_message.content in agent/conversation_loop.py:4334-4350, whereas structured reasoning deltas call reasoning_callback through run_agent.py:4745-4751 from agent/chat_completion_helpers.py:2354-2359 and :2675-2679. The proposed path can emit answer text as reasoning and omit native thinking deltas.
  • The reasoning_tokens change is already on main in agent/usage_pricing.py:883-906, from 3a122ba4a.

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.

Comment thread gateway/platforms/api_server.py Outdated
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,

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.

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.

@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state 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
@arminanton
arminanton force-pushed the feat/expose-reasoning-api-server branch from c5346ea to 55df9e9 Compare August 22, 2026 21:39
@arminanton

Copy link
Copy Markdown
Contributor Author

Rebased onto current main and wired reasoning from the structured source.

Structured reasoning, not reasoning.available (sweeper): both endpoints now wire the agent's reasoning_callback (fired from native reasoning_content/thinking deltas), not the reasoning.available progress event — conversation_loop.py derives reasoning.available from assistant_message.content, so sourcing it would serialize answer text as reasoning and drop native thinking. Threaded reasoning_callback through _create_agentAIAgent_run_agent.

Event shapes:

  • /v1/chat/completions: stream emits delta.reasoning_content chunks; non-stream adds a reasoning_content sibling of content.
  • /v1/responses: stream emits the spec reasoning family (response.output_item.added type reasoningreasoning_summary_part.addedreasoning_summary_text.delta.doneoutput_item.done) before tool-start/text/EOS; non-stream emits reasoning output items. Echoed-back reasoning items are skipped on input to avoid empty-turn 400s.

Gated by display.platforms.api_server.show_reasoning (fails closed, off by default = byte-identical wire), with a thinking.display summarized/omitted control. Also dropped the usage_pricing.reasoning_tokens hunk (already on main).

Tests: test_api_server_reasoning.py + test_anthropic_thinking_display.py → 26 passed; test_api_server.py regression → 108 passed (the one health-detailed red is a pre-existing SQLite-3.26 sandbox artifact, identical on main).

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).
@arminanton
arminanton force-pushed the feat/expose-reasoning-api-server branch from 55df9e9 to bf9ee52 Compare August 23, 2026 00:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have provider/anthropic Anthropic native Messages API 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 sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

3 participants