Repository navigation
fix: detect template caps as llama.cpp does and route LFM2.5 tool lists - #721
Merged
Merged
Conversation
Contributor
|
Chat app preview removed for |
This was referenced Sep 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #716
Closes #720
Detects template capabilities the way llama.cpp
7fe450e19does, gives each template the content form llama.cpp gives it, and routes LFM2.5 tool lists to the LFM2 format.JinjaAnalyzerinferred string/typed content caps from the template's structure, and got SmolVLM, TranslateGemma 2B (typed-only), Ministral 3 and LFM2.5-8B (string-only) wrong. llama.cpp instead renders fixed probe conversations and reads which input values the template used (jinja::caps_get).JinjaUsageProberewrites the parsed template so each identifier, member access, filter, test andforiterable passes through a recording function, then renders it with dinja. As in llama.cpp, aforloop over a string throws.JinjaAnalyzerruns llama.cpp's probes on it for system role, tools, tool calls, parallel calls, string/typed content and object arguments.supportsThinkingstill comes from thinking markers.*argsparameters, so the written source parses to the same AST. A test writes, re-parses and compares every fixture template plus generated literal and signature forms.LegacyJinjaAnalyzer, base'sJinjaAnalyzerrenamed), never default caps.messages_inp_normalizerandto_json_oaicompatbuild it:<__media__>marker inside the text. Only templates with both caps get media as typed parts.[{'type': 'text', 'text': 'sunny'}]after an image.<|tool_list_start|>and<|tool_list_end|>(LFM2), orList of tools: [without<|tool_list_start|>(LFM2.5). llamadart also required<|tool_call_start|>, so LFM2.5-1.2B-Instruct and -Thinking fell to the generic format and got a stray "Respond in JSON format" instruction.Lfm2Handlerkeeps passing each tool flat (name,description,parameters), as the LFM2 and LFM2.5 model cards show. This deliberately differs from llama-server, which passes the OpenAI shape (type,function). The LFM2.5 parity case and the differential ignore only that shape and compare the rest exactly.Production-readiness scope
User-facing scope: SmolVLM prompts keep the message text; Ministral 3 and TranslateGemma 2B render an image followed by a reasoning-only turn; Kimi-K2 tool results after an image are plain text; LFM2.5-1.2B tool prompts use the LFM2 format.
Supported platforms/paths: All. The change is in the shared Dart template path (native, WebGPU, LiteRT-LM templates).
Unsupported or intentionally unavailable paths: None added. A template dinja cannot parse still falls back to the regex caps, uncached and logged.
Out of scope / follow-ups: Gaps that exist at base and are unchanged here:
en-GB; llama.cpp removed its TranslateGemma handler (ggml-org/llama.cpp#18675) and prints empty codes.Lfm2Handlerbuilds a grammar only forrequiredor the force-JSON marker; llama.cpp'slfm2.cppbuilds a lazy Python-style tool-call grammar whenever tools are present. LFM2.5 now inherits this.Follow-ups from this PR's audit (non-blocking):
JinjaAnalyzerandLegacyJinjaAnalyzerduplicate about 70 lines (_supportsThinking,_findAll). The legacy analyzer could be deleted once the probe is trusted, since it only runs for a template the probe cannot prepare./props(tools false, tool calls true). The warning is misleading for this template: its prompt and grammar are unchanged.Completeness checklist
PR type guidance
template_message_json_parity_test.dart: 5 new llama-server/apply-templatecases, each failing at basedb7b45881: SmolVLM text (typed-only), Ministral 3 and TranslateGemma 2B image plus reasoning-only turn, Ministral 3 14B image plus empty assistant turn, LFM2.5 with tools. Kimi-K2's image case now compares everything after the user text, including the tool result (fails at base). A new test checks detected caps against thechat_template_caps/propsreported for these templates.template_caps_cache_fixtures_test.dartpins caps for all 32 fixture templates; each bit butsupports_thinkingequals llama-server/props.jinja_usage_probe_test.dartcovers the probe;jinja_analyzer_test.dartcovers probe outcomes.specialized_tool_grammar_validation_e2e_test.dart: the LFM2.5requiredgrammar compiles and accepts or rejects calls (fails at base: generic format).Test Plan
At head
3ab42978e, run serially on a loaded shared Mac.dart run tool/prepare_workspace.dart: no tracked changes, at head and base.dart format --output=none --set-exit-if-changed .dart analyzedart run tool/testing/check_platform_boundaries.dartdart test -p vm -j 1 --exclude-tags local-only: 3,561 pass, 4 skipped; basedb7b45881: 3,502 test names. 22 base names are gone, all renamed: 16 render-context variants now also named bytypedContentOnly; LFM2.5 detection now expects LFM2; the probe-failure cache test now expects caching; 4jinja_analyzer_test.darttests now name the llama.cpp probe outcome they assert. None removed against the previous headb37c64868.dart test -p chrome --exclude-tags local-only: 1,703 pass; base: 1,655 test names. 21 base names are gone, the same renames less the VM-only detection test. None removed againstb37c64868../tool/testing/run_template_parity_suites.shpasses: detection 74, diagnostic 96, unit template 800, upstream chat tests 4, compiled grammar 18.(cd example/chat_app && flutter test): 491 pass.flutter test --platform chrome test/chat_generation_service_test.dartpasses.Caps against llama-server
7fe450e19/props(the 7 fields both report): 97 of 97 templates llama-server loads (base: 90), and 173 of 173 distinct templates the VM unit tests passed to caps detection. One more, the empty template, is not comparable: llama-server replaces it with the model's default template.Differential against llama-server
7fe450e19/apply-template, basedb7b45881vs head:test/fixtures,tool/litert_lm_templates, llama.cppmodels/templates, and templates from local GGUF files).<__media_…__>markers, the leading BOS and dates are normalized; LFM2 tool lists are compared in the flat shape.The 25 away:
res_str) differs only in llama-server continuing a final assistant turn, which llamadart does not do (pre-existing).<__media__>Describe; the rest differs by the pre-existingen-GBcodes.Grammar, triggers, stops, preserved tokens, parser and thinking-forced-open change only for LFM2.5-1.2B (format generic to LFM2, intended) and for 38 image renders, each of which now equals its text analogue.
Mutations, each run against the tests named and each failing:
/propscaps testtyped-only templates get the audio marker in a text parttojsoninput deeplymarks every nested value used when tojson reads a valuethrows on a for loop over a string, as llama.cpp doestoStringround-trips literals in every expression positionround-trips signatures, calls, filters, tests and statements*argsparameters rejectedround-trips signatures, calls, filters, tests and statementsround-trips signatures, calls, filters, tests and statementsfalls back to the legacy analysis when the probe cannot be preparedMatrix Evidence
static-format-analyzeroot-vmroot-chrometemplate-parityb10549templatesrun_template_parity_suites.shstructured-output-adversarial7fe450e19examples-testschat_generation_service_test.dart4 passhigh-risk-exact-head-independent-qa3ab42978evsdb7b45881claude-opus-5-5-fresh-subagent-audit721: 0 PR-caused P1s, 0 unresolved threadsReview Notes
b37c64868againstdb7b45881with 0 P1s. It asked for one PR-caused, non-blocking fix before merge: the probe rewriter threw on floats Dart prints with an exponent and on*argsmacros, leaving default caps (tools, tool calls and parallel calls false; base and llama-server report true).3ab42978efixes it: the three repros failed atb37c64868and pass now. At3ab42978e, caps and prompts equalb37c64868's on all 3,626 differential renders. A delta audit then accepted3ab42978eagainstdb7b45881with 0 P1s and 0 unresolved threads (record below).3ab42978e(23 pass, 5 skipped).High-risk regression review
structuredOutput), fromclassify_high_risk_changes.dart.b37c64868, then the3ab42978edelta).tool/testing/high_risk_readiness.darton the evidence with this record exits 2 (unverifiedPrerequisites). Record:{"auditor_identity":"claude-opus-5-5-fresh-subagent-audit721","audit_kind":"operator-owned","audit_head_sha":"3ab42978eecd6d64495edbf7815cce3a99849eb3","audit_base_sha":"db7b45881d8a92cc6d9d1e0aabfe7e368e9856a1","decision":"accepted","unresolved_review_threads":0,"known_pr_caused_p1_regressions":0,"summary":"2026-09-25, two rounds. R1 (b37c6486): own harness vs own llama-server 7fe450e19 build, 95 templates x 36 conversations in 4 variants: 0 new throws, caps 94/94 vs /props (base 87), 2334 vs 2282 of 3268 identical to the server, 20 away checked hunk by hunk (19 SmolVLM, 1 TranslateGemma, all old behaviour); FunctionGemma real run byte-identical to base; FireFunction prompt and grammar unchanged plus a warn log; LFM2.5 gains native parallel calls; 9/9 mutations red; gates green. Flagged a non-blocking rewriter fallback to default caps on exponent floats and *args. R2 (3ab42978, delta): repros fixed; caps and renders identical to b37c6486 on all 3420x4 renders and 87 synthetic templates; synthetic caps equal /props on all 81 server-loadable templates; 183-template corpus round-trips; 25k fuzzed templates (8222 parse): 0 failures outside the dinja `not`-as-identifier quirk on invalid Jinja (163 cases fall back to base or probe unary not); legacy fallback is base's analyzer verbatim, so never worse than base, and fixture round-trip tests keep writer bugs visible; about 70 duplicated lines, non-blocking; 5/5 delta mutations red; VM 3485+78 skipped, Chrome 1702, parity, chat_app green; CI 23 pass and 5 skipped; 0 threads; merges cleanly with main (#724 docs only)."}3ab42978e/db7b45881.7fe450e19/apply-templateand/props) pinned as durable fixtures.