Repository navigation
Conversation
generate() wrapped the prompt in the bundle's chat turn, so create() requests that fall back to a Dart-rendered prompt were templated twice. Raw generation now uses a copy-through conversation template.
…dit) A raw prompt that starts with the start token the runtime adds, such as Gemma's <bos>, no longer gets a second one; an empty prompt throws LlamaArgumentException. The legacy template-setter refusal names every conversation template, and the LiteRT-LM benchmark measures chat again.
This was referenced Nov 9, 2026
leehack
added a commit
that referenced
this pull request
Nov 9, 2026
leehack
added a commit
that referenced
this pull request
Nov 9, 2026
Owner
Author
|
Landed through the 0.19.2 batch integration PR #1870 (rebase merge, one commit per constituent; this PR's pinned head was audited/CI-gated there). |
This was referenced Nov 9, 2026
leehack
added a commit
that referenced
this pull request
Nov 9, 2026
#1873) engine.chatTemplate() on native LiteRT-LM renders the bundle template since #1854, so a Gemma 4 prompt starts with the text `<bos>`. The renderer counted it with tokenize(prompt, addSpecial: false), which the LiteRT-LM tokenizer reads as three plain tokens, while native prefill strips that text and adds one start token: tokenCount was 2 too high. A runtime-template prompt is now counted by the backend through an internal BackendRuntimePromptTokenization probe. LiteRtLmService reuses the start-token stripping raw generate() uses (#1864) and tokenizes the rest with the start token added once. Other prompts, backends and Web keep the plain count. Fixes #1872 Refs #1845
This was referenced Nov 10, 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
generate()now sends the prompt unchanged. It used to create its conversation with the bundle's chat template, so the runtime wrapped the prompt in a user turn and generation prompt (12 extra tokens on Qwen3.5, 8 on Qwen3, 7 on Gemma 4).create()requests that fall back to a Dart-rendered prompt (a trailing system message,parallelToolCalls: true) reach the same backend method, so they were templated twice. They are now templated once.liteRtLmRawPromptTemplate, an internal template that copies the message text through. It uses the existinglitert_lm_conversation_config_set_prompt_templatesetter, so no native change is needed. Raw generation no longer runs the bundle-template probe, and its send failures no longer carry the bundle-template note.<bos>(which the runtime parses as the token but the tokenizer API does not) is removed as text.None) is removed only when the rest of the prompt keeps its tokens.tokenize('', addSpecial: true)anddetokenize.LlamaArgumentException(name: 'prompt')before native work. Previously the runtime failed with "No token IDs found" (Qwen3.5) or "Session is not prefilled yet" (Gemma).LlamaUnsupportedException. The message names the setter, says llamadart cannot set a conversation template (needed by raw prompt generation,ModelParams.chatTemplateand the built-in family templates such as Qwen3 text chat), and names the packagedlitert-lm-nativeversion.litert_lm_benchmark_app.dart) now measures a user turn throughcreate()rather than rawgenerate(), so its numbers still describe chat prompts.generation-and-streaming.md, thellamadart-chat-streamingSKILL line, and a changelog bullet in both files.Fixes #1853. Refs #1845 (lane 9). Web follow-up: #1863. Pre-existing bug found by the audit, not fixed here: #1867.
Stack: based on #1854 (
lane3-1836-template-split, audited atd6a62a80). Merge #1854 first; this PR's diff is only #1853.Maintainer decision (option A of the options in #1853): "Raw generate (Recommended)". Options considered: A, make
generate()raw; B, keep the wrapping, document it, and add new backend SPI so only the fallbacks go raw; C, refuse the fallbacks on LiteRT-LM.Production-readiness scope
engine.generate(prompt)passes the prompt to the model without a chat template, as on llama.cpp and asgenerate()is documented, with exactly one start token where the model has one.create()fallbacks send the Dart-rendered prompt once.v0.19.1-2runtime (C API 1.0, which requires the setter), plus legacy runtimes that export it.LlamaArgumentException.LlamaUnsupportedExceptionfor raw generation instead of silently wrapping the prompt.@litert-lm/[email protected]cannot replace its conversation template. This is documented in the guide and SKILL and tracked in LiteRT-LM Web generate() wraps the raw prompt in the bundle's user turn #1863. Webcreate()is not double-templated, because its Dart template passes only the latest message's text through.Completeness checklist
validation_coverage_test.dartpasses in the Markdown suites.PR type guidance
LiteRtLmService._generatebuilt its conversation through_createBundleTemplateConversation, and the client sends the prompt as{"role":"user","content":[{"type":"text","text":prompt}]}. The bundle template therefore wrapped every raw prompt, including the fallback promptscreate()had already rendered. Regression tests are listed below.Test Plan
dart run tool/prepare_workspace.dartdart format --output=none --set-exit-if-changed .dart analyze(no issues);check_platform_boundaries.dartOKdart test -p vm -j 1 --exclude-tags local-only: CI run 42660841299 green (workflow_dispatch; CI does not run on PRs into a non-default base). Locally,test/unit/backends/litert_lm/plus the Markdown suites pass (2247 tests).dart test -p chrome --exclude-tags local-only: Web (Chrome) 1–3 in the same CI run (no Web code changed)coverage-lib: 93.63% line coverage (threshold 70) at6101b292, local macOS arm64. The follow-up commit adds only covered code (every new branch is hit by the unit tests, as the mutations show). The local run's 6 failures are all inwindows_registry_qualification_test.dart, which needspwsh(missing on this host); they pass on CI Windows tooling.(cd example/chat_app && flutter test test test_vm)passes (690), and the Chrome subset (chat_generation_service_test,image_generation_screen_test,unit_test,conversation_state_service_test) passes (133).Failing-first:
litert_lm_service.dartandlitert_lm_runtime.dart(keeping the new constant so the tests compile): 4 tests fail. The new local-only e2e test also fails on base: Qwen3 runtime input 10 vs 2 expected; Qwen3.5 13 vs 1.6101b292's service and runtime: 5 tests fail (the start-token pair, the two empty-prompt refusals, and the legacy wording).New and changed tests:
litert_lm_service_test.dart:liteRtLmRawPromptTemplate, passes the prompt unchanged, and neither reads the bundle nor runs the probe; a later chat still probes and adapts.LlamaArgumentExceptionbefore any conversation or send.generateas their trigger now usegenerateChat.litert_lm_bundle_template_test.dart: the template renders prompts unchanged as parts and as a string. The prompts include leading and trailing whitespace, ChatML, literal Jinja syntax and Unicode. It renders only text parts.litert_lm_conversation_template_test.dart: the legacy refusal names the setter, "cannot set a conversation template", raw prompt generation, andlitert-lm-native $liteRtLmReleaseTag.test/e2e/backends/litert_lm_bundle_template_e2e_test.dart(local-only):tokenize(prompt, addSpecial: true).hiis sent ashi, with runtime input tokens equal totokenize('hi', addSpecial: true), and the start token alone throwsLlamaArgumentException.Mutations (each applied alone against the three unit files; all 14 killed):
The first commit's "raw send failure gets the bundle-template note" mutation was killed at
6101b292, and that code is unchanged.Token counts. Native LiteRT-LM
v0.19.1-2, macOS arm64 CPU,numberOfThreads: 2. "Sent" istokenize(prompt, addSpecial: true)of the prompt_generatereceived.generate('hi')generate(ChatML 'hi' turn)create(), trailing system message, toolscreate(parallelToolCalls: true), toolsgenerate('hi')create(), trailing system message, toolscreate(parallelToolCalls: true), toolsgenerate('hi')generate('<bos>hi')6101b292generate(chatTemplate().prompt), which starts with<bos>6101b292create(), trailing system message, toolscreate(parallelToolCalls: true), toolsNative-chat
create()is unchanged (Qwen3.5 16/16, Qwen3 142/142, Gemma 57/57). For the Gemma<bos>rows, "Sent" is the prompt's token count with its leading<bos>read as the single start token.Matrix Evidence
root-vmcoverage-lib6101b292litert-lm-engine-smoke6101b292)promptEvalTokens14 =promptTokenCountWithSpecial14 (Qwen3); 13 = 13 (Qwen3.5)litert-lm-chat-features-smoke6101b292)<think>); the follow-up does not touch chatmaintoo; see #1867high-risk-exact-head-independent-qa6101b292; re-audit of the new head requestedlitert-lm-v1-public-*,litert-lm-json-schemaReview Notes
6101b292passed with no blocking findings. Its P2 (double start token) and P3s (empty prompt, legacy wording, benchmark) are fixed inee80158a. It also found LiteRT-LM bundle-template probe lets a C API 1.0 render failure escape, so chats on string-concatenating templates fail #1867, which is pre-existing onmainand filed. The head has moved, so the audit must be re-run or extended toee80158abefore mark-ready.ee80158ada816bd8cec329afa16c3daf20e24958, CI run 42660841299 green (workflow_dispatch; CI does not run on PRs into a non-default base).High-risk regression review
backendRuntime)6101b292(no blocking); re-audit ofee80158apendingee80158ada816bd8cec329afa16c3daf20e24958/lane3-1836-template-splitatd6a62a80