Skip to content

fix(llm): recognize raw <tool_call> wrappers as function calls - #4608

Open
sambai-dev wants to merge 2 commits into
OpenHands:mainfrom
sambai-dev:fix/normalize-tool-call-wrappers
Open

fix(llm): recognize raw <tool_call> wrappers as function calls#4608
sambai-dev wants to merge 2 commits into
OpenHands:mainfrom
sambai-dev:fix/normalize-tool-call-wrappers

Conversation

@sambai-dev

@sambai-dev sambai-dev commented Aug 24, 2026

Copy link
Copy Markdown

HUMAN:
I have reviewed the approach, the diff, and the validation results, and I approve opening this for maintainer review. The change was prepared with AI assistance (Hermes) under my direction; details and full disclosure below.

AGENT:

Why

Models served behind a plain OpenAI-compatible endpoint (observed with Kimi K3 via the Synthetic provider) emit their native <tool_call>NAME ... wrapper instead of this SDK's <function=NAME> dialect. The non-native function-call converter only strips those wrapper tags when a well-formed <function= call is already present, so the raw syntax leaks into the assistant text and the tool never fires. Reproduction and raw model output are in #4540.

Summary

  • Add _TOOL_CALL_WRAPPER_PATTERN to _preprocess_model_output() so a raw <tool_call>NAME followed by a <parameter=...> line is rewritten into the canonical <function=NAME> form, and the first </tool_call> becomes </function>.
  • Two guards keep the rewrite safe: it fires only when no <function= exists anywhere in the content, and only on the exact malformed shape, so prose merely mentioning the tag is never converted. Truncated calls are completed by the existing _fix_stopword closer-append path.
  • Three regression tests: wrapper normalized (with leading prose), truncated wrapper still parses, plain-text mention stays untouched.

Issue Number

Fixes #4540

How to Test

From a local checkout of this branch:

uv run pytest tests/sdk/llm/test_llm_fncall_converter.py -q

Expected: 33 passed, including the three new tool_call_wrapper tests. On unmodified main the two normalization tests fail with StopIteration because no tool call is produced.

Full module: uv run pytest tests/sdk/llm/ -q gives 981 passed and 1 failure (test_llm_profile_store.py::test_default_provider_store_is_sibling_of_base_dir) which fails identically on unmodified main on this Windows machine (verified via git stash re-run); pre-existing environment issue, untouched here. Lint and types: uv run ruff format && uv run ruff check and uv run pyright on both changed files are clean.

Video/Screenshots

Not applicable: library-level change with no UI. Command output evidence is included under How to Test.

Risk

This changes tool-call parsing behavior, so per the repository review guide it belongs in eval-risk territory: a COMMENT review with lightweight evals before approval seems right rather than a fast approve. The guards keep the blast radius narrow, and the new tests cover both directions. No public API or persisted-event shapes change.

AI assistance

AI-assisted: investigation and implementation were prepared with Hermes; all reported checks were executed against this branch. The approach follows the analysis posted in #4540 by @VascoSch92.

@all-hands-bot

Copy link
Copy Markdown
Collaborator

👋 This PR needs a couple of things fixed before OpenHands can review it:

  • the PR description's HUMAN: section needs at least 20 characters describing what you tested, not just the template placeholder

Push an update once this is addressed and this check re-runs automatically.

This is an automated check - no AI was used to generate this comment.

@all-hands-bot

all-hands-bot commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

🚦 CI is currently failing on this PR's latest commit.

Please fix the failing checks before OpenHands reviews it - this is re-checked automatically once you push a new commit. (A maintainer can also request @all-hands-bot as a reviewer to have it reviewed regardless of CI status.)

This is an automated check - no AI was used to generate this comment.

@sambai-dev

Copy link
Copy Markdown
Author

Follow-up 8ba7e1cb is pushed after an independent adversarial review. It fixes the case where a literal </tool_call> inside a parameter was mistaken for the outer wrapper closer; the closer rewrite is now end-anchored and the regression is covered.

Validation on the current head:

  • converter suite: 34 passed
  • changed-file pre-commit checks: all passed (format, lint, pycodestyle, pyright, import rules, tool registration)
  • broader LLM suite: 982 passed, with the same unrelated Windows path assertion documented in the PR

The current description check confirms the HUMAN/template requirements pass; its only remaining error is that #4540 still lacks ready-for-dev. I have already provided the issue author with the exact readiness text. As an external contributor I also cannot add the repository's integration-test label, so a maintainer will need to add it and approve the fork workflows when appropriate.

@sambai-dev
sambai-dev marked this pull request as draft August 24, 2026 03:48
@sambai-dev
sambai-dev marked this pull request as ready for review August 24, 2026 03:52
Some models served behind plain OpenAI-compatible endpoints (Kimi K3 via
the Synthetic provider) emit their native <tool_call>NAME wrapper instead
of our <function=NAME> dialect. The non-native function-call converter
only stripped those tags when a well-formed call was already present, so
the raw syntax leaked into assistant text and the tool never fired.

Rewrite the malformed shape (<tool_call>NAME followed by a <parameter=
line) into <function=NAME>. Two guards keep this safe: it only fires when
no <function= is present anywhere in the content, and only on the exact
malformed shape, so prose merely mentioning the tag is never converted.

Fixes OpenHands#4540
@sambai-dev
sambai-dev force-pushed the fix/normalize-tool-call-wrappers branch from 8ba7e1c to 4f3b647 Compare August 24, 2026 07:46
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.

[Bug]: Tool calls return incorrect/weird syntax with Synthetic Provider

2 participants