++
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user