Repository navigation
LFM2 tool calls: typed arguments, JSON list form, per-call streaming, Pythonic grammar for tool_choice - #1265
BlivionIaG wants to merge 9 commits into
Conversation
LFM2 writes Python literals, so a model that means "123" or 4 can write the other spelling. A value the schema does not take is now read as the type it names: a string as a number, boolean or JSON value, a number as its own text for a string, 4.0 as 4 for an integer, and 0 or 1 as a boolean. A value that already fits, or that no reading fits, is kept. The unit test runs strings, integers, numbers, booleans and a union through the rewrite.
LFM2 can also write its calls as a JSON list, [{"name": "f",
"arguments": {...}}], between the same markers. A block whose list opens
with a brace is read as JSON: `parameters` stands for `arguments`, and
arguments given as a JSON string are parsed. The calls go through the
same offered-tool check and the same schema typing as the Pythonic form
and become the same DSML. A list that is not wholly offered calls stays
text, and an open block is still held back while the reply streams.
The tests cover the spellings, the typing, the refused lists, and the
byte-at-a-time stream against the whole reply.
A client may send a call's arguments as "" for a tool that takes none.
The template saw it as unparsable and rendered _invalid_arguments='' into
the prompt, so LFM2's history read now(_invalid_arguments='') instead of
now(). A blank string is now an empty mapping, as {} is.
lfm2_template_test.zig renders LFM2.5's own chat_template.jinja over a
history of one assistant message with two calls and two tool results,
with the arguments as a mapping and as a JSON string, and over no-argument
calls with a system message and the tool list.
lfm2_json is a JSON list of one call, written with `parameters` and a
string where the schema takes an integer; lfm2_typed is a Pythonic call
with a number where the schema takes a string and a string where it
takes an integer. Each runs whole, streamed, and with one call allowed,
and the request log gains the twelve requests.
The expected replies are lfm2_tool's with the arguments the typing
should give, {"limit":4} and {"query":"123","limit":4}, and the token
counts, token hash and draft counts of the longer scripts.
|
Failing test before change 3 ( With the change the test passes: Full run on head |
…ugh a made-up LFM2 template lfm2_template_test.zig and its template were not how the engine checks real templates (tools/zig/template_parity.py does). A test beside the other messages.zig tests now covers toolArguments: blank text is an empty object, bad text stays under _invalid_arguments, omitted arguments stay omitted, and the tool results of a two-call turn keep their order.
tool_choice "required" and named functions on a template that writes calls as <|tool_call_start|>[name(key=value)]<|tool_call_end|> now compile a grammar for that block: the offered names, keyword arguments typed by their schemas, one call or several in one list. Other templates keep the JSON-style structural tag.
While a reply streams, a block of LFM2 calls is rewritten as one DSML envelope per call that is complete, instead of being held until its closing tag, so the stream sends call 1 before call 2 is finished. The finished reply still becomes a single envelope, and a block that is not a valid list stays text. A call whose later siblings turn out not to be calls has already been sent; what follows it in the block is dropped from the stream.
…ll allowed
lfm2_two writes two Pythonic calls in one block, the first ending in the
first round and the second starting after it. Each runs whole, streamed,
and with one call allowed, and the request log gains the four requests.
The expected replies are two_tools' with LFM2's arguments, {"query":"ok",
"limit":4} and {"query":"no"}; the one-call replies keep the first. The
token counts, token hash and draft counts come from the fake tokenizer
applied to the script, not from the server's reply.
|
Landed on main in 8086c20..2e33e29, with your records under your authorship. Thank you, @BlivionIaG. LFM2 tool calls now:
The goldens cover whole, streamed and single-call-allowed forms, and the host suites and goldens pass. - Synthema, Ash's AI Assistant. |
LFM2 tool calls: typed arguments, JSON list form, per-call streaming, Pythonic grammar for tool_choice
Follows #1246 and #1250.
Changes
123for a string parameter becomes"123"."4"for an integer becomes4, and4.0becomes4.1or0for a boolean becomestrueorfalse.[{"name": "f", "arguments": {...}}]is read as calls.parametersis accepted forarguments.argumentscan be a JSON string."arguments": ""went into the prompt as_invalid_arguments=''.toolArgumentsinmessages.zignow reads blank text as an empty object, as it does{}.This is shared code: the prompt changes the same way for every family, not only LFM2.
tool_lfm2.zig.chat.zig,tool_stream.zigand every other format are unchanged.If a later part of the block is not a valid call, the stream drops that part.
A reply that is not streamed treats a block with an invalid part as text.
tool_choicerequiredand named use a Pythonic grammar for LFM2:tool_grammar_lfm2.zig.<|tool_call_start|>[name(key=value, ...)]<|tool_call_end|>with the offered tools.parallel_tool_callsis false.<|tool_call_start|>[tfprobe_fn(tfprobe_k=. Other families keep their grammar.name().The grammar writes the Python form only. Items inside arrays and objects are not typed by their schemas.
lfm2_typed,lfm2_jsonandlfm2_two, each whole, streamed, single and single streamed.The requests come from
freeze_cases.py. The expected responses are written by hand from existing ones.For
lfm2_typedandlfm2_json, the token counts andtoken_shacome from the candidate fake server.For
lfm2_two, they come fromfake_text.py, which reproduced the committed numbers of four existing goldens first.The goldens do not show timing. The unit tests show the byte at which each call is first sent.
There is no golden for item 5: the golden fake server has no grammar library and no LFM2 template.
Tests
Head
ddc3e53aa, on upstreammainat3d219d215. Apple M4 Pro, 48 GiB, macOS 26.5.1, Zig 0.17.0.zig build test test-golden native -Dcpu=apple_m1 -Dversion=1.1.4: 449/449 steps succeeded, exit code 0.test-golden: 322/335 equal, 0 unexpected differences. Second set: 26/26 equal.zig fmt --check: clean.python3 tools/zig/lean_check.py: 437 problems on this branch and 437 onmain.tool_lfm2.zig(typing, JSON list),tool_lfm2_stream_test.zig(byte boundaries, early send, refusals),tool_grammar_lfm2_test.zig(grammar text, replies that fit it parse throughtool_lfm2andtool_parse),messages.zig(item 3).libtfgrammar.dylib, no model): the grammar compiled, accepted conforming replies and masked bad ones.This ran on a hand-patched copy of the generated grammar. The exact committed output was not run through it.
Live check
Checkpoint
LiquidAI/LFM2.5-24B-A2B-MLX-4bit, revision667408d494bde191c2cb9dd4f4549827816b9275.Command:
tensorfold-native serve <checkpoint> --prompt-cache-gib 0(default sampling),libtfgrammar.dylibfrom a release archive beside the binary.One model process.
/v1/chat/completionsparallel_tool_calls: false;tool_choicerequired and named; no-tool prompt; tool-result round tripcontent/v1/messagestool_resultblockstool_useblocks,stop_reason: tool_use; the follow-up uses both results/v1/responsesfunction_call_outputitemsfunction_callitems; stream completes; the follow-up uses both resultsNot measured
Overlap
atc overlap --json(ATC 0.7.0,complete: false: some pull request files were unavailable):zig/src/server/root.zig. Both add test import lines.