JacobLinCool Codex commited on
Commit
bfb16e5
·
verified ·
1 Parent(s): ffb673b

fix: parse qwen thinking json output

Browse files

Co-authored-by: Codex <noreply@openai.com>

Files changed (2) hide show
  1. model_runtime.py +21 -6
  2. tests/test_model_runtime.py +10 -0
model_runtime.py CHANGED
@@ -252,14 +252,29 @@ def _loads_lenient(content: str) -> dict[str, Any]:
252
  try:
253
  parsed: Any = json.loads(text)
254
  except json.JSONDecodeError:
255
- start, end = text.find("{"), text.rfind("}")
256
- if start == -1 or end == -1 or end <= start:
257
  raise ValueError("Model response was not valid JSON.")
258
- try:
259
- parsed = json.loads(text[start : end + 1])
260
- except json.JSONDecodeError as exc:
261
- raise ValueError("Model response was not valid JSON.") from exc
262
 
263
  if not isinstance(parsed, dict):
264
  raise ValueError("Model response was not a JSON object.")
265
  return parsed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  try:
253
  parsed: Any = json.loads(text)
254
  except json.JSONDecodeError:
255
+ candidates = list(_json_object_candidates(text))
256
+ if not candidates:
257
  raise ValueError("Model response was not valid JSON.")
258
+ parsed = candidates[-1]
 
 
 
259
 
260
  if not isinstance(parsed, dict):
261
  raise ValueError("Model response was not a JSON object.")
262
  return parsed
263
+
264
+
265
+ def _json_object_candidates(text: str) -> list[dict[str, Any]]:
266
+ decoder = json.JSONDecoder()
267
+ candidates: list[dict[str, Any]] = []
268
+ cursor = 0
269
+ while True:
270
+ start = text.find("{", cursor)
271
+ if start == -1:
272
+ return candidates
273
+ try:
274
+ parsed, consumed = decoder.raw_decode(text[start:])
275
+ except json.JSONDecodeError:
276
+ cursor = start + 1
277
+ continue
278
+ if isinstance(parsed, dict):
279
+ candidates.append(parsed)
280
+ cursor = start + max(consumed, 1)
tests/test_model_runtime.py CHANGED
@@ -74,6 +74,16 @@ class ModelRuntimeTests(unittest.TestCase):
74
 
75
  self.assertEqual(memo["outcome_audit_memo"], MEMO_JSON["outcome_audit_memo"])
76
 
 
 
 
 
 
 
 
 
 
 
77
  def test_run_model_assist_uses_selected_model(self) -> None:
78
  result, narrative = analyze_trace_file(Path("examples/sample_trace_redacted.jsonl"))
79
  generate = RecordingGenerator()
 
74
 
75
  self.assertEqual(memo["outcome_audit_memo"], MEMO_JSON["outcome_audit_memo"])
76
 
77
+ def test_parse_model_json_uses_final_object_after_thinking_braces(self) -> None:
78
+ raw = (
79
+ "<think>Draft {not json} and a scratch object "
80
+ '{"draft": "ignore this"} before the final answer.</think>\n'
81
+ + json.dumps(MEMO_JSON)
82
+ )
83
+ memo = parse_model_json(raw)
84
+
85
+ self.assertEqual(memo["executive_memo"], MEMO_JSON["executive_memo"])
86
+
87
  def test_run_model_assist_uses_selected_model(self) -> None:
88
  result, narrative = analyze_trace_file(Path("examples/sample_trace_redacted.jsonl"))
89
  generate = RecordingGenerator()