把 GLM-5.2 接进任意 OpenAI 格式编码 agent,tool-call 不再静默错配——一层薄协议适配,harness 的 OpenAI 代码路径一行不改。
把 GLM-5.2 接到硬编码 OpenAI tool_calls 解析的编码 agent 上,tool-call 会静默错配、循环卡死;glm-toolbridge 用一行 wrap() 在中间补一层协议适配,让那条循环直接跑通。
国内开发者越来越多地把 GLM-5.2(智谱)当作 Claude Code 式 harness 或 Cursor 这类编码 agent 的后端模型——可这些 harness 默认对端说的是 OpenAI/Anthropic 的 function-call 协议。GLM-5.2 的 tool-call 在四个地方和 OpenAI 形状不一样(参数编码、并行调用、推理交织、流式拼装),于是 json.loads(arguments) 抛异常、content 非空被当成终态、并行调用对不上 id——而且大多是静默失败。像 farion1231/cc-switch 这样"在同一个 harness 后面换模型后端"的工具正把这种用法变成日常,错位也就发生在换上 GLM 的那一刻。glm-toolbridge 把这层差异收敛进一个透明代理:你的 OpenAI 代码路径一行不改,GLM 的响应回到 harness 时已经是合法的 OpenAI tool_calls。
单进程 Python 库,没有服务、没有守护进程。wrap(client) 返回一个和原 client 接口完全一致的透明代理:
- 去程
denormalize_request(tools)——把 OpenAI 形状的 tool 定义降格成 GLM-5.2 接受的请求体; - 回程
normalize_response()——把 GLM 形状的响应还原成 harness 期望的 OpenAItool_calls; - 遇到任何已知 delta 都覆盖不了的 shape,抛出有名字的显式错误(
UnsupportedProtocolShape/MalformedToolArguments/StreamAssemblyError),而不是返回半成品让 harness 三帧之后才崩——和今天的"静默错配"正好相反。
uv add glm-toolbridge # 或者:pip install glm-toolbridge冷启动到第一个可见结果,三条命令:
git clone https://github.com/SuperMarioYL/glm-toolbridge && cd glm-toolbridge
uv sync
uv run python examples/openai_harness_demo.py示例输出
================================================================
glm-toolbridge demo — same OpenAI harness, GLM-5.2 backend
================================================================
[LEFT] stock harness against raw GLM-5.2 ...
✗ tool loop broke: TypeError: the JSON object must be str, bytes or bytearray, not dict
(GLM sent arguments as a native object; json.loads chokes — the silent breakage devs hit today.)
[RIGHT] same harness, one-line wrap: client = wrap(client) ...
✓ tool loop completed: Beijing: 21 celsius, clear
(arguments normalized to a JSON string, content forced null, reasoning relocated — the harness never knew GLM was behind it.)
glm-toolbridge 给了三个抽象层级,按需取用。完整可运行示例见 examples/openai_harness_demo.py。
把现有的 OpenAI-SDK client 包一层,之后的代码一行不动:
from openai import OpenAI
from glm_toolbridge import wrap, GLM_DEFAULT_BASE_URL
client = wrap(OpenAI(base_url=GLM_DEFAULT_BASE_URL, api_key="你的智谱-key"))
resp = client.chat.completions.create(
model="glm-5.2",
messages=[{"role": "user", "content": "北京天气怎么样?"}],
tools=[...], # 你原本的 OpenAI 形状 tool 定义
)
resp.choices[0].message.tool_calls # 已是合法的 OpenAI 形状直接对 wire-shape 字典做转换,适合自己掌控请求/响应循环的 harness:
from glm_toolbridge import normalize_response, denormalize_request
glm_kwargs = denormalize_request(openai_request) # OpenAI tool 定义 → GLM 请求体
result = normalize_response(glm_raw_response) # GLM 响应 → OpenAI 形状
result.completion.tool_calls # 经 pydantic 校验的类型化视图
result.as_openai_dict() # harness 直接消费的普通 dict
result.deltas_applied # 这次实际命中了哪几条 delta流式响应先把分片列表交给 assemble_stream()(或直接把 chunk 列表传给 normalize_response())拼装成完整调用,再走上面的转换。
四条 delta 都是可执行的检测器,可单独查询:
from glm_toolbridge import DELTAS, deltas_present
for d in DELTAS:
print(d.kind.value, "—", d.summary)
deltas_present(glm_raw_response) # → [DeltaKind.ARG_ENCODING, DeltaKind.REASONING_INTERLEAVE, ...]完整的差异对照表见 docs/PROTOCOL_DELTAS.md。
同一个 OpenAI 格式 harness:左边直连 GLM-5.2,tool 循环静默卡死;右边只多了一行 wrap(),同一个 tool-call 解析成功、循环跑完。
- m1 协议审计——把 GLM-5.2 vs OpenAI 的四条 tool-call 差异连同捕获的 fixture 落进
docs/PROTOCOL_DELTAS.md,每条都有可执行检测器 - m2 双向适配器——
normalize()/denormalize()在四条差异的 fixture 上通过 roundtrip 测试 - m3 drop-in 包装器——
wrap()透明适配 OpenAI-SDK client,examples/跑通"未接入失败 / 接入成功" - 覆盖更多 GLM-5.2 tool-call 边界场景(按真实 issue 反馈补 delta)
- Anthropic Messages 格式适配(当前仅 OpenAI
tool_calls) - 视需求决定是否扩展到其他国产模型协议——深度优先于广度
不在 v0.1 范围内:Web UI / dashboard、其他模型(Qwen / Kimi / DeepSeek / 豆包 / MiniMax)的适配、自带的编码 agent、托管服务 / 计费、微调。
MIT 许可证,详见 LICENSE。欢迎提交 issue 或 PR——尤其是你撞见了某个当前 delta 没覆盖的 GLM-5.2 tool-call 形状,请把那段响应贴进 issue,我们会补一条 delta。
MIT © 2026 SuperMarioYL
