Skip to content

fix(indicator): strip reasoning-model <think> blocks from AI-generated code - #299

Closed
kenylerich wants to merge 2 commits into
OpenByteInc:mainfrom
kenylerich:fix/strip-llm-think-blocks-in-codegen
Closed

kenylerich wants to merge 2 commits into
OpenByteInc:mainfrom
kenylerich:fix/strip-llm-think-blocks-in-codegen

Conversation

@kenylerich

Copy link
Copy Markdown
Contributor

Problem

AI-assisted indicator code generation failed permanently with Security Error: Unsafe code rejected: Code syntax error whenever the configured LLM was a reasoning model such as MiniMax-M3. Root cause verified against a live endpoint: those models prepend <think>...</think> chains of thought, _strip_code_fences() passed the prose through, and the safety validator's ast.parse() rejected it. The auto-fix retry re-entered the same path and failed identically, so the UI always reported a broken result even though the generated code itself was valid.

Fix

Harden _strip_code_fences() in app/services/indicator_ai_generation.py:

  • Remove complete <think>...</think> blocks (case-insensitive, multiline).
  • Truncate at an unterminated <think> tag (truncated model output).
  • When any complete fenced block exists, return the last one instead of only handling fences at the very start/end, so prose or explanation fences around the code no longer leak into validation.
  • Plain fenced / plain code behavior unchanged.

Testing

  • New regression tests: tests/test_indicator_ai_generation_strip.py (12 cases: terminated/multiline/case-insensitive/unterminated think blocks, prose around fenced code, multiple fences, empty inputs, prose-only passthrough) — all pass.
  • Existing tests/test_ai_generation_prompt_contracts.py — 17 pass, no regression.
  • End-to-end against a live MiniMax-M3 endpoint in the running backend container: raw model output → patched stripper → ast.parse succeeds.

"""
text = str(text or "")
text = _THINK_BLOCK_RE.sub("", text)
unterminated = text.lower().find("<think>")
…d code

AI code generation always failed with "Unsafe code rejected: Code syntax
error" when the configured LLM was a reasoning model (e.g. MiniMax-M3).
Those models prepend <think>...</think> chains of thought to the code, and
_strip_code_fences() passed the prose through to the safety validator,
where ast.parse() rejected it. The auto-fix retry hit the same path and
also failed, so users saw a permanent failure even though the generated
code itself was valid.

Harden _strip_code_fences():

- Remove complete <think>...</think> blocks (case-insensitive, multiline).
- Truncate at an unterminated <think> tag (truncated model output).
- When any complete fenced block exists, return the last one instead of
  only handling fences at the very start/end, so prose or explanation
  fences surrounding the code no longer leak into validation.

Plain fenced/plain code behavior is unchanged. Verified end to end
against a live MiniMax-M3 endpoint: output now passes ast.parse after
stripping.

Adds regression tests covering think blocks (terminated, multiline,
case-insensitive, unterminated), prose around fenced code, multiple
fences, empty inputs, and the unchanged prose-only passthrough.
The previous fix only covered initial candidate generation. The auto-fix
repair path in routes/indicator.py (_repair_code_via_llm) carried its own
inline startswith/endswith fence strip without think handling, so a
reasoning model's repaired code still failed validation with "Unsafe code
rejected: Code syntax error" even when the fix itself was correct.

Centralize the sanitizer and cover every LLM response parser:

- Add shared strip_think_blocks() to app/services/llm.py.
- _repair_code_via_llm now reuses the hardened _strip_code_fences().
- _parse_payload() in ai_code_edits.py strips think prose before
  json.loads() (edit-operation payloads had the same leak).
- safe_call_llm() strips think prose before JSON parsing.
- indicator_ai_generation.py uses the shared helper (no behavior change).

Adds regression tests for strip_think_blocks() and think-prefixed edit
payloads. Verified end to end against a live MiniMax-M3 endpoint.
@kenylerich
kenylerich force-pushed the fix/strip-llm-think-blocks-in-codegen branch from 1961d26 to 3d6ebbb Compare October 19, 2026 16:33
@kenylerich kenylerich closed this Oct 19, 2026
@kenylerich
kenylerich deleted the fix/strip-llm-think-blocks-in-codegen branch October 19, 2026 16:36
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