Repository navigation
Two defects left after 7c79515: strict-gateway 400 from missing required in tools_schema, and DSML markers inside tool_call arguments #846
Description
Summary
Two defects that survive 7c79515 (verified on b70079d, Windows 11, Python 3.11). Both are independent of the content-channel leak tracked in #837.
A. Strict gateways reject every request: Invalid schema for function 'code_run': null is not of type "array"
Observed (real log, agent could not start):
!!!Error: HTTP 400: {"error":{"message":"Invalid schema for function 'code_run': null is not of type \"array\" ...
"type":"invalid_request_error","param":"","code":null},"request_id":"..."}
Root cause: assets/tools_schema.json ships 9 tools and none of them declares required (verified: 0/9). llmcore._prepare_oai_tools() forwards the schema untouched for both api_mode="chat_completions" and "responses". Strict OpenAI-compatible validators (DeepSeek-style) do not treat a missing required as optional — their validator sees null and rejects the whole request with HTTP 400 before any model call, so the agent never runs.
Proposed fix (normalize, deep copy, never mutate the shared schema):
def _normalize_tool_parameters(fn):
fn = dict(fn)
params = fn.get("parameters")
if not isinstance(params, dict):
params = {"type": "object", "properties": {}}
else:
params = dict(params)
props = params.get("properties")
params["properties"] = dict(props) if isinstance(props, dict) else {}
if not isinstance(params.get("required"), list):
params["required"] = []
params.setdefault("type", "object")
fn["parameters"] = params
return fnApply it to every tool inside _prepare_oai_tools (both API modes). After the change: all 9 tools serialize with "required": [], original TOOLS_SCHEMA stays unmodified, full test suite unchanged.
B. DSML markers also leak into tool_call.arguments (not just content)
agent_loop.py parses arguments with no sanitizing:
else: tool_calls = [{'tool_name': tc.function.name, 'args': json.loads(tc.function.arguments), 'id': tc.id}
for tc in response.tool_calls]Two real failure shapes:
- Marker appended inside a string value — the call "succeeds" but the tool receives garbage:
{"script": "print(1)</||DSML|| parameter>"}→ executed as-is / SyntaxError. - Marker truncates the JSON —
json.loadsraises and the whole turn aborts.
Proposed fix: run the same _strip_dsml_markers() on arguments before json.loads, and degrade a still-unparseable payload to no_tool (retry) instead of raising:
if 'DSML' in _raw_args:
_raw_args = _strip_dsml_markers(_raw_args)
try:
_args = json.loads(_raw_args)
except Exception:
tool_calls = [{'tool_name': 'no_tool', 'args': {}}] # ask the model for a clean callC. Why 7c79515 does not cover these
The commit only inspects the last 50 chars of content for a closing </|DSML|...> marker. Neither A nor B touches content; and even in the content channel, </||DSML|| parameter> (ASCII pipes), leading/opening tags, and markers in the middle of a longer answer still pass through.
Verification of the combined patch
- 24 new unit tests (marker variants
|/||/|/‖, cross-chunk splits, text-channel<code_run>recovery, argument sanitizing): 24 passed - Full
frontends/tests: 291 passed, 8 failed — all 8 pre-existing on this machine (5x missing tauri config files, 2x Windows symlink, 1x_model_lockmissing on the test FakeAgent), unrelated to this change - E2E with a mocked LLM response: polluted arguments are stripped and executed correctly; truncated arguments fall back to
no_tooland retry instead of crashing
Related: #837 (content-channel leak), #838 (robust stripping + text-channel recovery, still unmerged), #836, #805.
Activity
lunxian8 commentedon Oct 22, 2026
Reproduced the
null is not of type "array"400 exactly, so here is the minimal condition (this is not universal — it depends on the gateway and the client fingerprint).Trigger:
agentrouter.org+User-Agent: codex_cli_rs/...(what GA sends by default) + the rawassets/tools_schema.json(norequired).Matrix (same payload, both variants):
requiredrequired: []codex_cli_rsclaude-cli/2.1.280So: several gateways are tolerant, but the strict path rejects it, and it fails before the model ever sees the request. Note the client fingerprint matters too —
agentrouterreturnsunauthorized client detectedfor a plainpython-urllibUA even with a valid key, so this is easy to miss in a quickcurltest.Bare
curlequivalent:Also relevant:
assets/tools_schema.jsonhas 9 tools and zerorequiredkeys;assets/tools_schema_cn.jsonlikewise. Addingrequired: []is schema-valid and changes nothing semantically, so it is safe to always emit it.lunxian8 commentedon Oct 22, 2026
One practical note while testing the fix for A: on the desktop build, patched code will appear not to work, because closing/reopening the window does not restart the bridge process — the running kernel keeps the
llmcore.pyit imported at startup. I hit exactly this: patch applied at 08:46, kernel still running since two days earlier, same 400 kept coming. Verified onb70079d:POST /services/bridge/exitexists infrontends/desktop_bridge.pybut has no callers in the repo.So when validating this fix, kill the
desktop_bridge.pytree and relaunch it (taskkill /F /T /PID <bridge>thenpythonw frontends\desktop_bridge.py), otherwise you are testing the old code. Filed separately as #847.lunxian8 commentedon Oct 22, 2026
另有一类同源的网关兼容崩溃,已单独记录:#848 —— 网关会发
data: null心跳帧,_parse_openai_sse里json.loads得到None后直接.get()抛AttributeError: 'NoneType' object has no attribute 'get',被_stream_with_retry吞成[!!! 流异常中断 ... !!!],随后进入Incomplete response. Regenerate死循环。已在本地加if not isinstance(evt, dict): continue守卫 + traceback 落盘验证通过(未提交,等官方合并)。fix: 伪工具调用救回机制全面加固(误执行 0 / 漏检 0)