From 4ffe555bfda9712b6342fd5fbaa7a5c9ca794057 Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Sun, 7 Jun 2026 14:34:35 +0100 Subject: [PATCH] fix(captcha): strip reasoning-model blocks before json extraction Reasoning models prefix responses with ... (or ...) scratchpad that can itself contain a JSON-looking object. The brace-counting extractor would grab that inner object instead of the real answer. Strip think/thinking blocks via a package-level regexp before the balanced-object scan. Adds table cases covering both tag spellings while keeping the plain, prose-wrapped, and unbalanced cases intact. --- internal/captcha/json.go | 13 ++++++++++++- internal/captcha/json_test.go | 10 ++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) 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) {