Skip to content

fix(openai): recover relaxed/malformed tool-call argument JSON#68

Merged
senamakel merged 2 commits into
tinyhumansai:mainfrom
YellowSnnowmann:fix/relaxed-json-tool-call-args
Jul 23, 2026
Merged

fix(openai): recover relaxed/malformed tool-call argument JSON#68
senamakel merged 2 commits into
tinyhumansai:mainfrom
YellowSnnowmann:fix/relaxed-json-tool-call-args

Conversation

@YellowSnnowmann

@YellowSnnowmann YellowSnnowmann commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Add a third recovery strategy to recover_tool_arguments (new relaxed_json module) that repairs the relaxed / malformed JSON some models emit for tool-call arguments, so an otherwise-executable call is no longer marked invalid and 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:

  • unquoted object keys{tool:"X",arguments:{guild_id:"Y"}}
  • redundant wrapping braces{{tool:"X",arguments:{…}}}, escalating ({{{…}}}, {{{{…}}}}) as the model retries a bounced call
  • leaked chat-template quote tokens[<|">discord<|">] in place of ["discord"] (seen with Kimi-family models served via GMI)

serde_json::from_str rejects all of these, so the call resolves as ToolCall::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_json module wired as Strategy 3 in recover_tool_arguments. Conservative, meaning-preserving repairs, composed and retried at each brace depth:

  1. substitute leaked quote tokens (<|"> / <|"|>) back to a literal "
  2. peel a redundant outer brace layer that wraps exactly one object ({{…}}{…})
  3. quote bare identifier keys in object position — string- and array-aware, so string contents and array/value positions are never rewritten

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

  • No public API change (relaxed_json is a private module; recover_tool_arguments keeps its signature).
  • No behavior change on valid input — the new path is recovery-only, gated behind an already-failed strict parse, and re-validates its output before accepting it.

Tests

  • 19 unit tests for the repair: each malformed form (unquoted keys, {{…}}/{{{{…}}}}, leaked quote tokens, reordered keys), plus the guards — keyless-nested rejected, non-object scalar rejected, valid-input pass-through, brace-inside-string preserved.
  • 3 integration tests through 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 failed
  • cargo clippy --all-targets -- -D warningsclean
  • cargo fmt --checkclean

Summary by CodeRabbit

  • Bug Fixes
    • Improved recovery of tool-call arguments when JSON contains relaxed formatting, missing key quotes, or redundant object wrappers.
    • Correctly handles leaked quote-token placeholders in tool-call payloads.
    • Preserves invalid status for unsupported or unrecoverable argument formats instead of fabricating structured data.
    • Added validation coverage for recovered, normalized, and rejected tool-call arguments.

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.
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 62b33849-cbc2-4e65-8a4f-5114017b7510

📥 Commits

Reviewing files that changed from the base of the PR and between 2583fcc and 9a540e6.

📒 Files selected for processing (4)
  • src/harness/providers/openai/convert.rs
  • src/harness/providers/openai/mod.rs
  • src/harness/providers/openai/relaxed_json.rs
  • src/harness/providers/openai/test.rs

📝 Walkthrough

Walkthrough

OpenAI 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.

Changes

OpenAI tool-argument recovery

Layer / File(s) Summary
Relaxed object repair implementation
src/harness/providers/openai/relaxed_json.rs, src/harness/providers/openai/mod.rs
Adds conservative normalization, bare-key quoting, redundant-brace peeling, strict object parsing, and focused unit tests.
Recovery strategy integration
src/harness/providers/openai/convert.rs
Adds the relaxed-object fallback as the third tool-argument recovery strategy and documents its object-only behavior.
Provider parsing validation
src/harness/providers/openai/test.rs
Tests recovery of relaxed and token-normalized arguments, plus rejection of keyless nested objects.

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
Loading

Suggested reviewers: senamakel, m3ga-mind

Poem

A rabbit found braces tangled tight,
And keys with no quotes in sight.
It peeled, patched, and parsed with care,
Rejected nonsense hiding there.
“Valid tools now hop just right!”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: recovering relaxed or malformed OpenAI tool-call argument JSON.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@senamakel
senamakel merged commit 7a04352 into tinyhumansai:main Jul 23, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants