Skip to content

fix(gpt-oss): keep a tool call's sampled content type on re-render - #215

Draft
willccbb wants to merge 1 commit into
gpt-oss-tool-call-arguments-textfrom
gpt-oss-sampled-tool-call-header
Draft

willccbb wants to merge 1 commit into
gpt-oss-tool-call-arguments-textfrom
gpt-oss-sampled-tool-call-header

Conversation

@willccbb

@willccbb willccbb commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

A gpt-oss tool call's header carries a content type: <|constrain|>json or a bare code. When a conversation is re-rendered from messages, the renderer picks the content type from the arguments (json for a JSON object, code otherwise). The model doesn't always match the two. It can sample

<|channel|>commentary to=functions.bash code<|message|>{"command":"ls"}<|call|>

and re-rendering that call from its message writes

<|channel|>commentary to=functions.bash <|constrain|>json<|message|>{"command":"ls"}<|call|>

so the re-rendered history no longer matches the tokens the model sampled.

The fix:

  • ParsedToolCall gains content_type. The gpt-oss parser sets it from the call's header: the word after <|constrain|>, else the header's bare content type, and None when the header has none.
  • The gpt-oss renderer uses a tool call's content_type when it's given (json or code, on the tool-call dict or its function dict). Otherwise it falls back to the arguments' type as before, so calls without it render unchanged.

Tests: a code call on JSON arguments now round-trips token for token, and header parsing is covered for both header orders and for a header with no content type.

This is stacked on gpt-oss-tool-call-arguments-text, which adds arguments_text the same way.


Note

Low Risk
Scoped to gpt-oss Harmony parse/render round-trips; other renderers leave content_type unset and behavior unchanged when the field is omitted.

Overview
gpt-oss tool-call headers can say json (<|constrain|>json) or code (bare code), but re-rendering from messages used to infer the type from the argument body only. When the model sampled code for a JSON object, the history no longer matched the original tokens.

ParsedToolCall now carries optional content_type, set by the gpt-oss parser from the header (word after <|constrain|>, otherwise the bare type after the recipient, including recipient-first layouts). The gpt-oss renderer honors content_type on the tool-call or function dict when it is json or code, and otherwise keeps the old argument-based fallback.

New tests cover code on JSON arguments round-tripping token-for-token and header parsing when the type is missing.

Reviewed by Cursor Bugbot for commit b77bec7. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Preserve sampled gpt-oss tool-call content type on re-render

  • parse_gpt_oss in parsing.py now extracts the content-type word from tool-call headers and stores it on ParsedToolCall, including for invalid-JSON calls. Other formats leave it None.
  • GptOssRenderer reads content_type from the tool-call dict (or its function dict) and uses it when it is json or code; otherwise it infers the type from the arguments as before.
  • Previously, a sampled code header on JSON arguments was re-rendered as json, producing different assistant tokens than what was sampled.
  • Behavioral Change: ParsedToolCall gains an optional content_type field defaulting to None; re-rendered gpt-oss headers can now use code where they previously always used json for JSON arguments.

Macroscope summarized b77bec7.

A gpt-oss tool call's header carries a content type: ` <|constrain|>json` or a
bare ` code`. The renderer picked it from the arguments when re-rendering a
conversation from messages, but the model doesn't always match the two: it can
sample

    <|channel|>commentary to=functions.bash code<|message|>{"command":"ls"}<|call|>

and a re-render of that call wrote

    <|channel|>commentary to=functions.bash <|constrain|>json<|message|>{"command":"ls"}<|call|>

so the re-rendered history no longer matched the sampled tokens.

ParsedToolCall gains content_type, the parser sets it from the call's header
(the word after <|constrain|>, else the header's bare content type, None when
there is none), and the renderer uses a tool call's content_type when given
(json or code), falling back to the arguments' type as before.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Will Brown <will@primeintellect.ai>
Co-authored-by: Cursor <cursoragent@cursor.com>

This branch has not been deployed

No deployments
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.

2 participants