Repository navigation
find_code_blocks() crashes with TypeError when LM response is None #166
Description
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,
rlmsinstalled 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
TypeErrormid-iteration. The crash typically happens 3-6 iterations in, after severalsearch()/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.
Activity
LMS927369 commentedon May 26, 2026
Adding quantified incidence data from a downstream benchmark matrix we ran 2026-05-26 against
rlmsat commit8467a580(current main).Matrix configuration:
zai-glm-4.7(premium-tier reasoning model)compaction=True,compaction_threshold_pct=0.80,max_depth=2,max_iterations=50,max_timeout=240.0s,environment="local"Crash incidence:
(Of the remaining 5 non-crashed runs at 90K, the model produced output that failed downstream validation for unrelated reasons — fabricated file paths, excerpt mismatch, missing required sections. Those are separate failure modes; this issue concerns only the
find_code_blocks(None)crash.)Pattern observed: crashes consistently fire several iterations into the RLM loop, after multiple successful
search()/parse_structure()tool calls. The model is mid-task when the LM API returns a response object whose content deserializes toNone. At 75K specifically, every single run crashed — making this corpus size effectively unusable on the current backend.Why the suggested fix matters in practice:
Option A (defensive guard in
find_code_blocks) would convert a hardTypeErrorabort into the RLM loop seeing an empty code-block list for that iteration. The subsequent iteration would prompt the model again and likely get usable output. In our matrix, 22 of 27 runs would have had a chance to continue rather than dying mid-stream — substantially raising the floor of what's reachable in the 60K-90K band.Option B (normalize
None → ""at the LM-client boundary) is structurally the same outcome forfind_code_blocksand also future-proofs any other downstream consumer that doesn't expectNone.Happy to send a PR for Option A if you'd like — single-file change with a one-line guard at the top of
find_code_blocks. The fix is the same regardless of which call-site triggers; I just have the empirical evidence handy that this site is the dominant one at production-relevant scales.Matrix artifacts (raw JSON per run + summary) are preserved locally; can share if useful for repro.
AaryanPuri commentedon Jul 21, 2026
I've opened a PR for this: #175