fix(openai): recover relaxed/malformed tool-call argument JSON#68
Conversation
Some models, and gateways that fail to detokenize a tool-call template cleanly,
place non-strict JSON in function.arguments: unquoted object keys, redundant
wrapping braces ({{...}}, escalating as the model retries a bounced call), and
leaked chat-template quote tokens ([<|">discord<|">] in place of ["discord"]).
serde_json rejects all of these, so the call resolves as ToolCall::invalid and is
fed back to the model, which repairs it by adding another brace -- an infinite
retry that exhausts the step budget without ever executing the tool. Only
zero-argument calls survive.
Add a third strategy to recover_tool_arguments (new relaxed_json module):
substitute leaked quote tokens back to a quote, peel redundant wrapping braces,
and quote bare identifier keys (string- and array-aware). Accept only a
strict-parseable object. Runs only after strict parsing has already failed, so a
well-formed argument object is never rewritten; no public API change.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughOpenAI tool-call argument recovery now repairs selected relaxed JSON inputs, including unquoted keys, redundant braces, and leaked quote tokens, while rejecting unsupported shapes. The provider wiring and unit tests cover successful recovery and invalid-input handling. ChangesOpenAI tool-argument recovery
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant parse_response
participant recover_tool_arguments
participant recover_relaxed_object
participant serde_json
parse_response->>recover_tool_arguments: tool-call argument text
recover_tool_arguments->>recover_relaxed_object: malformed candidate
recover_relaxed_object->>serde_json: repaired JSON object
serde_json-->>recover_relaxed_object: parsed object or failure
recover_relaxed_object-->>recover_tool_arguments: recovered object or None
recover_tool_arguments-->>parse_response: valid or invalid tool call
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
Summary
Add a third recovery strategy to
recover_tool_arguments(newrelaxed_jsonmodule) that repairs the relaxed / malformed JSON some models emit for tool-call arguments, so an otherwise-executable call is no longer markedinvalidand bounced into an infinite retry loop.Problem
Some models — and gateways that fail to detokenize a model's tool-call template cleanly — place non-strict JSON in
function.arguments:{tool:"X",arguments:{guild_id:"Y"}}{{tool:"X",arguments:{…}}}, escalating ({{{…}}},{{{{…}}}}) as the model retries a bounced call[<|">discord<|">]in place of["discord"](seen with Kimi-family models served via GMI)serde_json::from_strrejects all of these, so the call resolves asToolCall::invalid, is fed back to the model, and the model "repairs" it by adding another brace — an infinite retry that exhausts the step budget without ever executing the tool. Only zero-argument calls ({}) survive, because{}is valid strict JSON.Solution
New
relaxed_jsonmodule wired as Strategy 3 inrecover_tool_arguments. Conservative, meaning-preserving repairs, composed and retried at each brace depth:<|">/<|"|>) back to a literal"{{…}}→{…})The result is accepted only when it strict-parses to a JSON object, so a scalar scraped from noise can never masquerade as arguments. It runs only after strict parsing has already failed (the same invariant as the existing marker-stripping strategy), so a well-formed argument object is never reached or rewritten.
Behavior / API impact
relaxed_jsonis a private module;recover_tool_argumentskeeps its signature).Tests
{{…}}/{{{{…}}}}, leaked quote tokens, reordered keys), plus the guards — keyless-nested rejected, non-object scalar rejected, valid-input pass-through, brace-inside-string preserved.parse_response(structured-args path), including the exact combined{arguments:{guild_id:<|">…<|">},tool:<|">…<|">}form.Commands run locally
cargo test --lib harness::providers::openai::— 133 passed, 0 failedcargo clippy --all-targets -- -D warnings— cleancargo fmt --check— cleanSummary by CodeRabbit