Skip to content

[Feature]: Expose claude-cli thinking blocks as reasoning on /v1/responses (and /v1/chat/completions)` #68374

Description

@jkeen871

Summary

When a claude-cli-backed agent is invoked through the Gateway's HTTP API (POST /v1/responses or POST /v1/chat/completions), the reasoning/thinking content Claude Code CLI emits internally is stripped before reaching the client. Please forward it on the HTTP surface — the same way reasoningDefault already renders reasoning on the chat-channel paths (WhatsApp/Telegram/Slack).

Problem to solve

Claude CLI's stream-json output already carries reasoning as dedicated content_block items of type "thinking" with thinking_delta events. That data exists, is structured, and is delivered to OpenClaw. But when OpenClaw translates the stream into the OpenAI-compat HTTP API, those blocks are discarded. Only the final-answer text reaches the client.

The effect: any UI built on /v1/responses or /v1/chat/completions cannot show Claude's reasoning to its users, even though the model produced it and the operator wants it shown. The existing reasoningDefault: "on" / "stream" agent config knob has no effect on HTTP output. It only takes effect on the chat-channel code path.

Verified empirically on openclaw/openclaw built-in claude-cli backend, sonnet-4-6 model, with reasoningDefault: "on" set on the agent.

Proposed solution

Emit reasoning as a dedicated output item on the HTTP stream, gated by the agent's existing reasoningDefault setting so current silent-default behavior doesn't change for anyone who hasn't opted in.

Option A (preferred) — separate output_item, mirrors Anthropic Responses and OpenAI o-series:

event: response.output_item.added
data: {"type":"response.output_item.added","output_index":0,"item":{"type":"reasoning","id":"rs_...","status":"in_progress","content":[{"type":"summary_text","text":""}]}}

event: response.reasoning_text.delta
data: {"type":"response.reasoning_text.delta","item_id":"rs_...","output_index":0,"content_index":0,"delta":"..."}

event: response.reasoning_text.done
...

event: response.output_item.done
data: {"type":"response.output_item.done","output_index":0,"item":{"type":"reasoning", ...}}

(then the existing message output_item at index 1)

Option B — add a reasoning content part inside the existing message item:

{"type":"message","content":[{"type":"reasoning","text":"..."},{"type":"output_text","text":"..."}]}

Control matrix using the knob you already ship:

  • reasoningDefault: "off" (default): emit nothing (today's HTTP behavior, unchanged)
  • reasoningDefault: "on": emit the reasoning item/part alongside the answer
  • reasoningDefault: "stream": flush reasoning deltas as they arrive on HTTP (today it's Telegram-only)

Optional ergonomics: accept x-openclaw-include-reasoning: true as a per-request opt-in header so HTTP clients can toggle without reconfiguring the agent.

Alternatives considered

All tried against the current built-in claude-cli backend; none produce reasoning on the HTTP surface.

Setting reasoningDefault: "on" on agent list entries in openclaw.json — config accepted, no change to /v1/responses SSE output. reasoningDefault: "stream" behaves the same, and docs confirm stream is Telegram-only today.

reasoning_effort in the /v1/chat/completions body — silently ignored by the gateway's OpenAI-compat layer.

reasoning: {effort: "high"} on /v1/responses — documented as "Accepted but currently ignored" in docs/gateway/openresponses-http-api.md.

Inline /reasoning on or /t high directives in the input string — rejected with "Unknown command: /reasoning" on the HTTP endpoint. Directives are a chat-channel feature.

Spawning claude-cli directly from the client, bypassing the gateway — works but loses session management, worker pooling, fence/permissions, and the whole point of running agents through OpenClaw. Not a viable path for clients that want to stay inside the gateway's architecture.

Impact

Affected: any downstream tool talking to OpenClaw through /v1/responses or /v1/chat/completions. Custom personal-assistant backends, IDE integrations, dashboard UIs, Open WebUI, LibreChat, agent-framework clients. Chat-channel plugins (WhatsApp/Telegram/Slack) are unaffected; they already surface reasoning correctly.

Severity: medium. A first-class capability (reasoning visibility) is fully implemented for one category of clients (channels) and fully absent for another (HTTP). Creates parity confusion and forces HTTP clients to either do without, or reinvent the plumbing by spawning claude-cli themselves.

Frequency: every HTTP request to any reasoning-capable claude-cli agent. Not intermittent. The reasoning content is dropped on every single turn.

Consequence: downstream UIs can't explain why the model answered a given way, can't surface chain-of-thought for debugging model behavior, and can't show reasoning as a user-facing transparency feature. Operators end up either writing workarounds or accepting opaque assistant turns. Concrete reference: a personal-assistant backend has schema slots for chat_messages.thinking and scaffolded per-message UI toggles that currently render empty bubbles because the data never arrives on the HTTP surface.

Evidence/examples

Claude CLI emits real thinking blocks — direct probe:

$ echo "What is sqrt(12345) to 2 decimal places? Think carefully." |
claude -p --model claude-sonnet-4-6
--output-format stream-json --verbose
--effort xhigh --include-partial-messages

{"type":"stream_event","event":{"type":"content_block_start","index":0,"content_block":{"type":"thinking"}},...}
{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"111 + 24/223 ≈ 111 + 0.1076 ≈ 111.108"}},...}
{"type":"stream_event","event":{"type":"content_block_start","index":1,"content_block":{"type":"text"}},...}
...

Note: content_block index 0 is type: "thinking" with real thinking_delta text. Index 1 is the normal text answer block.

Same prompt through the gateway drops the thinking block:

$ curl -sN http://127.0.0.1:18789/v1/responses
-H "x-openclaw-session-key: probe-$(date +%s)"
-H "Authorization: Bearer $TOKEN"
-H "Content-Type: application/json"
-d '{"model":"openclaw/worker-1","stream":true,"input":"What is sqrt(12345) to 2 decimal places? Think carefully."}'

SSE event types observed (deduped):

event: response.created
event: response.in_progress
event: response.output_item.added
event: response.content_part.added
event: response.output_text.delta (final-answer text only)
event: response.output_text.done
event: response.content_part.done
event: response.output_item.done (one item, phase: "final_answer")
event: response.completed

No response.reasoning_* events. No second output_item. No reasoning-typed content part. agent.reasoningDefault was set to "on" during this run; behavior identical to default.

Gateway version: OpenClaw Gateway v2026.3.31. Claude CLI v2.1.114.

Additional information

Relevant existing docs that describe the channel-side feature: docs/tools/thinking.md ("Reasoning visibility (/reasoning)", levels on|off|stream, explicitly scoped to chat channels today).

Relevant docs that describe the HTTP surface: docs/gateway/openresponses-http-api.md lists reasoning as "Accepted but currently ignored".

Relevant docs that describe the per-agent knob: docs/gateway/configuration-reference.md — reasoningDefault: "on | off | stream", "Applies when no per-message or session reasoning override is set."

Prior art this mirrors: OpenAI Responses API reasoning output items (https://developers.openai.com/api/reference/resources/responses#responses-create); Anthropic Messages API extended thinking (https://platform.claude.com/docs/en/build-with-claude/extended-thinking).

Backward compatibility: the default-off path preserves today's behavior exactly. Clients that don't know about reasoning items ignore them (they appear as a new unknown output_item.type, already a defensive path in most OpenAI SDKs). Opt-in via the existing reasoningDefault: "on" config.

Open questions for maintainers:

  1. Is there a reason reasoningDefault was scoped to chat channels only originally (privacy, or accidental)? Knowing this would shape the implementation.

  2. Should reasoning: {effort: ...} on /v1/responses be wired to map to /think levels internally, or stay ignored with visibility controlled by agent config only? Happy either way.

  3. Event-type preference: response.reasoning_text.delta (matches OpenAI Responses beta naming) vs. treating reasoning as a nested item and reusing response.output_text.delta on the reasoning item's text content? We'll align with whichever shape ships.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:needs-maintainer-reviewClawSweeper marked this issue as needing maintainer review before automation.clawsweeper:needs-product-decisionClawSweeper marked this issue as needing a product or behavior decision.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.enhancementNew feature or requestimpact:message-lossChannel message delivery can be lost, duplicated, or misrouted.impact:securitySecurity boundary, credential, authz, sandbox, or sensitive-data risk.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.maturity:stableIssue affects a taxonomy feature currently scored M4/M5.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions