Skip to main content

flutter_edge_ai_litertlm 1.12.1 copy "flutter_edge_ai_litertlm: ^1.12.1" to clipboard
flutter_edge_ai_litertlm: ^1.12.1 copied to clipboard

LiteRT-LM (.litertlm) engine for flutter_edge_ai: dart:ffi on 5 native platforms plus web, CPU/GPU/NPU, and the LiteRT embedding backend.

flutter_edge_ai_litertlm #

Renamed from flutter_gemma_litertlm. Same package, new name: swap the dependency and the package:flutter_gemma_litertlm/ imports; nothing on the device changes. See the migration guide.

LiteRT-LM on-device engine for flutter_edge_ai: runs .litertlm models and LiteRT .tflite embeddings. Opt-in package — add it only if you run either. Android, iOS, macOS, Linux and Windows via dart:ffi; Web (early preview) via @litert-lm/core and LiteRT.js.

This package owns the shared LiteRT-LM native library (libLiteRtLm) and exposes the LiteRT C API bindings (LiteRtBindings); both are shared by flutter_edge_ai_speech. It also ships the LiteRT C API embedding backend (LiteRtEmbeddingBackend) — see Embeddings below — over the runtime-agnostic embedding pipeline in flutter_edge_ai. Tokenizers come from flutter_edge_ai_embeddings, which the app registers; this package does not depend on it.

Teach your AI assistant this package #

dart run skills@ get --all

Installs the agent skills flutter_edge_ai bundles — this package depends on it, so they come with it. One of them, flutter-edge-ai-inference, covers the .litertlm engine, installing a model from Hugging Face, sessions, streaming, and the platform setup for all six targets.

Usage #

import 'package:flutter_edge_ai/flutter_edge_ai.dart';
import 'package:flutter_edge_ai_litertlm/flutter_edge_ai_litertlm.dart';

await FlutterEdgeAi.initialize(
  inferenceEngines: [LiteRtLmEngine()],
);

LiteRtLmEngine handles ModelFileType.litertlm models; pass it alongside other engines (e.g. MediaPipeEngine from flutter_edge_ai_mediapipe) if your app uses both formats.

Depend on both packages (flutter_edge_ai: ^2.2.1, flutter_edge_ai_litertlm: ^1.12.1) and install .litertlm files with fileType: ModelFileType.litertlm: installModel defaults to ModelFileType.task, which routes the file to MediaPipe.

Sampling #

Leave temperature, topK, topP and randomSeed unset on createSession or createChat and each comes from the sampler the .litertlm file ships, else from the model family's defaults (SamplingParams.forModelType), else temperature: 0.8, topK: 40, topP: 0.95, randomSeed: 1; a value you pass wins, field by field. topK: 1 or temperature: 0 decodes greedily. Before core 2.2.0 an unset topK was 1, so every session decoded greedily.

  • LiteRT-LM keeps the sampler of an engine's first generation, random state included, for every later conversation (LiteRT-LM #2080): set sampling before the first generation, or close and reload the model to change it. A warning says so once per loaded model when a later session asks for other values and either set was yours.
  • On web the file's sampler is not read, so unset values come from the family defaults. The values you pass are applied; before 1.12.0 web dropped them.
  • On the NPU decoding is greedy whatever you pass; a one-time warning says so when you set any value.

Context and reply length #

maxTokens on getActiveModel is the whole context window — prompt, history and reply — not the reply length. On CPU and GPU a value below 1024 is raised to 1024, because the published bundles fail to allocate below it; on the NPU it is passed through, so use the context size the bundle was compiled for. Cap the reply with createSession(maxOutputTokens:).

Install from a Hugging Face repo (litertlm_manifest.json) #

Repos that ship a litertlm_manifest.json deployment manifest describe every .litertlm file they contain — which backends each is verified on, which file a given platform should pick, sha256/ size identity, and session guidance. LitertlmManifestResolver reads it so an app installs "the right file for this device" without hardcoding filenames:

import 'dart:math' show max;

// LiteRtLmEngine carries this resolver, so registering the engine registers
// it too. Pass huggingFaceResolvers: only to override — e.g.
// [LitertlmManifestResolver(revision: 'abc123')] to pin a commit.
await FlutterEdgeAi.initialize(inferenceEngines: [LiteRtLmEngine()]);

final r = await FlutterEdgeAi.resolveHuggingFace(
    'litert-community/Qwen3-4B-Thinking-2507',
    fileType: ModelFileType.litertlm);
await FlutterEdgeAi.installModel(
      // The manifest types 2507 as qwen3; ModelType.qwen is right for it.
      // For other repos, r.modelType ?? ModelType.general.
      modelType: ModelType.qwen,
      fileType: r.fileType,
    )
    .fromNetwork(r.url) // authoritative: carries the resolver's revision pin
    .install();
final model = await FlutterEdgeAi.getActiveModel(defaults: r.runtime);
final session = await model.createSession(
  enableThinking: r.runtime.thinkingDeclared ?? false,
  // minOutputTokens is a floor, not a cap: keep the app's own budget unless
  // the manifest asks for more.
  maxOutputTokens: max(1024, r.runtime.minOutputTokens ?? 0),
);

Everything the manifest returns is an overridable default (explicit argument > manifest > SDK default); r.notes carries platform caveats and known issues worth surfacing to developers. Repos without a manifest keep working through installModel(...).fromHuggingFace(repo, file: ...).

To resolve and install in one step, omit file: fromHuggingFace(repo) reads the manifest at install time, installs the revision-pinned variant, and returns the same defaults on InferenceInstallation.runtime (plus notes). The two-step form above stays the offline-safe one — manifest mode needs the network on every install, because the variant's filename is only known after the fetch.

Embeddings #

import 'package:flutter_edge_ai_embeddings/flutter_edge_ai_embeddings.dart';
import 'package:flutter_edge_ai_litertlm/flutter_edge_ai_litertlm.dart';

await FlutterEdgeAi.initialize(
  embeddingBackends: [LiteRtEmbeddingBackend()],
  embeddingTokenizers: [GemmaEmbeddingTokenizers()],
);

Both lists, and both packages: this one brings the backend, and flutter_edge_ai_embeddings brings the tokenizers it asks core for. On web the tokenizer list is unused — the LiteRT.js bundle tokenizes in JS.

LiteRtEmbeddingBackend runs Gecko / EmbeddingGemma .tflite models via the LiteRT C API. The pipeline it plugs into — the forward-pass seam, the worker isolate and the pooling — lives in flutter_edge_ai; the tokenizers come from flutter_edge_ai_embeddings, which your app registers via embeddingTokenizers:. This package depends on neither beyond core. On web it runs via LiteRT.js instead; see Embeddings on web below for the four files and the <script> tag your app needs.

EmbeddingModel.activeBackend is cpu on native, the only backend this package's embedder uses, so preferredBackend is not applied. On web it is null and LiteRT.js picks: window.getLiteRtEmbeddingAccelerator() names where the output buffer lived after the first embedding, and window.getLiteRtEmbeddingFullyAccelerated() says whether the graph landed entirely on the requested accelerator — false also when LiteRT silently recompiled a WebGPU request for WASM.

Embeddings on web #

On web, flutter_edge_ai_litertlm's embedding backend runs via LiteRT.js. Copy all four files from this package's web/ into your app's web/, next to index.html — litert_embeddings.js imports the other three by relative path, so they have to sit together:

litert_embeddings.js  sentencepiece.js  litert.js  tensorflow.js

They are four pieces of one bundle (the entry plus three vendor chunks), built together by tool/web_build, so never mix them across package versions. Find this package's directory with grep -A1 '"name": "flutter_edge_ai_litertlm"' .dart_tool/package_config.json, then load the entry module from web/index.html:

<script type="module" src="litert_embeddings.js"></script>

