diff --git a/internal/captcha/json.go b/internal/captcha/json.go
index 274b2c6..6b1f532 100644
--- a/internal/captcha/json.go
+++ b/internal/captcha/json.go
@@ -1,13 +1,24 @@
package captcha
-import "strings"
+import (
+ "regexp"
+ "strings"
+)
+
+// thinkRe matches reasoning-model scratchpad blocks (… or
+// …). These can themselves contain JSON-looking objects,
+// so they are stripped before the brace scan to avoid grabbing the wrong object.
+var thinkRe = regexp.MustCompile(`(?s).*?`)
// extractJSON returns the first balanced {...} object found in s, or s unchanged
// if no opening brace is present. It scans by brace depth so prose, markdown
// fences, or an example object before the real one do not corrupt parsing.
+// Reasoning-model … blocks are stripped first so a JSON-looking
+// object inside the scratchpad is not mistaken for the answer.
// Note: it does not account for braces inside string literals; that is
// acceptable for the short, structured captcha responses this handles.
func extractJSON(s string) string {
+ s = thinkRe.ReplaceAllString(s, "")
start := strings.IndexByte(s, '{')
if start < 0 {
return s
diff --git a/internal/captcha/json_test.go b/internal/captcha/json_test.go
index 564151b..27e703e 100644
--- a/internal/captcha/json_test.go
+++ b/internal/captcha/json_test.go
@@ -27,6 +27,16 @@ func TestExtractJSON(t *testing.T) {
},
{"no brace", `no json here`, `no json here`},
{"unbalanced returns from start", `tail {"text":"x"`, `{"text":"x"`},
+ {
+ "think block stripped, real object wins",
+ "reasoning with {\"tiles\":[9]}\n{\"tiles\":[0,1,2],\"confidence\":0.9}",
+ `{"tiles":[0,1,2],"confidence":0.9}`,
+ },
+ {
+ "thinking block stripped",
+ `let me reason {"tiles":[5]}{"text":"ab12"}`,
+ `{"text":"ab12"}`,
+ },
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {