From 185cb4586eb494719c5384356a156cfa77b748ae Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Mon, 9 Mar 2026 18:17:06 +0200 Subject: [PATCH] fix: strip R1 think blocks before JSON extraction DeepSeek-R1 models emit ... before their actual response. The brace-counting extractor would grab the first { inside the thinking block (which contains partial JSON fragments) rather than the final strategy JSON. strip_think_blocks() removes all ... sections including unterminated blocks (truncated responses), leaving only the final output for extract_json to process. Co-Authored-By: Claude Sonnet 4.6 --- src/claude.rs | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/claude.rs b/src/claude.rs index cef64a9..370ca8e 100644 --- a/src/claude.rs +++ b/src/claude.rs @@ -98,9 +98,13 @@ impl ClaudeClient { } } -/// Extract a JSON object from Claude's response text. -/// Looks for the first `{` ... `}` block, handling markdown code fences. +/// Extract a JSON object from a model response text. +/// Handles markdown code fences and R1-style `...` blocks. pub fn extract_json(text: &str) -> Result { + // Strip R1-style thinking blocks before looking for JSON + let text = strip_think_blocks(text); + let text = text.as_ref(); + // Strip markdown fences if present let cleaned = text .replace("```json", "") @@ -137,3 +141,25 @@ pub fn extract_json(text: &str) -> Result { serde_json::from_str(&cleaned[s..e]).context("parse extracted JSON") } + +/// Remove `...` blocks emitted by R1-family reasoning models. +/// Handles nested tags and unterminated blocks (truncated responses). +fn strip_think_blocks(text: &str) -> std::borrow::Cow<'_, str> { + if !text.contains("") { + return std::borrow::Cow::Borrowed(text); + } + let mut out = String::with_capacity(text.len()); + let mut rest = text; + while let Some(start) = rest.find("") { + out.push_str(&rest[..start]); + rest = &rest[start + "".len()..]; + if let Some(end) = rest.find("") { + rest = &rest[end + "".len()..]; + } else { + // Unterminated — discard the rest (truncated thinking block) + rest = ""; + } + } + out.push_str(rest); + std::borrow::Cow::Owned(out) +}