Skip to content

harness(openai): recover tool-call args corrupted by leaked chat-template delimiters#45

Merged
senamakel merged 5 commits into
tinyhumansai:mainfrom
M3gA-Mind:harness/fix-tool-call-args-4766
Jul 10, 2026
Merged

harness(openai): recover tool-call args corrupted by leaked chat-template delimiters#45
senamakel merged 5 commits into
tinyhumansai:mainfrom
M3gA-Mind:harness/fix-tool-call-args-4766

Conversation

@M3gA-Mind

Copy link
Copy Markdown
Contributor

Summary

Fixes the concrete mechanism behind the dominant sub-agent reduce-hang found in the openhuman agent-efficiency eval: a sub-agent produced a composio_execute tool call whose function.arguments were corrupted by a leaked chat-template delimiter (a trailing <tool_call|> marker), so the JSON never parsed, the tool call never resolved, and the parent's join/reduce hung — ending in an empty reply with no terminal event.

Root cause (in this crate): Some OpenAI-compatible gateways fail to strip a model's chat-template tool-call delimiters before placing the call in function.arguments. parse_tool_arguments (in src/harness/providers/openai/convert.rs, shared by both the streaming OpenAiStreamAcc::into_response reduce and the unary parse_response) then fails to parse the otherwise-valid JSON.

Change

On a parse failure, parse_tool_arguments now attempts one conservative repair before erroring:

  1. Strip known chat-template tool-call markers (<tool_call|>, </tool_call>, <|tool_call|>, and Kimi/Qwen/Hermes-style variants) and re-parse — recovers a valid call whose only corruption is a leaked delimiter.
  2. Else recover the first complete leading JSON object — handles a valid call followed by trailing marker noise or a second concatenated fragment.

Guarantees:

  • The repair runs only after the raw string has already failed serde_json::from_str, so well-formed arguments are never rewritten (covered by a test where the marker text sits inside a legitimate string value).
  • If the input is still unparseable after repair, it fails fast with the existing clear, non-retryable Model error — never a never-resolving tool call that stalls the agent loop.

Tests

Added to src/harness/providers/openai/test.rs:

  • recovers_tool_args_with_leaked_trailing_template_marker
  • recovers_tool_args_wrapped_in_tool_call_tags
  • recovers_first_call_when_fragments_are_concatenated
  • still_fails_fast_on_genuinely_malformed_tool_args (the exact stray-] blob from the eval)
  • valid_tool_args_containing_marker_substring_are_left_intact
  • sse_stream_recovers_tool_args_with_leaked_template_marker (streaming reduce path)

Behavior change

Tool calls whose arguments are corrupted only by a leaked chat-template delimiter now execute instead of failing the turn; genuinely malformed arguments still fail closed (unchanged). No public API change.

Closes tinyhumansai/openhuman#4766

…late delimiters

Some OpenAI-compatible gateways fail to strip a model's chat-template
tool-call delimiters (e.g. a trailing `<tool_call|>`) before placing the
call in `function.arguments`. The otherwise-valid JSON then fails to parse,
so the tool call is unusable and the sub-agent produces no valid call —
orphaning the parent's join/reduce (empty reply, no terminal event).

`parse_tool_arguments` (shared by the streaming `OpenAiStreamAcc::into_response`
reduce and the unary `parse_response`) now attempts one conservative repair
on a parse failure before erroring:

  1. strip known chat-template tool-call markers and re-parse; else
  2. recover the first complete leading JSON object (handles a valid call
     followed by trailing marker noise or a second concatenated fragment).

The repair runs only after the raw string has already failed to parse, so
well-formed arguments are never rewritten. If the input is still unparseable
it fails fast with the existing clear, non-retryable Model error — never a
never-resolving tool call that stalls the agent loop.

Adds unit tests for marker-stripping recovery, tag-wrapped args, concatenated
fragments, fail-fast on genuinely malformed args, a valid-args-with-marker
no-op, and the SSE streaming reduce path.

Closes tinyhumansai/openhuman#4766
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 271d80e7-a6d5-414e-865e-e948da01883e

📥 Commits

Reviewing files that changed from the base of the PR and between 95fa263 and 6f95714.

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

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

@M3gA-Mind

Copy link
Copy Markdown
Contributor Author

CI guardian: ran cargo fmt on src/harness/providers/openai/test.rs (the only cargo fmt --check diff) — fixes the Rust SDK fmt failure.

Sha256 no longer implements std::io::Write, so serde_json::to_writer into
the hasher fails to compile (E0277) under `clippy --all-targets -D warnings`.
Serialize to a Vec and feed the bytes via Digest::update instead. Pre-existing
break on tinyagents main; folded in here to unblock this PR's Rust SDK check.
@M3gA-Mind

Copy link
Copy Markdown
Contributor Author

CI guardian: Rust SDK also hit a pre-existing compile error on main (E0277: Sha256: io::Write in src/harness/prompt/mod.rs, unrelated to this PR). Folded in the minimal fix — hash serialized bytes via Digest::update instead of serde_json::to_writer — so this PR's Rust SDK check can go green (also unblocks tinyagents main).

@M3gA-Mind

Copy link
Copy Markdown
Contributor Author

CI guardian: fixed clippy::collapsible_if in convert.rs — collapsed the nested if stripped.is_some() { if let Ok(..) } into a let-chain.

@senamakel senamakel merged commit 088ad7c into tinyhumansai:main Jul 10, 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

2 participants