diff --git a/app/reviewer.py b/app/reviewer.py index 6e5acd9..e1cc35e 100644 --- a/app/reviewer.py +++ b/app/reviewer.py @@ -173,7 +173,18 @@ def _safe_json(text: str) -> dict[str, Any]: try: return json.loads(text) except json.JSONDecodeError: - start, end = text.find("{"), text.rfind("}") - if start != -1 and end != -1: - return json.loads(text[start : end + 1]) - raise + pass + # Reasoning models often emit chain-of-thought (which may contain stray "{") + # before the JSON object. Scan every "{" and return the first that decodes to + # an object, rather than assuming the span from the first "{" to the last "}". + decoder = json.JSONDecoder() + for i, ch in enumerate(text): + if ch != "{": + continue + try: + obj, _ = decoder.raw_decode(text[i:]) + except json.JSONDecodeError: + continue + if isinstance(obj, dict): + return obj + raise json.JSONDecodeError("no JSON object found", text, 0) diff --git a/tests/test_reviewer.py b/tests/test_reviewer.py index 726b14d..0eb1a19 100644 --- a/tests/test_reviewer.py +++ b/tests/test_reviewer.py @@ -36,6 +36,12 @@ def test_safe_json_prose_wrapped() -> None: assert _safe_json(text) == {"summary": "ok"} +def test_safe_json_skips_reasoning_braces() -> None: + # A reasoning preamble containing stray "{" must not confuse extraction. + text = 'We should return {the result}. Final: {"summary": "ok", "block": []} done.' + assert _safe_json(text) == {"summary": "ok", "block": []} + + def test_safe_json_malformed_raises() -> None: with pytest.raises(json.JSONDecodeError): _safe_json("this is not json at all")