Upgrading from an earlier version: delete the copies in your app's web/ and re-copy all four from this package. Before flutter_gemma_litertlm 1.8.0 they came from flutter_gemma_embeddings, and the copies you have are built against an older @litertjs/core than the runtime this version loads. If you built your own web/wasm/, either delete it and take the CDN default or rebuild it from the version in LiteRtWebRuntime.pinnedVersion.

Loading litert_embeddings.js straight from a CDN with a Subresource-Integrity hash — which an older README suggested — cannot work: the module's three imports resolve against the CDN path, where they do not exist, so the module never executes and every embedding call fails on an undefined global. SRI would not have covered the imports either.

The WASM runtime #

LiteRT.js loads a WASM runtime at the first embedding call — litert_wasm_internal.js, or litert_wasm_compat_internal.js on a browser without relaxed SIMD, each with a ~9 MB .wasm beside it. Since flutter_gemma_litertlm 1.8.0 they come from the pinned @litertjs/core build on jsDelivr by default — nothing to install, and nothing this package has to carry into every native-only app.

To serve them yourself (offline, an air-gapped deploy, or a CSP that forbids third-party script), copy node_modules/@litertjs/core/wasm/ into your app's web/wasm/ and point the package at it before the first embedding:

import 'package:flutter_edge_ai_litertlm/flutter_edge_ai_litertlm.dart';

LiteRtWebRuntime.wasmPath = '/wasm/';

Those files come from @litertjs/core — npm i @litertjs/core@2.5.3 in a scratch directory, then copy its wasm/.

Set the prefix before the first embedding — the runtime is loaded once and cached, so a later assignment is ignored. LiteRT.js inserts the separator when it joins the prefix with the file name, so the trailing slash above is convention, not a requirement; the value is root-absolute, and an app served under a base href other than / needs /my-app/wasm/ or a full URL.

Pin @litertjs/core to LiteRtWebRuntime.pinnedVersion if you vendor it. The runtime and this package's web/litert.js are two halves of one release — litert.js calls that release's WASM entry points by name — and a mismatch fails at the first embedding with something that does not mention versions at all: a runtime older than the glue gives Cannot read properties of undefined (reading 'create').

Serving it yourself is also the answer if a third-party script in your app's runtime path is not acceptable to you: LiteRT.js injects the <script> itself, so the CDN copy carries no Subresource-Integrity hash.

Whatever host you use must send Access-Control-Allow-Origin (LiteRT.js sets crossOrigin="anonymous" on the script it injects) and serve .wasm as application/wasm.

Native embeddings need no web files; the platform setup under Platforms still applies.

Web setup (early preview) #

.litertlm web inference runs via @litert-lm/core (WebGPU/WASM). Load a model's web export (gemma-4-E2B-it-web.litertlm, not gemma-4-E2B-it.litertlm). It is text-only: an image or audio message throws UnsupportedError, because the JS API creates the LLM engine with no vision or audio executor, and loraPath throws too. createSession(maxOutputTokens:) is honoured here as it is on native. Add the handshake below to your app's web/index.html <head> — the ESM doesn't assign window globals and module scripts are deferred, so Dart awaits window.litertLmReady (which resolves to the Engine constructor):

<script type="module">
window.litertLmReady = (async () => {
  const m = await import('https://cdn.jsdelivr.net/npm/@litert-lm/core@0.18.0/+esm');
  window.Engine = m.Engine;
  return m.Engine;
})();
</script>

Native platforms need no web setup.

Platforms #

Platform Support
Android ✅ FFI (GPU via OpenCL, NPU on Qualcomm — opt-in, below)
iOS ✅ FFI (GPU via Metal on device; CPU on simulator)
macOS / Linux ✅ FFI (GPU via Metal / Vulkan; NPU on Qualcomm Linux arm64 — opt-in, below)
Windows ✅ FFI (CPU + GPU via DirectX 12 + Intel NPU)
Web ⚠️ early preview via @litert-lm/core (CDN, text-only)

