Repository navigation
server: parse LFM2's Pythonic tool calls - #1250
outcastofmusic wants to merge 1 commit into
Conversation
LFM2.5-24B-A2B (lfm2_moe) writes its calls as a Python list between <|tool_call_start|> and <|tool_call_end|>. No parser read that markup, so the server returned it as message content with no tool_calls, and a streamed reply showed it as prose. tool_lfm2.zig rewrites each complete block of offered calls as the DSML block the parser and the streamer already read, as tool_minimax.zig does for MiniMax-M3. A scanner that knows strings and brackets splits the calls and their arguments. Positional arguments take the schema's property order, and each value reads as JSON, then as a Python literal. A block that does not parse, or that names a tool the request did not offer, stays text. chat.zig rewrites the answer after the think split, in the streamed and the whole reply, so the reasoning's byte offsets hold and a call the reasoning mentions is not made. While a reply streams, an open block is held back: the streamer reads on from where it stopped, so it must first meet the block as DSML. reply_text.calls gains the pair, so the hider covers the block as LFM2 writes it. The goldens add lfm2_tool whole, streamed, and with one call allowed.
…, a finished reply's unclosed LFM2 block stays text, comments within the lean limits
|
Landed on main as 7b4a62c, thank you @outcastofmusic. LFM2's Pythonic tool calls are parsed into OpenAI - Synthema, Ash's AI Assistant. |
Real-model validation of the LFM2 tool-call parserI validated the merged Environment
Build and tests
Served requests (
No unexpected behavior or failures were observed. Caveat: the source build does not include This is validation only; I’m not proposing code changes. |
What this changes
LFM2.5-24B-A2B's tool calls (
<|tool_call_start|>[name(key=value)]<|tool_call_end|>) now come back astool_calls, whole and streamed. Before, the server returned them ascontent. Fixes #1246.The new
tool_lfm2.zigrewrites each complete block of offered calls as the DSML block that the parser and the streamer already read, astool_minimax.zigin #1235 does for MiniMax. Positional arguments take the schema's property order. Each value reads as JSON, then as a Python literal. A block that does not parse, or that names a tool the request did not offer, stays as text.Two differences from #1235's hook:
chat.zigrewrites the answer after the think split. The split works on byte offsets, which a rewrite of the whole text would shift. A call the reasoning only mentions is also left alone.A value that mixes Python's
Truewith JSON'snullinside one container does not parse, becausepythonLiteralreads only Python's spelling inside containers. That block stays as text.Overlaps: #1235 also adds an import, a hook in
chat.zig, and areply_text.callsentry, so the two conflict on neighbouring lines. #1243 sizes aleadstable bycalls.len, which needs one more&.{}entry for this pair.Receipt
mainat 6244c62 plus this commit, Zig 0.17.0, Linux aarch64 with a GB10. No checkpoint: the change is in server text handling.tools/bench_concurrent.py --alone --serialortoken_shaagainst"draft": false: n/a. Tokens do not change; only how the reply text becomes content and calls.tools/bench_openai.py: n/a. The rewrite runs on reply text after decode, and returns at once when the reply holds no opener.zig build test-cuda-host: 210/210 steps, 1343/1346 tests pass, 3 skipped.zig build test-golden: 310/323 equal, 0 unexpected differences; the 13 are on the existing known list. The four newlfm2_toolgoldens (whole, stream, single, single-stream) fail with thechat.zighook removed. The 5 tests intool_lfm2.zigcover positional and keyword arguments, nested values, two calls, an unknown tool, a block cut off mid-stream, and streamed calls that equal the whole reply's calls.lfm2_moeon Metal, and I have no Mac.Checklist
"draft": false, a resumed prompt equals a fresh one, and concurrent equals solo.SUMMARYline is in the receipt. (No kernel changes.)