Skip to content

find_code_blocks() crashes with TypeError when LM response is None #166

@LMS927369

Description

@LMS927369

Bug

rlm/utils/parsing.py:18 calls re.finditer(pattern, text, re.DOTALL) on the response from lm_handler.completion() without validating that text is a string. When the LM client returns None (which we've observed intermittently from the Cerebras OpenAI-compatible API at scale on zai-glm-4.7), the regex chokes and the whole RLM run dies with a TypeError.

Stack trace (verified against current main, commit 8467a58)

File "rlm/core/rlm.py", line 605, in _completion_turn
    code_block_strs = find_code_blocks(response)
File "rlm/utils/parsing.py", line 18, in find_code_blocks
    for match in re.finditer(pattern, text, re.DOTALL):
File ".../python3.14/re/__init__.py", line 285, in finditer
    return _compile(pattern, flags).finditer(string)
TypeError: expected string or bytes-like object, got 'NoneType'

Repro

  • Environment: Python 3.14, rlms installed from git+main@8467a58
  • Backend: Cerebras API (base_url=https://api.cerebras.ai/v1), model_name=zai-glm-4.7, OpenAI-compatible
  • RLM config: environment="local", compaction=True, compaction_threshold_pct=0.80, max_depth=2, max_iterations=50, max_timeout=240.0
  • Corpus: 25 Python files, ~100K input tokens
  • Behavior: intermittent — re-running the same input sometimes succeeds (with the model honestly reporting it can't read the corpus) and sometimes hits this TypeError mid-iteration. The crash typically happens 3-6 iterations in, after several search() / parse_structure() tool calls.

Why it's a bug rather than caller error

lm_handler.completion() is expected to return a string for downstream parsing. The upstream API (Cerebras / GLM 4.7) is occasionally returning a response object whose content field deserializes to None instead of empty-string — likely under tool-use pressure or near context limits. Whether the client should normalize None → "" is one option; whether find_code_blocks should defensively check text is None is another. Either way, the current code raises a hard TypeError that aborts an entire RLM run rather than letting the iteration continue with empty content (which would let compaction / fallback paths handle the missing response).

Suggested fix (one of)

Option A — defensive guard in find_code_blocks:

# rlm/utils/parsing.py
def find_code_blocks(text):
    if not text:
        return []
    for match in re.finditer(pattern, text, re.DOTALL):
        ...

Option B — normalize at the LM-client boundary (e.g., in BaseLM.completion post-processing): coerce None to "" before returning, so every code path downstream sees a string. Logs a warning when the coercion fires so providers returning malformed responses are visible.

Option A is the smaller change; Option B is the more principled fix because the same None would also break any other downstream regex in the codebase that consumes response.

Context

This is distinct from PR #127 (Claude 4.6 text-FINAL hallucination, superseded by #162 new_final_answer_format). PR #162's protocol change handles the case where the LM emits trailing text-FINAL; it doesn't help when the LM returns None at all.

Happy to send a PR for Option A if useful. Discovered while writing a downstream verification harness for KCE — the LM was almost certainly under near-context-limit pressure when this fired.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions