This commit is contained in:
2026-09-18 15:21:47 +08:00
parent 0d8c34a2c9
commit aa27d35a47
55 changed files with 2842 additions and 471 deletions
+53
View File
@@ -51,6 +51,9 @@ class LLMClient:
response = await self.client.chat.completions.create(**kwargs)
await self._track(response.usage)
content = response.choices[0].message.content or ""
if not content:
finish = getattr(response.choices[0], 'finish_reason', 'unknown')
logger.warning("LLM returned empty content (finish_reason=%s)", finish)
return content.strip()
except Exception as e:
if attempt == retries - 1:
@@ -83,16 +86,66 @@ class LLMClient:
def parse_json(text: str) -> dict:
text = text.strip()
# Strip modern LLM "thinking" blocks (Anthropic, DeepSeek, etc.) that may
# appear either before or around the JSON payload.
text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
# Markdown code fence: ```json ... ``` or ``` ... ```
if text.startswith("```"):
match = re.search(r"```(?:json)?\s*\n?(.*?)```", text, re.DOTALL)
if match:
text = match.group(1).strip()
# Try the whole text first.
try:
return json.loads(text)
except json.JSONDecodeError:
pass
# Otherwise find the first balanced { ... } or [ ... ] substring that parses.
def _balanced_at(start: int) -> int | None:
open_ch, other_ch = ("{", "}") if text[start] == "{" else (("[", "]") if text[start] == "[" else (None, None))
if open_ch is None:
return None
depth = 0
in_str = False
esc = False
for j in range(start, len(text)):
c = text[j]
if in_str:
if esc:
esc = False
elif c == "\\":
esc = True
elif c == '"':
in_str = False
continue
if c == '"':
in_str = True
elif c == open_ch:
depth += 1
elif c == other_ch:
depth -= 1
if depth == 0:
return j
return None
for opener in ("{", "["):
i = 0
while True:
idx = text.find(opener, i)
if idx == -1:
break
end = _balanced_at(idx)
if end is not None:
try:
return json.loads(text[idx : end + 1])
except json.JSONDecodeError:
pass
i = idx + 1
# Last-ditch: trim from the end while we can find a parseable prefix.
for i in range(len(text) - 1, -1, -1):
if text[i] in "]}":
try: