67 lines
3.1 KiB
Python
67 lines
3.1 KiB
Python
"""从损坏的 tool_call arguments 里抢救完好 JSON 的纯逻辑。
|
||
|
||
单独成模块(不 import litellm)是刻意的:core/loop.py 顶层 import litellm 在部分
|
||
开发机导入期会拉 GitHub cost map 卡死(见 memory),使 import core.loop 无法跑;而
|
||
本机制的正确性盲区(线上真垃圾前缀本机复现不出)恰恰只能靠单测兜底。把纯函数放这里,
|
||
tests 可无 litellm 依赖直接 import 校验守卫逻辑。
|
||
|
||
定层结论(见 project_deepseek_malformed_toolcall_salvage / DESIGN):畸形 arguments 是
|
||
provider 在 wire 上把别处正文字节错标成本 tool_call 的 arguments delta,粘在**开头**
|
||
(char-0 垃圾前缀),尾部真 JSON 完好并跑到串尾。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from typing import Optional
|
||
|
||
|
||
def salvage_tool_arguments(raw: str, allowed_keys: set) -> Optional[dict]:
|
||
"""从损坏的 arguments 里抢救出完好的 JSON 对象;抢不出返回 None。
|
||
|
||
左→右扫每个 `{`,试 `json.loads(raw[i:])`——第一个满足三条的即采纳:
|
||
(a) parse-to-end 成功(json.loads 要求整个后缀是合法 JSON,尾部有残渣就失败);
|
||
(b) 结果是非空 dict;
|
||
(c) 顶层 key ⊆ 该工具 schema 的参数名。
|
||
双护栏缺一不可:(a) 挡尾部被截断的半个 JSON;唯一例外是 wire 已给出至少两个
|
||
完全一致的完整参数对象、随后又粘了截断副本,此时一致副本可作为相互校验;
|
||
(c) 挡垃圾前缀里恰好自洽的旁支 JSON(如模型正文里引用的 `{...}` 片段)。
|
||
都不满足返回 None。纯函数、无副作用,便于单测(线上真前缀样本当夹具)。
|
||
"""
|
||
# 只可能出现在 '{' 处;非 '{' 直接跳过,省下大部分 json.loads 尝试。
|
||
start = 0
|
||
decoder = json.JSONDecoder()
|
||
repeated_candidates: list[dict] = []
|
||
while True:
|
||
i = raw.find("{", start)
|
||
if i < 0:
|
||
break
|
||
try:
|
||
obj = json.loads(raw[i:])
|
||
except (json.JSONDecodeError, ValueError):
|
||
# 某些 wire 抖动会形成 ``{完整参数}{完整参数}{截断副本``。
|
||
# parse-to-end 无法处理,但 raw_decode 仍能取出前面的完整对象。
|
||
# 这里只收集候选,最终必须至少两个且完全一致才可执行。
|
||
try:
|
||
partial_obj, _end = decoder.raw_decode(raw, i)
|
||
except (json.JSONDecodeError, ValueError):
|
||
start = i + 1
|
||
continue
|
||
if (
|
||
isinstance(partial_obj, dict)
|
||
and partial_obj
|
||
and set(partial_obj.keys()) <= allowed_keys
|
||
):
|
||
repeated_candidates.append(partial_obj)
|
||
start = i + 1
|
||
continue
|
||
if isinstance(obj, dict) and obj and set(obj.keys()) <= allowed_keys:
|
||
return obj
|
||
# 解析成功但不是想要的 dict(如前缀里一段自洽 JSON):继续往后找,真 JSON 可能在更后面。
|
||
start = i + 1
|
||
if (
|
||
len(repeated_candidates) >= 2
|
||
and all(obj == repeated_candidates[0] for obj in repeated_candidates[1:])
|
||
):
|
||
return repeated_candidates[0]
|
||
return None
|