Repository navigation
forward-serve: a request's tools reach the chat template — MiniCPM-V 4.7 answered a tool request in prose, now a call and a round trip (Refs #1788) - #2172
Conversation
…ction/parameter call form is read (Refs #1788) The chat route passed only chat_template_kwargs and enable_thinking to the container's template, so a template's tools block never rendered: MiniCPM-V 4.7 got 25 prompt tokens where its template renders 292 and answered in prose. Three changes: - npu::request_template_vars: tools are a template variable, in the caller's key order. - the qwen3 answer markup also reads <function=NAME><parameter=KEY>VALUE</parameter></function>, the form Qwen3.5's and MiniCPM-V 4.7's templates teach. - npu::messages_for_template: an earlier call's arguments string is handed to the template as the object. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
ready for review at d47afeb |
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
|
VERDICT: READY at Why this closes a class rather than a caseThree fixes tonight have now been the same shape: #2064 (stop set decided before the template was known), #2144 (turn end read off the template), #2151 (what It also matters for Two things the acceptance should show
On the scope line"condition 7: a tool-call round trip on the served route" is the right way to attach this to a 15-condition row: it says which condition this PR moves, which is what a lead needs to see progress without reading the diff. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at d47afeb: READY
A request's tools reaching the chat template is the kind of fix that changes an answer without changing a byte of kernel code. request_template_vars(ordered, thinking_from_body…) collects the per-request facts and passes them as extra into render_chat_template, so the model is shown the tool definitions it was actually called with. Without it the template renders a prompt with no tools and the model answers as if none were available — a failure that looks like a weak model rather than a plumbing gap.
Passing extra alongside add_generation_prompt, bos_text and eos_text puts the per-request variables in the same call as the fixed template facts, which is right: they are all inputs to one rendering, and a second path that injected tools afterwards would drift from the template's own ordering.
Same family as #2118/#2131/#2151 — four changes today on the demo path's visible behaviour (whitespace, reasoning-token accounting, thinking template order, and now tools), which is what a release row needs and what a kernel-only view would miss entirely.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at d47afeb: READY
Reviewed head d47afeb (refs #1788, condition 7). This is the second verdict on this head.
Checked
git merge-treeonto main: no conflicts. CIbuild(success) and CodeQL green.- ctest
npu_templateandnpu_answer_parserpass, built from this head. The new template arms ran in this build (no jinja renderer needed for them): "request tools: the template sees the request's tools", "… in the caller's key order", "an empty list renders no tools block", and "call arguments: the template walks the object, in the string's key order". - The parser's existing arms still pass, including the reasoning-token ones from #2131, so the new call form does not disturb the splitter they count over.
- Mutants of
app/npu_answer_parser.h, each rebuilt and run:
| mutant | arm that fails |
|---|---|
| parameter values always strings | "JSON values are values, text is a string (a date is not a number)" |
the <function=…> form switched off |
"function form: one call, named" |
| text between parameters accepted | "function form: text between parameters goes back to content" |
I restored the file (git status clean).
- The fallback is safe.
close_function_call()returns false for anything that is not exactly<function=NAME>followed by<parameter=K>V</parameter>…</function>, so a JSON-form call or a malformed block takes the existing path, as the body says.
Note, not blocking: a parameter value that parses as JSON becomes JSON, so a numeric-looking string argument (an id 12345, a zip 90210) reaches the tool as a number even when the tool's schema says "type": "string". A date or a leading-zero value stays a string, because the JSON parse fails, which the arm pins. When tools carries a schema, consulting parameters.properties[key].type would settle it.
Not re-run by me: the NPU round trip on MiniCPM-V-4.7-1B (finish tool_calls and the second turn).
Refs #1788 (condition 7: a tool-call round trip on the served route).
The fault
forward-serverenders the container's own chat template, but handed it onlychat_template_kwargsandenable_thinking. A request'stoolswent to the grammar and the answer parser and never to the template, so the model was never told what it could call.Before, on the NPU with
MiniCPM-V-4.7-1B-NPU2(main 6df0386,ONEBIT_JINJA=ON,get_weather(city), temperature 0, "What is the weather in Paris right now? Use the tool."):The reference (
tokenizer.apply_chat_template(messages, tools=tools, add_generation_prompt=True)) renders the same request as 292 tokens.What changes
npu::request_template_vars(app/npu_template.h):toolsis a template variable, in the caller's key order; an empty list is left out. The route uses it for the prompt and for the--prefix-pinprobe.qwen3answer markup also reads the call form this template (and Qwen3.5's) teaches:<tool_call><function=NAME><parameter=KEY>VALUE</parameter></function></tool_call>. With the tools rendered the model wrote exactly that, and it came back ascontentwithtool_calls null. A value is a JSON value when it is an object, array, number,true/false/null, otherwise the text itself. A block that is neither form goes back tocontentunchanged, as before.npu::messages_for_template: an earlier call'sfunction.arguments(a JSON string in the API) is handed to the template as the object. This template walkstool_call.arguments|items, so the second turn was a 400:Unknown (built-in) filter 'items' for type String.After, on the NPU (this branch at d47afeb, same pack, same request, one
npu-holdticket, serve ended with the ticket)The tools prompt is now 292 tokens, the reference's count.
Tests
ctest -R "npu_answer|npu_template": 2/2 pass. New arms:The first
function formarm is the reply the model wrote on the device.Not in this PR
17) comes back as the literal. Noted in the code and in docs/npu-chat-template.md.tool_choice: "required"still constrains the answer to the JSON call form (the existing grammar); both forms parse. Not measured on this pack.Docs: docs/npu-chat-template.md.
🤖 Generated with Claude Code