Skip to content

serve: a <tool_call> named in prose before the real call is content, not a malformed call - #700

Closed
T-Crypt wants to merge 1 commit into
Niko1221:mainfrom
T-Crypt:fix/tool-call-tag-in-prose
Closed

T-Crypt wants to merge 1 commit into
Niko1221:mainfrom
T-Crypt:fix/tool-call-tag-in-prose

Conversation

@T-Crypt

@T-Crypt T-Crypt commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

What

OutputParser.feed treated every <tool_call> in the content as the start of a call. A reply that names the tag in its prose before making the real call, for example

I will use the `<tool_call>` format now.
<tool_call>
<function=write>
...

took everything after the first tag as the call body, and parse_tool_call raised ValueError("malformed tool call: ..."), which ended the request.

A call is the tag followed (after whitespace) by <function=, which is how serve/chat_template.jinja writes it. With this change, a <tool_call> followed by anything else is content. While streaming, the tag is held until its follower arrives (the same way a partial tag is held), so streamed and whole output give the same events. A tag at the very end of the output is flushed as content by finish(), as before.

How it was found

I was porting tolerant tool-call recovery from a Qwen engine (ninfer's --tolerant-tool-calls) and checked its three recovery cases against Strata's parser:

case before
a value that quotes </parameter> / <tool_call> handled (#210)
prose after the calls handled
<tool_call> named in prose before the real call ValueError, request ends

Only the third needed a change.

Tests

  • New ToolCallTagInProse in serve/test_server.py: three prose forms (backticks, bare, two tags), stream_tools on and off, steps 1 / 7 / whole; plus a tag alone at the end of the output.
  • On main the new class fails (3 failures, 18 errors across subtests); with the change it passes.
  • python -m unittest serve.test_server: 120 tests OK (Linux, Python 3.14).
  • Not measured: a live model producing this output. The case is from ninfer's tool-call tests and agentic sessions, not from a Strata log.

…not a malformed call

A reply such as "I will use the `<tool_call>` format now." followed by the call raised
ValueError("malformed tool call") in OutputParser.feed and ended the request: everything after the
first <tool_call> was taken as the call body. A call is the tag followed (after whitespace) by
<function=, as the chat template writes it; the tag with anything else after it is now content, and
it is held while streaming until its follower arrives, so streamed and whole output agree.

Found while porting tolerant tool-call recovery from a Qwen engine (ninfer --tolerant-tool-calls):
of its three recovery cases, Strata already handled values that quote the markup (Niko1221#210) and prose
after the calls; this was the one that failed. Tests: ToolCallTagInProse (fails on main with 21
subtest errors, passes here); python -m unittest serve.test_server: 120 tests OK.
@Niko1221

Niko1221 commented Oct 6, 2026

Copy link
Copy Markdown
Owner

Merged for 0.1.40: a <tool_call> named in prose before the real call is content, not a malformed call.

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