<< All versions

Skill v1.0.0

LLM-judged scan90/100
denisovav/flutter_edge_ai/flutter-edge-ai-inference
──Details
PublishedOctober 21, 2026 at 10:34 AM
Content Hashsha256:74e125d4bb140845...
Git SHA
──Files
Files (1 file, 27.3 KB)
SKILL.md27.3 KBactive
SKILL.md · 327 lines · 27.3 KB

version: "1.0.0" name: flutter-edge-ai-inference description: Use when adding on-device LLM inference to a Flutter app with flutter_edge_ai — offline chat, running an LLM locally, installing a model from Hugging Face (gated repos included), streaming replies, a system prompt, thinking or reasoning output, image or audio prompts, picking a CPU, GPU or NPU backend, stopping generation — or setting up the recommended .litertlm engine (ModelFileType.litertlm) on Android, iOS, macOS, Windows, Linux or web, including the Android minSdk and internet permission, the Apple entitlements and Podfile, and the web index.html script tags. Also use when a reply comes back empty, the model answers identically every time, maxTokens does not shorten replies, FlutterEdgeAi is an undefined name, getActiveModel throws "No inference engine can handle this model", a session throws "Session is closed", numbers come back wrong on the GPU, or .litertlm fails to load on Android. For .task or .bin models (ModelFileType.task or ModelFileType.binary), use flutter-edge-ai-mediapipe.


Running a model with flutter_edge_ai

Rules

  1. Depend on flutter_edge_ai and an engine package, and import both. Engine packages do not re-export core.
  2. Register the engine in FlutterEdgeAi.initialize(inferenceEngines: [...]). Core ships none.
  3. Declare fileType on installModel. It defaults to ModelFileType.task, and the declaration — never the file name — picks the engine.
  4. maxTokens is the context window. Cap the reply with maxOutputTokens on the session or chat.
  5. Pass isUser: true on every user Message.
  6. Close a session or chat when its conversation ends. Keep the model while the feature is in use, and close it when the app no longer needs it.
  7. Keep Hugging Face tokens out of source: read them with String.fromEnvironment. That keeps a token out of git, not out of the app — it is compiled into the binary, and on web into main.dart.js. A shipped app should download from a repo that needs no token.
  8. On Android, set minSdk 30 for anything built on .litertlm — inference, embeddings, speech.
  9. Read references/platform-setup.md before the first build on a platform: without those entries the model fails to load or the app is killed for memory.

Setup — the recommended engine (.litertlm)

sh
flutter pub add flutter_edge_ai flutter_edge_ai_litertlm
dart
import 'package:flutter/widgets.dart';
import 'package:flutter_edge_ai/flutter_edge_ai.dart';
import 'package:flutter_edge_ai_litertlm/flutter_edge_ai_litertlm.dart';
// In main(), before runApp: initialize() needs the Flutter binding.
WidgetsFlutterBinding.ensureInitialized();
await FlutterEdgeAi.initialize(inferenceEngines: [LiteRtLmEngine()]);
await FlutterEdgeAi.installModel(
modelType: ModelType.gemma4,
fileType: ModelFileType.litertlm,
)
.fromNetwork(
'https://huggingface.co/litert-community/gemma-4-E2B-it-litert-lm/resolve/main/gemma-4-E2B-it.litertlm',
)
.withProgress((int percent) => print('downloading: $percent%'))
.install();
final InferenceModel model = await FlutterEdgeAi.getActiveModel(maxTokens: 1024);

Gemma 4 E2B is 2.6 GB and needs no token. On web use gemma-4-E2B-it-web.litertlm from the same repo (2.0 GB).

Supported models, their file types and engines: https://flutteredge.ai/docs/models.

install() skips the download when the file is already installed from the same source, so calling it at every launch is safe. The latest install becomes the model getActiveModel loads.

A gated repo needs a token, given once:

dart
const hfToken = String.fromEnvironment('HUGGINGFACE_TOKEN');
await FlutterEdgeAi.initialize(
inferenceEngines: [LiteRtLmEngine()],
huggingFaceToken: hfToken.isEmpty ? null : hfToken,
);

Build with --dart-define=HUGGINGFACE_TOKEN=hf_....

When a Hugging Face repo publishes a deployment manifest, one call picks the variant and its tested runtime settings. The engine carries its own resolver, so registering LiteRtLmEngine is enough:

dart
final install = await FlutterEdgeAi.installModel(
modelType: ModelType.general,
fileType: ModelFileType.litertlm,
).fromHuggingFace('litert-community/LFM2.5-230M').install();
final model = await FlutterEdgeAi.getActiveModel(defaults: install.runtime);

Other sources on the same builder: .fromAsset(path) for a model bundled in the app, .fromFile(path) for one already on disk, .fromBundled(name) for a platform-bundled resource. A fromFile file is used in place and stays the app's: uninstalling leaves it, and it is active again after a restart. Installing the same file name again from another source replaces the install.

modelType tells flutter_edge_ai how the model writes tool calls and reasoning, and on some engines it also picks the prompt format. Gemma 3 and Gemma 3n are ModelType.gemmaIt — there is no gemma3. The full set: ModelType.general, ModelType.gemmaIt, ModelType.gemma4, ModelType.deepSeek, ModelType.qwen, ModelType.qwen3, ModelType.qwen35, ModelType.llama, ModelType.hammer, ModelType.functionGemma, ModelType.phi. A wrong type still generates text; tool calls and reasoning then arrive as raw text.

Traps

Core not imported

  • Symptom: Undefined name 'FlutterEdgeAi', Undefined class 'InferenceModel', with only the engine package imported.
  • Fix: import 'package:flutter_edge_ai/flutter_edge_ai.dart'; as well.

flutter_gemma names after upgrading to 2.0

  • Symptom: Undefined name 'FlutterGemma', Undefined name 'GemmaLogLevel', or a package:flutter_gemma/... import that does not resolve.
  • Fix: the flutter_gemma names are gone in 2.0. Rename the dependencies and imports (flutter_gemma* → flutter_edge_ai*; flutter_gemma_rag_sqlite and flutter_gemma_rag_qdrant become flutter_edge_ai_sqlite and flutter_edge_ai_qdrant), then run dart fix --apply, which renames the old class names. Delete the old flutter-gemma-* skill directories and run dart run skills@ get --all again — they still teach the old names. RAG also left core in 2.0: see the flutter-edge-ai-rag skill.

Deep imports after upgrading to 2.3

  • Symptom: Target of URI doesn't exist: 'package:flutter_edge_ai/core/model.dart' — or any other package:flutter_edge_ai/core/…, /mobile/…, /web/…, /desktop/… or flutter_edge_ai_interface.dart import.
  • Fix: since flutter_edge_ai 2.3.0 core's internals are private under lib/src. Apps import package:flutter_edge_ai/flutter_edge_ai.dart; an engine or tokenizer package imports package:flutter_edge_ai/engine.dart. The deprecated members went in the same release — setModelPath, installModelFromAsset, initialize(enableWebCache:) and the rest; core's MIGRATION.md lists each with its replacement.

After upgrading to 2.5

  • Symptom: The method 'fromLegacyUrl' isn't defined for the type 'InferenceModelSpec', generateEmbedding/generateEmbeddings/getDimension undefined, or a deprecation warning on FlutterEdgeAiPlugin.instance.createModel.
  • Fix: build the spec as InferenceModelSpec(name:, modelSource: ModelSource.network(url) /.asset/.bundled/.file, modelType:); fromLegacyUrl defaulted to ModelType.general, so pass a model type explicitly. Replace createModel(...) (removed in 2.6.0) with FlutterEdgeAi.getActiveModel(...) — on Android, iOS and desktop it ignored modelType and fileType and returned the active model anyway — or with FlutterEdgeAi.openModel for several models. Embeddings: embed(EmbeddingInput.text(t), taskType: TaskType.retrievalQuery), embedAll, dimension (see the flutter-edge-ai-rag skill and core's MIGRATION.md).

No engine registered

  • Symptom: StateError: No inference engine can handle this model (ModelFileType.litertlm). Add the engine package to pubspec.yaml and pass it in inferenceEngines: of FlutterEdgeAi.initialize(...)
  • Fix: add the engine package and register its provider — or fix fileType if the wrong engine is registered.
  • A .gguf model needs flutter_edge_ai_llama (llama.cpp through llamadart): register LlamaEngineProvider() and install with fileType: ModelFileType.gguf. Its build setup — the hooks: user_defines: llamadart: block, the Apple companion package with SwiftPM, iOS 16.4 / macOS 14, Android arm64-v8a and x86_64 only — is in that package's README.

`maxTokens` used as a reply length

dart
// WRONG — asks for a 100-token context, not a 100-token reply
final model = await FlutterEdgeAi.getActiveModel(maxTokens: 100);
  • Symptom: replies are as long as ever. On native .litertlm the value is raised to 1024, the smallest context those models support, and only a debug-mode log says so. The web .litertlm engine does not take the value at all; on MediaPipe it is the real limit.
  • Fix:
dart
final model = await FlutterEdgeAi.getActiveModel(maxTokens: 1024);
final session = await model.createSession(maxOutputTokens: 100);

Use 4096 or more with images or audio — one image costs hundreds of tokens.

`isUser` left out

  • Symptom: an empty response, no error. Message.isUser defaults to false, so the prompt is read as the model's own turn.
  • Fix: Message(text: prompt, isUser: true).

Same reply every time

  • Symptom: identical output for identical input.
  • Causes: topK: 1 is passed somewhere (greedy; it was the default up to flutter_edge_ai 2.1); the model's family decodes greedily by default (ModelType.phi, ModelType.hammer); or the seed is fixed — an unset randomSeed is 1 (or the .litertlm bundle's own), so a freshly loaded model repeats its first sampled answer.
  • Fix: leave temperature, topK and topP unset to get the model's own sampler or its family's defaults (SamplingParams.forModelType), or pass a temperature — with no topK, a greedy family or bundle then contributes no topK: 1. Set them on the first session after getActiveModel: on .litertlm the sampler of the first generation stays in effect for later sessions (a one-time warning says so when a later session asks for other values), and its random state carries on, so a new chat already gets a new answer. MediaPipe starts each session from its seed: vary randomSeed there for a different answer to the same prompt.
  • Exception: decoding stays greedy whatever you pass on .litertlm running on the NPU (activeBackend == PreferredBackend.npu); a one-time warning says so when you set any of them. ONNX follows the model's genai_config.json, which is greedy unless it sets do_sample; values you pass override it.

`Session is closed`

  • Symptom: StateError: Session is closed from a session or chat that is still in use.
  • Cause: createSession and createChat fill one slot per model; creating another closes the one before.
  • Fix: one conversation at a time, or openSession / openChat for several (below). On the web .litertlm engine a second createSession hands back the session that is already open, history and all, rather than a fresh one — close the current chat before creating the next.

Generate

dart
// Sampling left unset: the model's own sampler, else its family's defaults.
final InferenceModelSession session = await model.createSession(
maxOutputTokens: 256,
);
try {
await session.addQueryChunk(Message(text: prompt, isUser: true));
final String reply = await session.getResponse();
} finally {
await session.close();
}

Streaming:

dart
final reply = StringBuffer();
await session.addQueryChunk(Message(text: prompt, isUser: true));
await for (final token in session.getResponseAsync()) {
reply.write(token); // update the UI here
}

To stop early, call await session.stopGeneration() — chat.stopGeneration() on a chat. Cancelling the stream subscription detaches Dart but does not stop native decoding on every engine. On .litertlm on Android, iOS and desktop the chat keeps working after a stop — whether the app called stopGeneration() or cancelled its subscription. With flutter_edge_ai_litertlm 1.15.0 and later it is rewound to before the stopped turn, which is put back with its partial reply, so the model still sees images and audio from earlier turns. With 1.14.0 and earlier, and on the NPU, it is rebuilt from text instead, and those are gone — re-send an image there if the next question is about it. The legacy flutter_gemma_litertlm package needs 1.8.1 or later for a stopped chat to answer again at all.

Multi-turn chat

dart
final InferenceChat chat = await model.createChat(
systemInstruction: 'You are a concise assistant.',
maxOutputTokens: 512,
);
try {
await chat.addQueryChunk(Message(text: prompt, isUser: true));
final reply = StringBuffer();
await for (final r in chat.generateChatResponseAsync()) {
switch (r) {
case TextResponse(:final token):
reply.write(token);
case ThinkingResponse() || FunctionCallResponse() || ParallelFunctionCallResponse():
break;
}
}
} finally {
await chat.close();
}

The chat keeps the history: add the next user message and generate again. generateChatResponse() returns the whole reply as one sealed ModelResponse — TextResponse, FunctionCallResponse, ParallelFunctionCallResponse or ThinkingResponse — and a switch over it must cover all four.

Two conversations at once

createSession and createChat fill a single slot on the model, so a second one closes the first. For concurrent conversations use openSession / openChat, and close each one.

They live only on the base class, so — unlike createChat — they inherit nothing from the installed model: pass modelType: (and supportImage: / supportAudio: if the chat sends images or audio) explicitly, or the chat runs as ModelType.gemmaIt with images and audio off. They work on .litertlm (native and web), on MediaPipe Android and iOS, on built-in AI, and on flutter_edge_ai_llama, whose openChat does take the installed model's type and image/audio support, as its createChat does; everywhere else — ONNX and MediaPipe web — they throw UnsupportedError.

dart
final summariser = await model.openChat(modelType: ModelType.gemma4);
final assistant = await model.openChat(modelType: ModelType.gemma4);
try {
await summariser.addQueryChunk(Message(text: chunk, isUser: true));
await assistant.addQueryChunk(Message(text: question, isUser: true));
} finally {
await summariser.close();
await assistant.close();
}

Thinking models

Gemma 4, Qwen3, DeepSeek R1 and .litertlm bundles with a thought channel (e.g. SmolLM3, Phi-4 Mini Reasoning) can emit reasoning on their supported engines. Pass enableThinking: true to createChat. Reasoning arrives as ThinkingResponse only from generateChatResponseAsync(); generateChatResponse() strips it. On a native .litertlm, reasoning that a bundle streams on its thought channel arrives as ThinkingResponse whatever the modelType, and enableThinking: false sends enable_thinking: false to the template — a Qwen3 template that reads the flag thinks when it is missing. With ModelType.qwen3, core also appends /no_think to text turns, the only off switch for a bundle whose template ignores the flag; Qwen3.5 and later take ModelType.qwen35 and no suffix. On Web, the .litertlm engine sends the same enable_thinking, and Gemma 4 E2B returns ThinkingResponse in Chrome; core also parses Qwen3's emitted <think> tags there. MediaPipe Web has no thinking API, ONNX Web ignores enableThinking, and DeepSeek R1 thinks on Android and iOS only — it ships as a mobile .task build.

dart
final chat = await model.createChat(enableThinking: true, modelType: ModelType.qwen3);
final answer = StringBuffer();
await chat.addQueryChunk(Message(text: question, isUser: true));
await for (final r in chat.generateChatResponseAsync()) {
switch (r) {
case ThinkingResponse(:final content):
print('reasoning: $content');
case TextResponse(:final token):
answer.write(token);
case FunctionCallResponse() || ParallelFunctionCallResponse():
break;
}
}
await chat.close();

Images

dart
final model = await FlutterEdgeAi.getActiveModel(maxTokens: 4096, supportImage: true);
final chat = await model.createChat(supportImage: true);
await chat.addQueryChunk(
Message(text: 'What is in this photo?', isUser: true, imageBytes: bytes),
);

On web, images need a MediaPipe .task build such as gemma-4-E2B-it-web.task (same Hugging Face repo as the .litertlm, ModelFileType.task, the flutter-edge-ai-mediapipe skill). On that build Gemma 4 has no thinking, and its tools never reach the model. The .litertlm web engine is text-only: supportImage: true loads without error, but addQueryChunk with an image throws UnsupportedError.

Audio

dart
final model = await FlutterEdgeAi.getActiveModel(maxTokens: 4096, supportAudio: true);
final chat = await model.createChat(supportAudio: true);
await chat.addQueryChunk(
Message(text: 'What is said in this recording?', isUser: true, audioBytes: bytes),
);

audioBytes is a whole WAV file — 16 kHz mono, header included. The speech package is the opposite: transcribe takes raw PCM with no header. Audio input needs Gemma 4 or Gemma 3n, on Android, iOS or desktop (VibeVoice-ASR also takes audio, for transcription only; measured on Android and macOS); on the .litertlm web engine an audio message throws UnsupportedError.

The .litertlm engine sends a Gemma 4 message as images, text, then audio, as the Gemma 4 model card asks, and every other model type as images, audio, then text (flutter_edge_ai 2.4.0 with flutter_edge_ai_litertlm 1.14.0; litertlm 1.13.x ignores multimodalOrder). A model whose chat template expects another order takes getActiveModel(multimodalOrder: MultimodalOrder.imagesAudioText) or MultimodalOrder.imagesTextAudio.

The model is a singleton

getActiveModel returns one model per process. Calling it again with the same arguments returns that model, on every platform (web since 2.2.1). Different runtime arguments, or another active model, rebuild it and close the previous one — a handle still held stops working; model.isClosed says so from the moment close() starts, and a getActiveModel call made meanwhile waits for the close to finish before it builds the next model. With no model installed it throws NoActiveModelError, a StateError. Load it once, then create and close sessions per conversation.

For several models loaded at once (one file on CPU and GPU, or two files), use FlutterEdgeAi.openModel(spec, …) (2.5.0, mobile and desktop) with LiteRtLmEngine (flutter_edge_ai_litertlm 1.16.0); other engines throw UnsupportedError. Install the file with install(activate: false) and pass the returned spec. Every call builds a new model that the app closes: it is never cached, a getActiveModel rebuild leaves it open, and the plugin has no memory policy (on iOS a process past its memory limit is killed, not thrown at). An activate: false install that would replace the active model's file throws StateError, and openModel refuses a spec that is not the installed one. Its parameters mean what they do on getActiveModel; a model's sampler is fixed by its first generation, so sessions sharing one model use the same sampling; uninstalling a model does not close the models opened on it. Measured on macOS (CPU and GPU engines on one file, two GPU engines) and on Windows Lunar Lake (CPU, GPU and NPU engines together). Not measured: Android, iOS, Linux, and two GPU engines on Windows, where a second one can fail with wgpu::Instance already set (https://flutteredge.ai/docs/desktop).

dart
final spec = (await FlutterEdgeAi.installModel(
modelType: ModelType.gemma4,
fileType: ModelFileType.litertlm,
).fromFile(path).install(activate: false)).spec;
final cpu = await FlutterEdgeAi.openModel(spec, preferredBackend: PreferredBackend.cpu);
final gpu = await FlutterEdgeAi.openModel(spec, preferredBackend: PreferredBackend.gpu);
// ... generate on either ...
await gpu.close();
await cpu.close();

Backends

dart
final model = await FlutterEdgeAi.getActiveModel(
maxTokens: 1024,
preferredBackend: PreferredBackend.gpu,
);
print(model.activeBackend); // what actually loaded
preferredBackendTried in order
null or gpuGPU, then CPU
npuGPU, then CPU: a generic bundle has no NPU graph, so the NPU is not tried (with backendFallback: false, a LiteRtLmBundleException)
cpuCPU only
any, for a bundle compiled for an NPUthe NPU only: null and npu run there, gpu and cpu are refused

The rows are for a generic bundle. A text model that declares backend_constraint is tried only on the backends it allows (with null, GPU then CPU, each only if it allows it). With LiteRtLmEngine(backendFallback: false) (flutter_edge_ai_litertlm 1.14.0) only the first backend of a row is tried, and a failure throws BackendInitException with LiteRT-LM's own reason.

Read activeBackend rather than assuming the requested one loaded; the web .litertlm engine reports null. PreferredBackend.npu needs a Snapdragon (Android), a Qualcomm Linux arm64 board, a Google Tensor G5/G6 Pixel (Android 16 or later, not a BP2A build), or Intel Lunar/Panther Lake (Windows), and a model compiled for that NPU; the Intel NPU needs flutter_edge_ai_litertlm 1.15.0 — 1.14.0, like every release since native-v0.17.0, fails its load or ends the process (0xC0000409); PreferredBackend.cpu never falls back. The iOS Simulator is CPU-only. On an iOS device a MediaPipe .task on the GPU is killed for memory while loading, before any fallback can run: pass PreferredBackend.cpu for it (flutter-edge-ai-mediapipe). On web, MediaPipe is GPU-only.

On NPU, run a Gemma 4 bundle. Most Gemma 3 bundles on the Qualcomm or the Intel NPU (Google Tensor not measured) drop every prefill chunk after the first — no error, no log line, and a fluent reply that answers from the opening of the prompt and ignores the rest (LiteRT-LM#3508; of the published Qualcomm 1B files only …_sm8850 keeps them). maxTokens is also not clamped up to 1024 on the NPU attempt the way it is on CPU and GPU, because the safe context is baked into the compiled bundle: pass the cache_length it was built for. A bundle compiled for no NPU never runs on one, whatever its backend_constraint says: it has no NPU graph, and LiteRT would try to compile it on the device with a compiler plugin, which no NPU stack here ships, and run it on the CPU while reporting npu — so PreferredBackend.npu falls back to GPU, then CPU, for it (refused with backendFallback: false), and the floor applies to those attempts. A bundle compiled for an NPU (…_qualcomm_sm8750.litertlm, read from the file before any engine exists) runs on the NPU only: it is the default when preferredBackend is unset, gpu/cpu throw LiteRtLmBundleException, and an unreachable NPU throws BackendInitException at once. Unset preferredVisionBackend/preferredAudioBackend follow the file (the encoder's declared backend_constraint, the CPU when it allows the CPU; else the NPU it is compiled for; else CPU): Gemma 4 vision and audio run on the CPU, Gemma 3n vision on the GPU, because its section allows gpu only. Set one only where the section allows it (the published Gemma 4 and Gemma 3n audio encoders declare cpu, so gpu is refused; preferredVisionBackend: cpu is refused on Gemma 3n), or the load throws LiteRtLmBundleException; read activeVisionBackend/activeAudioBackend (filled by flutter_edge_ai_litertlm 1.14.0, null when the model has no such encoder). Its problem is wrongBackend for a backend the file rules out, incomplete for a download that stopped early, and unsupportedNpu for a text model, or an encoder that is on, compiled for an NPU this package has no dispatch for here (MediaTek, a Qualcomm bundle on Windows, a Google Tensor bundle off Android), for sections the load turns on that are compiled for two different NPUs, or for an NPU encoder next to a text model that is not on the NPU. LiteRtLmEngine(backendFallback: false) (1.14.0) makes one attempt and reports LiteRT-LM's reason (LiteRtLmEngineCreateException.nativeMessage); readLiteRtLmBundleInfo(path) (1.14.0) shows what the file declares. The NPU candidate is attempted only for a bundle compiled for an NPU, and only on Windows, on Android phones with Qualcomm FastRPC, on Qualcomm Linux arm64 boards with /dev/fastrpc-cdsp, and on Google Tensor G5/G6 Pixels with Android 16 or later (not a BP2A build; LiteRT itself refuses a bundle of the other Tensor generation when the engine is created), the last three only when the app sets qualcomm_npu or google_tensor_npu; on other Android phones, macOS, other Linux machines and iOS it is skipped, because nothing there can run it — and on macOS the native runtime was measured accepting npu anyway, which made activeBackend report an NPU the machine does not have. Windows has no device probe: a bundle compiled for the Intel NPU is not refused on a PC without one, and what LiteRT does then is not measured.

On GPU, Gemma 4 can copy numbers wrongly from a long prompt: 2026/06/23 comes back as 20226/12/17, the same way on every run (LiteRT-LM#3012 on Adreno, #2814 on Metal). The published Gemma 4 .litertlm files ask for half-precision activations. Ask for full precision when the answers carry figures, dates or amounts:

dart
final model = await FlutterEdgeAi.getActiveModel(
preferredBackend: PreferredBackend.gpu,
activationDataType: ActivationDataType.float32,
);

Prefill gets slower (about 3× on a Snapdragon 8 Elite and an iPhone 11, under 1.5× on an Apple M3 Max); decode speed barely changes. Left unset, the model file decides, with one exception: a FunctionGemma bundle that declares no prefer_activation_type gets float32 on its GPU attempt, because at float16 it answers with nothing. A plain Gemma 3 270M (ModelType.gemmaIt) answers with <pad> on the GPU too (LiteRT-LM#3280) but is not covered: pass float32 for it. A value you pass always wins. It applies to the text decoder of .litertlm models on Android, iOS and desktop — not to the vision or audio encoders, which keep what the model file asks for; MediaPipe, ONNX, built-in AI and the web engines ignore it entirely.

float32 needs more GPU memory than the default, and by default a GPU engine that cannot be created falls back to CPU without an exception, only a WARNING line in the log — right digits, a much slower run; with LiteRtLmEngine(backendFallback: false) the load throws BackendInitException instead. After loading, check model.activeBackend == PreferredBackend.gpu before concluding the setting did anything. On Android the GPU shares system memory, so on a 4–6 GB phone running out of it at float32 can end the app rather than fall back to CPU. Both precisions share one compiled GPU program cache per model, so switching recompiles the GPU programs (about 600 MB for Gemma 4 E2B): pick one precision per install rather than per request. Every flutter_edge_ai_litertlm release supports the setting; the legacy flutter_gemma_litertlm package needs 1.8.3 or later.

Platform setup

Android needs minSdk 30 and the internet permission in release builds, and ships arm64-v8a only. iOS needs Podfile or Xcode settings and memory entitlements; macOS needs entitlements and a Podfile build phase; web needs script tags in web/index.html. Read references/platform-setup.md before building for any of them — without those entries the model fails to load or the app runs out of memory.

All versionsv1.0.1 →