fix: preserve unfinished reasoning across renderers and bridges - #152
Conversation
9a408f5 to
cbb30de
Compare
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR makes broad production-path changes to response parsing, tool-call classification, multiturn bridging, and the public renderer protocol across many model families. The new shared reasoning state and prompt-context behavior warrant human review for cross-renderer compatibility and API integration impact. Notes:
You can add or adjust custom eligibility rules. Learn more. |
cbb30de to
79abba6
Compare
79abba6 to
04a9321
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 04a9321. Configure here.
| text = "Hello there!" | ||
| ids = tokenizer.encode(text, add_special_tokens=False) | ||
| parsed = renderer.parse_response(ids) | ||
| parsed = renderer.parse_response(ids, prompt_ids=[]) |
There was a problem hiding this comment.
I'm a little confused about the difference between renderer.parse_response(ids, prompt_ids=[]) vs renderer.parse_response(ids, prompt_ids=None). These code paths are treated differently. Can you clarify the intention here? Haven't understood when each is appropriate.
There was a problem hiding this comment.
good catch, this is a smell. the intention was to be able to tell apart “you weren’t given the prompt, so assume the renderer’s usual prompt” (the None) and “assume nothing opened reasoning beforehand.” (the empty list).
but actually, parsing shhould always depend on the supplied tokens, so reworking the API to make this clearer.
04a9321 to
e679220
Compare
e679220 to
94689c2
Compare

When a generation prompt already opens reasoning, a completion can end before emitting the closing marker. The parser currently treats that unfinished reasoning as the final answer, and a bridge can carry the open reasoning block into the next turn.
This PR preserves unfinished reasoning across all supported reasoning formats and closes it safely when building the next prompt.
Closes #137 — unfinished reasoning returned as final content.
Closes #139 — reasoning left open by
bridge_to_next_turn.Parsing
For example, with a prompt ending in
<think>and a completion ofLet me work this out, parsing returns:For reasoning-first formats, reasoning starts only with an initial
<think>after any assistant header, or when the prompt already opened the block. The first</think>switches to content. Later markers remain literal content:Before<think>example</think>Afteris entirely content. Harmony, Gemma, and Inkling follow their explicit channel or message-segment grammar.Tool-call drafts inside unfinished reasoning also stay in
reasoning_content.parse_responseaccepts optionalprompt_idsto identify the starting channel, including partial assistant prefixes and Gemma's post-tool reasoning. Omittingprompt_ids, passingNone, and passing[]all mean the completion is self-contained: reasoning needs its own initial opener or channel header. Generation settings do not supply missing parsing context. Callers parsing a continuation whose prompt supplied the opener must pass that prompt.reasoning_completeisTruefor closed reasoning andNonewhen the state cannot be determined. An empty final answer can still have closed reasoning; consumers decide whether an answer is useful.The generate client supplies prompt context and forwards
reasoning_complete. Custom renderers used withgeneratemust accept the optionalprompt_idskeyword.Bridging
Each renderer appends its missing reasoning close before continuing the conversation. Added tokens are prompt scaffolding, marked non-sampled and non-content. The original prompt and sampled completion remain an exact prefix.
If a sampled stop prevents that repair without changing existing tokens, the bridge returns
Nonefor a full-render fallback. Parsing and bridging share boundary detection independently; each format owns its closing rules.Validation
Regression coverage exercises every reasoning renderer with thinking enabled and disabled, reasoning opened in the prompt or completion, partial and closed prompt prefixes, complete or truncated output, and literal think markers after content begins. It also checks tool arguments and token spans, tool drafts, literal marker lookalikes, empty final answers, and exact bridge prefixes.
Note
Medium Risk
Broad behavioral change to response parsing, multiturn bridging, and the
parse_responseprotocol across many model families; incorrect adoption ofprompt_idscould mis-classify reasoning vs content in custom integrations.Overview
Adds shared reasoning-boundary detection (
scan_reasoning,prompt_ends_in_reasoninginrenderers/reasoning.py) and threads it through parse and bridge paths for every reasoning-capable renderer.Parsing:
parse_responsenow accepts optionalprompt_idsso completions that continue a prompt-opened reasoning channel (e.g. prefilled ``, Gemma post-tool thought, Harmony analysis) are not treated as final content or executable tool calls when the close marker is missing.ParsedResponse.reasoning_completereports whether the reasoning channel is closed (`False` when truncated inside reasoning). Self-contained parsing is unchanged when `prompt_ids` is omitted, `None`, or `[]`. Token-level parsers and `ThinkTextReasoningParser` only treat an initial think opener as reasoning; later markers stay literal content.Bridging:
bridge_to_next_turnchecks the prior completion for an open reasoning region and appends the format’s closing marker as non-loss prompt context before the turn close; if a stop token was already sampled inside open reasoning, it returnsNone(no silent repair). The generate client passes prompt ids intoparse_responseand exposesreasoning_completeon the response. Examples and README document the contract.Reviewed by Cursor Bugbot for commit be74213. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Preserve unfinished reasoning in renderer parsers and multiturn bridges
prompt_tokenargument toRenderer.parse_responsein base.py so parsers can detect reasoning opened by the generation promptNonewhen a stop token is already presentreasoning_completestatus field toParsedResponsein base.py and forwards it through the generation client in client.pyRenderer.parse_responseprotocol method signature changes to acceptprompt_token. Out-of-tree implementations must add the new parameter to match the interfaceChanges since #152 opened
parse_gpt_ossfunction,Gemma4Renderer.parse_responsemethod, andGptOssRenderer.bridge_to_next_turnmethod to track whether reasoning extends to the end of the token stream [be74213]unfinished_reasoningfunction fromrenderers.parsingmodule and inlined its behavior at call sites [be74213]tests.test_reasoning_boundariesmodule validating GPT-OSS reasoning boundary behavior [be74213]Macroscope summarized 94689c2.