Skip to content

fix: normalize None OpenAI content for reasoning-parser backends - #188

Open
yaroxy wants to merge 1 commit into
alexzhang13:mainfrom
yaroxy:fix/openai-none-content
Open

yaroxy wants to merge 1 commit into
alexzhang13:mainfrom
yaroxy:fix/openai-none-content

Conversation

@yaroxy

@yaroxy yaroxy commented Sep 20, 2026

Copy link
Copy Markdown

Summary

  • OpenAI-compatible servers with a reasoning parser (e.g. sglang --reasoning-parser qwen3) can return message.content = None while putting the thinking trace in message.reasoning_content.
  • RLM then passes that value into find_code_blocks → re.finditer, and also into message history / RLMIteration.response, which crashes mid-run with:
    TypeError: expected string or bytes-like object, got 'NoneType'.
  • Add _extract_content() in OpenAIClient so completion / acompletion always return a str: prefer content, else fall back to reasoning_content, else "".

Related to #166. Complementary to #175 (which guards find_code_blocks); this is closer to Option B suggested in that issue — normalize at the LM-client boundary so every downstream consumer sees a string, and so reasoning text is not discarded when content is empty/None.

Why not only #175?

#175 stops the regex crash, but None can still leak into trajectory / history / partial-answer tracking. For reasoning backends the model may have produced useful thinking with no post-</think> body; falling back to reasoning_content preserves that progress instead of treating the turn as empty.

Root cause (verified)

Servers that split think vs answer often implement something like content = text if text else None. content becomes None when:

  1. generation is truncated inside the think block (finish_reason == "length");
  2. </think> is closed but no further tokens (or only whitespace) are emitted;
  3. </think> never appears before EOS, so the whole output is classified as reasoning.

Reproduced against sglang + Qwen3.5 with forced small max_tokens (truncation during think → content=None, reasoning_content populated).

Changes

  • rlm/clients/openai.py: _extract_content() + use it in sync/async completion paths.

Out of scope / follow-ups

sglang/vLLM-style reasoning parsers can leave message.content as None
when thinking is truncated or empty after </think>, which then crashes
find_code_blocks via re.finditer. Fall back to reasoning_content.

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.

1 participant