openvino_genai 0.2.0
openvino_genai: ^0.2.0 copied to clipboard
On-device LLM and vision-language (VLM) inference for Flutter via Intel OpenVINO GenAI: streaming chat and image description on CPU/GPU, hardened load/fallback. Windows x64; unofficial.
openvino_genai #
On-device LLM and vision-language (VLM) inference for Flutter via Intel OpenVINO™ GenAI: streaming chat completion and image description on CPU and GPU, with the load/fallback hardening a heterogeneous Windows fleet actually needs.
Unofficial. Not affiliated with, endorsed by, or supported by Intel Corporation. "OpenVINO" is a trademark of Intel Corporation or its subsidiaries.
Platform support #
| Platform | Status |
|---|---|
| Windows x64 | ✅ via openvino_genai_windows |
What you get #
-
Streaming chat completion through the OpenVINO GenAI C API. Messages go to the native side as a chat history and the model's own chat template is applied there — no hand-formatted prompt markup.
-
Device-ladder loading with a smoke test. Loads walk an ordered device ladder — default
['GPU', 'CPU']; setOpenVinoGenAiService.devicePreference = ['NPU', 'GPU', 'CPU']for the full chain.ov_bridge_initcan report success on machines whose accelerator then silently produces zero tokens; a tiny probe generation catches that, the failing device is struck out for the process lifetime, and the model transparently reloads on the next rung. Opt out withOpenVinoGenAiService.acceleratorSmokeTestEnabled = false. Devices that don't enumerate on the machine are skipped before any load is attempted (SR-IOV VMs, for example, expose no NPU at all).The NPU rung is experimental — validate before enabling. It needs the platform package built with
OPENVINO_GENAI_BUNDLE_FULL=ON(the default bundle omits Intel's 72 MB NPU compiler), and OpenVINO's NPU plugin has been observed to fail-fast the whole process (no catchable error) on incompatible model/driver combinations even when the device is present. Only enable it for combinations you have actually seen work. -
Hardened lifecycle. Blocking native calls (model load/free) run in a background isolate; loads have a caller-set wait budget with a detached self-healing load behind it; generation timeouts measure stall (time since the last token), so slow-but-alive CPUs are never killed mid-answer.
-
Typed failures (
OpenVinoGenAiExceptionhierarchy) instead of bareExceptions, anddoctor()for one-call diagnostics.
Quick start #
dependencies:
openvino_genai: ^0.2.0
import 'package:openvino_genai/openvino_genai.dart';
final llm = OpenVinoGenAiService();
// An OpenVINO IR model directory: openvino_model.xml/.bin + tokenizer files.
await llm.loadModel(r'C:\models\qwen2.5-1.5b-instruct-int4-ov');
await for (final token in llm.generateChatStream([
const LlmMessage.system('You are a helpful assistant.'),
const LlmMessage.user('Why is the sky blue?'),
])) {
stdout.write(token);
}
Non-streaming: generateText(...). Cancel a running generation by cancelling
the stream subscription, or via cancelGeneration().
Models #
Any OpenVINO IR LLM export works — e.g. the pre-converted models in
OpenVINO's Hugging Face collections, or your
own export via optimum-intel. Point loadModel at the directory containing
openvino_model.xml.
Sampling #
llm.generateChatStream(
messages,
params: const LlmGenerationParams(maxTokens: 512, temperature: 0.4, topP: 0.9),
);
LlmGenerationParams.defaults is a balanced profile; .greedy is
deterministic decoding. temperature: 0.0 always means greedy.
Vision (VLM) #
OpenVinoVlmService describes an image for a prompt through OpenVINO GenAI's
VLM pipeline, on its own independent native lane (own DLL, lock, thread and
wedge latch) — it can run concurrently with the LLM service on the one loaded
runtime.
import 'package:image/image.dart' as img; // your choice of decoder
final vlm = OpenVinoVlmService();
await vlm.loadModel(r'C:\models\internvl2-1b-int4-ov'); // an optimum-intel VLM export
// The package takes RGB8 pixels only: decode, convert and resize yourself.
final decoded = img.decodeImage(await File('photo.jpg').readAsBytes())!;
final resized = img.copyResize(decoded, width: 1024); // keep the longest edge <= ~1024
final image = VlmImage.rgb8(
bytes: resized.getBytes(order: img.ChannelOrder.rgb),
width: resized.width,
height: resized.height,
);
final text = await vlm.describeImage(
image,
prompt: 'Describe this picture.',
params: const LlmGenerationParams(maxTokens: 128, temperature: 0, topP: 1),
);
describeImageStream streams chunks; cancelling the subscription cancels the
generation — but note cancellation is only observed at decode time: the
vision encode and prompt prefill emit nothing and cannot be interrupted
(10–30 s for a 640×480 frame on a desktop CPU, longer on slow machines).
lastTimeToFirstToken / lastGenerateDuration let an app size its own
budgets and "still looking" cues from measurements.
- RGB8 contract.
bytes.length == width * height * 3, interleaved R,G,B, no alpha, no row padding, edges 1..16384, ≤ 256 MiB — validated byVlmImage.rgb8(throwsVlmImageException). Camera sources are usually BGR: swap channels in your capture layer. No resize/normalisation happens in the package; the pipeline's own preprocessor tiles the image, so bigger images cost proportionally more encode time. - CPU-first.
OpenVinoVlmService.devicePreferencedefaults to['CPU']: an iGPU shared with the app's renderer can trip the Windows display watchdog during a long vision encode.['GPU', 'CPU']is opt-in for validated fleets, with the same enumeration guard and smoke test as the LLM lane; accelerator failures demote and throw (no automatic reload-retry — a VLM retry costs tens of seconds). - Unloading.
unloadModel()cancels, waits for the thread on the Dart side, then frees natively with a bounded join. SizeOpenVinoVlmService.unloadJoinBudget(default 180 s) from your measured describe durations and prefer letting a description finish before unloading; a thread that ignores cancellation for the whole budget latches the lane wedged for the process lifetime (VlmEngineWedgedException— advise a restart). The LLM lane is unaffected by a VLM wedge.
The rules of the road (0.x) #
Each native pipeline is a process-wide singleton: one LLM and one VLM
resident at a time, one generation per lane at a time. Loading a different
model swaps the resident one out; a second concurrent generation on the same
lane throws LlmBusyException / VlmBusyException. A handle-based API is
planned for 1.0.
One runtime pin per process. This package's platform implementation pins one exact OpenVINO GenAI runtime (see its README). Never ship a second copy of the OpenVINO runtime DLLs in the same app: Windows caches DLL modules by file name, so two copies at different versions silently cross-bind and fail in undebuggable ways.
Custom DLL locations #
By default the bridge DLL and runtime are bundled next to your .exe by the
Flutter build. If your installer relocates them:
OpenVinoBridgeBindings.dllPathOverride = r'C:\MyApp\engine\openvino_genai_bridge.dll';
OpenVinoVlmBridgeBindings.dllPathOverride = r'C:\MyApp\engine\openvino_genai_vlm_bridge.dll';
Both bridge DLLs and the whole runtime DLL set must sit in that same directory — one copy of the runtime per process.
Diagnostics #
final report = await OpenVinoGenAiService().doctor();
print(report); // DLL path, bridge version, runtime pin, pipeline state
print(await OpenVinoVlmService().doctor()); // same shape for the VLM lane
Route the package's log lines into your logger with
openVinoGenAiLogger = (line) => ...;.
License #
Apache-2.0. The OpenVINO runtime bundled by the platform package is
© Intel Corporation, Apache-2.0 — see the openvino_genai_windows package for
redistribution notices.