The native library (native-v0.18.0-d, LiteRT-LM v0.18.0) is fetched at build time by hook/build.dart from a SHA256-verified GitHub release. The app still needs its platform setup: Android minSdk 30, iOS 15.0 and the memory entitlements, and on macOS a post_install block in macos/Podfile, without which the first model load fails with Library not loaded: @rpath/libGemmaModelConstraintProvider.dylib. See platform setup.

PreferredBackend.npu is attempted only on Windows, on Android devices with Qualcomm FastRPC (libcdsprpc.so) and on Qualcomm Linux arm64 boards, the last two when the app opted in (below); elsewhere it falls back to GPU, then CPU, and prints why. On Windows the check is per OS, so a PC without an Intel NPU can report activeBackend == npu while the model runs elsewhere.

Run a Gemma 4 bundle on the NPU: a Gemma 3 bundle drops every prefill chunk after the first, with no error, and answers from the start of the prompt alone (LiteRT-LM#3508).

Qualcomm NPU on Android and Linux arm64 (opt-in) #

This package does not ship Qualcomm's QNN runtime: Qualcomm licenses it for redistribution inside an application only. To use PreferredBackend.npu on Snapdragon, opt in from your app's pubspec.yaml:

hooks:
  user_defines:
    flutter_edge_ai_litertlm:
      qualcomm_npu: true

Put it in the pubspec of the app you build. If that app is a member of a pub workspace, put it in the workspace root's pubspec instead; pub ignores user_defines anywhere else.

With the flag set, the build hook downloads com.qualcomm.qti:qnn-runtime:2.50.0 from Maven Central once, checks its SHA-256, raises the Hexagon libraries' page alignment to the 16 KB Google Play requires, and bundles the ten QNN libraries into the Android app: about 22 MB more to download and 83 MB more installed (one blob per Hexagon generation, V73 to V81). The first NPU run copies them out of the APK once more, because the DSP loads them from files, so a phone that uses the NPU holds about 166 MB of them. Setting the flag means accepting Qualcomm's AI Stack License, the LICENSE.pdf the hook prints the path of. Its notices are in this package's NOTICES, so they reach your app's licence page.

Behind a proxy, the hook honours HTTPS_PROXY. Without access to Maven Central, point it at a mirror or at the AAR itself:

hooks:
  user_defines:
    flutter_edge_ai_litertlm:
      qualcomm_npu: true
      qualcomm_npu_maven_url: https://maven.example.com/maven2
      # or: qualcomm_npu_aar: third_party/qnn-runtime-2.50.0.aar

qualcomm_npu_aar is resolved against the pubspec that declares it and must be the same file as Maven's (same SHA-256). Without the flag, a request for PreferredBackend.npu on Android falls back to GPU, then CPU, and the log says how to enable it.

Linux arm64. The same flag bundles the stack into Linux arm64 builds, for Qualcomm Linux boards with a Hexagon V68–V81 compute DSP (QCS6490, QCS8275, QCS9075, …). Qualcomm publishes no Maven artifact for Linux, so the hook reads the fourteen libraries it needs (libQnnHtp, libQnnSystem and a Stub/Skel pair per Hexagon version) out of Qualcomm's public QAIRT 2.50 SDK zip with HTTP range requests — about 32 MB of a 2.6 GB archive — checks each against a pinned SHA-256, and caches them; about 90 MB more installed. Offline, download v2.50.0.260828.zip yourself and point the hook at it:

hooks:
  user_defines:
    flutter_edge_ai_litertlm:
      qualcomm_npu: true
      qualcomm_npu_qairt_zip: third_party/v2.50.0.260828.zip

On the board, Qualcomm's FastRPC library must be installed (qcom-fastrpc1 on Ubuntu; Qualcomm's images ship it) and the user must be in group fastrpc (sudo usermod -aG fastrpc $USER, then log in again). If either is missing, npu falls back to GPU, then CPU, and the log names what to fix. Use the model compiled for your SoC, e.g. gemma-4-E2B-it_qualcomm_qcs8275.litertlm. A Linux x64 build ignores the flag.

Troubleshooting #

Wrong digits from a long prompt on GPU #

Gemma 4 can copy numbers wrongly out of a prompt of about 2,000 tokens or more on Metal and Adreno, the same way on every run. The published Gemma 4 .litertlm files ask for half-precision activations; pass activationDataType: ActivationDataType.float32 to getActiveModel for full precision.

Linux: any tool call aborts the app (fixed in 1.11.1) #

Up to 1.11.0, a chat or session with tools aborted on its first constrained token on Linux x64 and arm64: the prebuilt constraint provider uses another C++ ABI than the runtime. 1.11.1 (native-v0.18.0-d) bridges it. See #551.

iOS/macOS: GPU crashes in apps that also use flutter_litert (fixed in 1.10.1) #

In 1.10.0 the GPU backend crashed in LiteRtCreateCompiledModel when the app also used flutter_litert. 1.10.1 (native-v0.18.0-b) fixes it; no app change is needed.

Qwen keeps reasoning with thinking off, or shows <|channel>thought text (fixed in 1.9.0) #

Up to 1.8.7 "thinking off" sent nothing to the template, and a Qwen3 template that reads enable_thinking treats a missing key as on: a bundle such as Qwen3-0.6B_dynamic spent its whole output budget reasoning. Bundles that declare a thought channel also streamed that reasoning into the answer as <|channel>thought…<channel|> text, and the web path sent thinking — a key no template reads. 1.9.0 sends enable_thinking both ways on every platform, and flutter_edge_ai 2.1.0 turns channel reasoning into ThinkingResponse for every ModelType and no longer appends /no_think to an audio message (Qwen3-ASR returned an empty transcript).

A stopped chat answers every later message with nothing (fixed in flutter_gemma_litertlm 1.8.1) #

Symptom: after stopGeneration() in the middle of a reply — or after abandoning the response stream — every later message on that chat or session comes back empty, on Android, iOS and desktop. A new chat on the same model answers normally. (The web engine is a separate path and is not covered by this entry.)

Cause: a conversation whose generation is cancelled mid-reply stays unusable in the native runtime.

Fix: every flutter_edge_ai_litertlm release has it. The first turn after a stop runs on a fresh conversation that replays the chat's history, including whatever the stopped reply had produced. That history is replayed as text: images and audio sent in earlier turns are not, so after a stop the model can no longer see them.

Google Play rejects the app over 16 KB page sizes (fixed in flutter_gemma_litertlm 1.8.0) #

Symptom: Play Console refuses the release with "Your app does not support 16 KB memory page sizes", on any app that depends on this package. Nothing fails at build or run time — the rejection happens at submission.

Cause: the Qualcomm Hexagon DSP blobs of the NPU path (libQnnHtpV{73,75,79,81}Skel.so) arrive from Qualcomm with a 4 KB p_align. Before 1.10.0 this package bundled them into every APK; Play scans lib/**/*.so and does not care that a Hexagon image is loaded by the DSP rather than mapped by the kernel.

Fix: every flutter_edge_ai_litertlm release has it. Since 1.10.0 the Skels ship only when the app sets qualcomm_npu: true, and the hook raises them to 16 KB. Check your own build with Google's check_elf_alignment.sh against the APK, not against this package.

Android GPU crashes at engine_create on Mali (fixed in flutter_gemma_litertlm 1.8.2) #

Symptom: in flutter_gemma_litertlm 1.7.0–1.8.1, PreferredBackend.gpu on an Android phone with a Mali GPU (Samsung A-series, MediaTek, Google Tensor) kills the process while the model loads — SIGSEGV at pc 0 inside libLiteRtOpenClAccelerator.so. The CPU backend and Adreno GPUs are unaffected.

Cause: the OpenCL and GPU accelerators from LiteRT-LM v0.17.0 call AHardwareBuffer_allocate without declaring libandroid.so as a dependency, so Android binds the call to address 0. Only Mali takes that path.

Fix: every flutter_edge_ai_litertlm release has it; no app change is needed. See #545.

Any tool call kills the app (fixed in flutter_gemma_litertlm 1.7.1) #

Symptom: in flutter_gemma_litertlm 1.7.0, a chat or session created with tools dies on the first decoded token — EXC_BAD_ACCESS / SIGSEGV inside the runtime, on every platform, CPU and GPU alike (Linux had a second cause, fixed in 1.11.1 — see above). Dart sees no exception; flutter test reports only that the test did not complete. Generation without tools is unaffected.

Cause: constrained decoding is implemented by a prebuilt companion, libGemmaModelConstraintProvider, that ships with the LiteRT-LM release. Upstream replaced the Constraint interface, and the companion published at tag v0.17.0 still implements the old one, so the runtime we build calls into the wrong vtable slot.

Fix: every flutter_edge_ai_litertlm release has it: since flutter_gemma_litertlm 1.7.1 the companion is rebuilt from upstream main.

Windows: embeddings or speech fail with status=3 (fixed in flutter_gemma_litertlm 1.7.0) #

Symptom: on Windows only, LiteRtEmbeddingBackend and flutter_edge_ai_speech fail with LiteRT call failed: CreateTensorBufferFromHostMemory(...) (status=3) in flutter_gemma_litertlm 1.4.0–1.6.4. Text generation is unaffected.

Cause: LiteRT made LiteRtLayout one layout on every compiler; this package still wrote tensor shapes in the old MSVC layout on Windows.

Every flutter_edge_ai_litertlm release includes the fix. On the legacy package line, upgrade flutter_gemma_litertlm to 1.7.0 and flutter_gemma_speech to 0.5.1.

Garbled or empty streams on Android (fixed in flutter_gemma_litertlm 1.5.2) #

Symptom: a generation delivers zero chunks and throws Exception: Stream error: <U+FFFD>, often followed by Callback invoked after it has been deleted and a SIGABRT that Dart cannot catch.

Cause: on Android the first dlopen of libLiteRtLm decides, for the whole process, whether its exports are reachable from the default symbol search scope, and bionic never promotes an already-loaded library afterwards. Before flutter_gemma_litertlm 1.5.2 the embeddings and speech entry point opened it locally, so an app that embedded or transcribed anything before its first generation left the stream-callback ABI probe unable to see the library — and the probe read that as "old library" and registered the wrong callback shape.

Fix: every flutter_edge_ai_litertlm release has it. If you load libLiteRtLm yourself from app or third-party code, load it before flutter_edge_ai does and with RTLD_GLOBAL. The package cannot repair that case — bionic never promotes an already-loaded library — but it no longer generates corrupt text: a .litertlm generation raises a StateError naming the condition, and embeddings or speech (which resolve through their own handle and do not need the symbols to be ambient) log a warning and carry on. See #447.

dlopen / "library not found" (libLiteRtLm) #

flutter_edge_ai_litertlm is the sole owner of the shared native library (libLiteRtLm) and bundles it via its build hook — this package's own LiteRtEmbeddingBackend and flutter_edge_ai_speech both use it directly. A stale Native-Assets cache after a native version bump can leave the library unbundled, surfacing as an opaque dlopen "no such file" on the first inference. Fix with a clean rebuild:

flutter clean
rm -rf ~/Library/Caches/flutter_gemma/native        # macOS
rm -rf ~/.cache/flutter_gemma/native                # Linux
# Windows: rmdir /s /q "%LOCALAPPDATA%\flutter_gemma\native"
flutter pub get
1
likes
0
points
8.58k
downloads

Publisher

verified publishersashadenisov.dev

Weekly Downloads

LiteRT-LM (.litertlm) engine for flutter_edge_ai: dart:ffi on 5 native platforms plus web, CPU/GPU/NPU, and the LiteRT embedding backend.

Homepage
Repository (GitHub)
View/report issues

Topics

#gemma #llm #litert #on-device #ffi

License

unknown (license)

Dependencies

code_assets, crypto, ffi, flutter, flutter_edge_ai, hooks, mutex, path_provider

More

Packages that depend on flutter_edge_ai_litertlm