fix(llm): P2-3 双防御——robust_json_parse 入口 isinstance(text,str) 统一 JSONParseError(非 str 曾 TypeError 裸逃逸穿透 502 契约变原生 500)+client 空回复 content:null 降级走 strict 重试一次(两轮仍空才 LLMError);null content 双调用+非 str 五形态测试 [vps] [no-doc]
This commit is contained in:
@@ -64,13 +64,12 @@ class LLMClient:
|
||||
await self._sleep(_BACKOFF_SECONDS[min(attempts - 1, 1)])
|
||||
continue
|
||||
if status == 200:
|
||||
content = self._content(text)
|
||||
usage = json.loads(text).get("usage") or {}
|
||||
logger.info("llm tokens prompt=%s completion=%s",
|
||||
usage.get("prompt_tokens"),
|
||||
usage.get("completion_tokens"))
|
||||
try:
|
||||
return robust_json_parse(content), usage
|
||||
return robust_json_parse(self._content(text)), usage
|
||||
except JSONParseError:
|
||||
return await self._strict_retry(messages, payload)
|
||||
if status in (401, 403, 404):
|
||||
@@ -121,6 +120,11 @@ class LLMClient:
|
||||
@staticmethod
|
||||
def _content(text: str) -> str:
|
||||
try:
|
||||
return json.loads(text)["choices"][0]["message"]["content"]
|
||||
content = json.loads(text)["choices"][0]["message"]["content"]
|
||||
except (KeyError, IndexError, TypeError, ValueError) as e:
|
||||
raise LLMError(f"LLM 响应缺 choices/message: {e}") from e
|
||||
if content is None:
|
||||
# P2-3: 兼容端点空回复 content:null 不罕见——按空回复降级走
|
||||
# strict 重试(JSONParseError 通道),两轮仍空才 LLMError=502 契约
|
||||
raise JSONParseError("content 为 null(空回复)")
|
||||
return content
|
||||
|
||||
@@ -53,6 +53,9 @@ def _balanced_braces(text: str) -> str | None:
|
||||
|
||||
def robust_json_parse(text: str) -> dict:
|
||||
"""五级降级:直读→围栏→配平截取→反斜杠修复→宽松(尾逗号/尾垃圾).全败 raise."""
|
||||
if not isinstance(text, str): # P2-3: 非 str(含 None)统一 JSONParseError,
|
||||
raise JSONParseError( # 曾 TypeError 裸逃逸穿透 502 契约
|
||||
f"输入须为 str: {type(text).__name__}")
|
||||
candidates: list[str] = [text]
|
||||
m = _FENCE.search(text)
|
||||
if m:
|
||||
|
||||
Reference in New Issue
Block a user