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)
+}