[Bug]: openai-completions merges `delta.reasoning` into the answer (OpenRouter-style sibling of #95280, not covered by #95283) · Issue #139996 · openclaw/openclaw · GitHub
Skip to content

[Bug]: openai-completions merges delta.reasoning into the answer (OpenRouter-style sibling of #95280, not covered by #95283) #139996

Description

@sdalu

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

Providers that stream reasoning in delta.reasoning (the OpenRouter
convention) have it concatenated onto the answer in a single text part,
with no thinking part — the same defect #95283 fixed for
reasoning_content.

Steps to reproduce

  1. Configure a custom openai-completions provider for OVHcloud AI
    Endpoints (https://oai.endpoints.kepler.ai.cloud.ovh.net/v1) with
    model gpt-oss-20b, reasoning: true, no compat overrides.
  2. Run a turn whose answer is easy to compare exactly:
    openclaw agent --model ovh/gpt-oss-20b --message 'Reply with exactly: ovh-ok'
  3. Read the stored assistant message parts.

Not deterministic: over one session's transcript, 2 of 3 turns split
correctly and 1 merged, same model, same version, same gateway. Repeat
the turn until a merged one appears.

Expected behavior

delta.reasoning opens and seals a thinking block exactly as
reasoning_content does after #95283, so the answer is delivered
separately:

parts=['thinking', 'text']
text='ovh-ok'

This is also what the same gateway produces on the turns that work.

Actual behavior

The reasoning and the answer arrive as one text part, no thinking part,
no separator:

parts=['text']
text='The user wants me to reply exactly "ovh-ok". No extra text.ovh-ok'

OpenClaw version

2026.9.2 (3928bad)

Operating system

FreeBSD 15.1-RELEASE-p3

Install method

npm install -g openclaw@latest, into an unprivileged user's ~/.local

Model

gpt-oss-20b

Provider / routing chain

Client -> local gateway -> OVHcloud AI Endpoints (https://oai.endpoints.kepler.ai.cloud.ovh.net/v1, api openai-completions). No proxy, no router, no TLS override.

Additional provider/model setup details

Custom provider block declaring the model with reasoning: true. No
compat block. request.proxy / request.tls unset. Streaming is on;
the request carries stream_options.include_usage: true.

Logs

# The provider separates the fields correctly. Three sampled streams,
# same request; identical shape each time:
delta keys observed:  "content" "name" "reasoning" "role"
content   ->  "ovh-ok"                 # clean, 3/3
reasoning ->  "The user says: ..."     # separate field, 3/3

# Final chunk is spec-correct:
data: {"choices":[],"model":"gpt-oss-20b","object":"chat.completion.chunk",
       "usage":{"prompt_tokens":68,"completion_tokens":8,"total_tokens":76}}

# What OpenClaw stored for a merged turn:
parts=['text']
text='The user wants me to reply exactly "ovh-ok". No extra text.ovh-ok'

# And for a correct turn, same model and version:
parts=['thinking', 'text']  thinkingSignature='reasoning'
text='ovh-ok'

Screenshots, recordings, and evidence

Counted over one session's stored transcript, filtered to this provider:
split (thinking+text) 2, text-only 1, empty 0.

Impact and severity

The visible answer is intermittently polluted with the model's own
reasoning. Anything consuming that text programmatically — a script, a
downstream tool, an automation parsing a short answer — receives a wrong
value rather than an obviously broken one, and only on some turns. Any
provider using the OpenRouter delta.reasoning convention is affected.

Additional information

#95280 reported the same shape for deepseek and #95283 fixed it by
sealing the open thinking block before visible text or a tool call
(merged 2026-06-22, released v2026.6.11, so present in the build tested
here). That fix seals reasoning_content deltas; this provider streams
delta.reasoning, so the same missing boundary remains on the other
field.

Two things that do not work as workarounds, both tested:

  • compat.thinkingFormat: "openrouter" changes the request rather than
    the parse; the provider then rejects every call with "provider
    rejected the request schema or tool payload".
  • The model cannot be asked to stop reasoning: reasoning_effort: "low"
    is accepted and shortens the text but the field is still emitted, and
    chat_template_kwargs returns HTTP 400.

Possibly the same code path: this provider also records zero token usage
(#75357, #56670) although the request sends
stream_options.include_usage and the stream returns the usage chunk
quoted above. In the same session, anthropic-messages turns recorded
61 tokens and $0.036 while the openai-completions turn beside them
recorded 0.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.bugSomething isn't workingbug:behaviorIncorrect behavior without a crashimpact:session-stateSession, memory, transcript, context, or agent state can drift or corrupt.issue-rating: 🦪 silver shellfishThin issue quality; more reproduction proof or environment detail is needed.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions