Skip to content

serve: stream GLM reasoning through reasoning_content on the OpenAI endpoint (#597 item 4) - #607

Merged
JustVugg merged 1 commit into
devfrom
fix/597-glm-reasoning-content
Jul 25, 2026
Merged

serve: stream GLM reasoning through reasoning_content on the OpenAI endpoint (#597 item 4)#607
JustVugg merged 1 commit into
devfrom
fix/597-glm-reasoning-content

Conversation

@JustVugg

Copy link
Copy Markdown
Owner

Fixes #597 item 4 for the OpenAI /v1/chat/completions endpoint.

The bug (worse than my earlier triage said)

On the OpenAI endpoint, GLM's reasoning was never separated from the answer:

  • with tools, it was silently dropped (parse_tool_calls strips everything before </think>);
  • without tools, the raw reasoning and the literal </think> were streamed as ordinary content.

Only the Anthropic endpoint (split_thinking_reply) and Inkling (InklingStreamSplit) handled it. My earlier comment on #597 that non-streaming was "already handled" was wrong — it was handled only for Anthropic/Inkling, not the OpenAI GLM path. This PR corrects that.

The fix

Route GLM reasoning to reasoning_content on both OpenAI chat paths, reusing the existing cross-chunk ThinkingStreamSplit:

  • streaming, no tools — reasoning → reasoning_content deltas, answer → content deltas.
  • streaming, tools — a think splitter runs before the tool-call buffer, so reasoning goes to reasoning_content and never reaches parse_tool_calls' input (@mkelcb's exact warning).
  • non-streamingsplit_thinking_reply fills message.reasoning_content and a clean message.content.
  • <think>/</think> markers are never exposed.

ThinkingStreamSplit gains initial_thinking (start in answer mode when thinking is off, so a marker-free answer isn't misfiled as reasoning) and a close() alias; enable_thinking is plumbed from chat_completion into generation().

Tests (the point of this PR)

5 unit cases on the splitter — single-chunk, close-tag split across chunks (abc</thi+nk>def), stray open marker, thinking-off is all-answer, missing close tag surfaces reasoning.
5 end-to-end HTTP cases — streaming split, cross-chunk </think>, thinking-off all-content, reasoning-stays-out-of-tool-args and out of visible answer, non-streaming split.
Full server suite 78/78.

Part of the #597 series (items 2 + 5 landed in #606). Remaining: item 3 (retest), item 6 (ACCEPT-frame context ordering — native protocol, separate PR).

…ndpoint (#597 item 4)

On /v1/chat/completions, GLM's reasoning was not separated from the answer: with
tools it was silently dropped (parse_tool_calls strips everything before </think>),
without tools the raw reasoning AND the literal </think> were streamed as ordinary
`content`. Only the Anthropic endpoint and Inkling split it. (My earlier note on
#597 that non-streaming was already handled was wrong — it was handled only for
Anthropic/Inkling, not the OpenAI GLM path.)

Route GLM reasoning to `reasoning_content` on both the streaming and non-streaming
OpenAI chat paths, reusing the existing cross-chunk ThinkingStreamSplit:

- streaming, no tools: reasoning -> reasoning_content deltas, answer -> content deltas
- streaming, tools: a think splitter runs BEFORE the tool-call buffer, so reasoning
  goes to reasoning_content and never contaminates parse_tool_calls' input
- non-streaming: split_thinking_reply populates message.reasoning_content and a clean
  message.content
- <think>/</think> markers are never exposed to the client

ThinkingStreamSplit gains initial_thinking (start in answer mode when thinking is off,
so a marker-free answer isn't filed as reasoning) and a close() alias for the
streaming path; enable_thinking is plumbed from chat_completion into generation().

Tests: 5 unit cases on the splitter (single-chunk, close-tag split across chunks,
stray open marker, thinking-off all-answer, missing close tag) + 5 end-to-end HTTP
cases (streaming split, cross-chunk </think>, thinking-off, reasoning-out-of-tool-args,
non-streaming split). Full server suite 78/78.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@JustVugg
JustVugg merged commit cf56052 into dev Jul 25, 2026
10 checks passed
@JustVugg
JustVugg deleted the fix/597-glm-reasoning-content branch July 25, 2026 11:38
NeuralNotwerk pushed a commit to NeuralNotwerk/colibri that referenced this pull request Jul 25, 2026
…en 200 stream (JustVugg#597 item 6)

A streaming chat request committed its HTTP 200 (SSE headers) before the engine
tokenised the prompt and checked it against CTX. An over-long prompt was only then
found -- ERROR CONTEXT_EXCEEDED -- far too late to send the 400 the client needs, so
the client saw a broken/empty stream instead of a clean context_length_exceeded.
(Non-streaming was already correct: it sends its 200 only after generation.)

Add an acceptance frame to the engine<->gateway protocol (mkelcb's design in JustVugg#597):

- c/colibri.c mux_submit(): after all cheap validation (encode, non-empty, fits CTX)
  and BEFORE prefill, emit `ACCEPT <id> <ntok>`. Every early failure already returns
  first with an ERROR, so a request yields exactly one of ACCEPT or an early ERROR.
- Engine._dispatch_stdout(): parse ACCEPT -> ("accept", {...}) without popping pending.
- Engine.generate(): new on_accept callback; commit exactly once on the first of
  ACCEPT / DATA / DONE. DATA/DONE before an ACCEPT still commit (an older engine with
  no ACCEPT frame keeps working); a second ACCEPT is a protocol error.
- APIHandler.generation() streaming: defer the SSE 200 + keepalive into start_stream(),
  fired via on_accept. An ERROR that arrives before ACCEPT now propagates as an APIError
  with nothing committed, so do_POST returns a normal JSON 400.

Tests: 4 Engine-level cases over a fake engine process (ACCEPT-before-DATA fires
on_accept first; ERROR-before-ACCEPT raises 400 and never commits; DATA-before-ACCEPT
still commits for an old engine; duplicate ACCEPT is a protocol error) + 1 end-to-end
HTTP case (streaming context-exceeded returns a clean 400 with context_length_exceeded,
not a 200 stream). Full server suite 83/83; the C engine builds clean.

Completes the JustVugg#597 series (items 1/2/4/5 already on dev via JustVugg#535/JustVugg#606/JustVugg#607).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant