betto_inferencing
1 betto_inferencing
ONNX Runtime inference and embedding models for dense text retrieval. Part of the Bettongia open-source family.
Provides OnnxEmbeddingModel backed by either BGE Small En
v1.5 (English-only) or multilingual-e5-small
(cross-lingual) via betto_onnxrt,
a ModelTokenizer abstraction over BERT WordPiece and
XLM-RoBERTa-family SentencePiece/Unigram tokenization, SQ8 vector
quantization helpers, and a validated model catalog with
download-on-demand.
1.1 Features
OnnxEmbeddingModel— loads an embedding model and embeds text into L2-normalised float32 vectors. Supports download-on-demand (SHA-256 verified) or loading from an explicit file path. Accepts an optionaltokenizerparameter to swap in a custom word segmentation backend for BERT-family models (e.g.IcuTokenizerfrompackage:betto_icu). Selects the concreteModelTokenizerimplementation viaModelSpec.meta['tokenizerFamily'].EmbeddingModel— abstract interface, allowing the consuming application or database to depend on the interface without the FFI-heavy implementation.embed(text, {kind})takes anEmbeddingKind(documentorquery, defaultdocument) so models with a mandatory passage/query prefix convention (e.g.multilingual-e5-small) apply the right one automatically.EmbeddingKind—document/query. SelectsModelSpec.meta’s'documentPrefix'/'queryPrefix', if the loaded model defines them. A no-op for models with neither key (e.g.bge-small-en-v1.5).ModelTokenizer— shared interface implemented byBertTokenizerandXlmRobertaTokenizer, lettingOnnxEmbeddingModelselect either at runtime from a singleencode(String) -> TokenizerOutputcall site.BertTokenizer— BERT WordPiece tokenizer loaded from avocab.txtfile. Normalises, segments, and assembles[CLS]/[SEP]-padded token ID sequences ready for ORT inference. Word segmentation delegates to abetto_lexicalTokenizer; defaults toRegExpTokenizerand acceptsIcuTokenizeras a drop-in replacement.TokenizerOutput— value type returned byBertTokenizer.encodeandXlmRobertaTokenizer.encode. CarriesinputIds,attentionMask, andtokenTypeIdsas parallelInt64Listarrays, plus atruncatedflag.XlmRobertaTokenizer— XLM-RoBERTa-family SentencePiece/Unigram tokenizer (e.g.multilingual-e5-small), loaded from a HuggingFacetokenizer.jsonfile. See “Why notdart_sentencepiece_tokenizeralone” below for why this isn’t a thin wrapper around that package.ModelCatalog— allowlist of validated embedding models with download-on-demand gating.ModelCatalog.lookup(id)returns theModelSpecfor a validated model;ModelCatalog.isKnown(id)checks registration without validating;ModelCatalog.defaultModelIdis'bge-small-en-v1.5'. Currently validated:bge-small-en-v1.5(384-d, English) andmultilingual-e5-small(384-d, ~100 languages).placeholder-modelis a permanent, never-validated internal test fixture (not a real model — see its doc comment inlib/src/model_catalog.dart).quantise/dequantise— SQ8 (scalar 8-bit) vector quantization helpers for compact storage of embedding indexes.- Re-exports from
betto_onnxrt:ModelDownloader,ModelSpec,ModelFile,ResolvedModel,DownloadProgress.
1.2 Platform support
This package is native-only. It must not be imported on the web platform.
| Platform | Support | Notes |
|---|---|---|
| macOS | arm64 only | Intel (x86_64) is not supported |
| Linux | x64, aarch64 | |
| Windows | x64, arm64 | |
| Android | arm64-v8a, armeabi-v7a, x86_64, x86 | Requires minSdkVersion 35 |
| iOS | arm64 | Requires the betto_onnxrt_ios companion plugin (iOS ≥
16) |
| Web | Not supported |
The ONNX Runtime shared library is staged at build time by the
betto_onnxrt native-assets build hook — no manual download
or bundling is required.
1.2.1 iOS setup
iOS ORT support requires the betto_onnxrt_ios Flutter
plugin, which statically links the ORT XCFramework into the host app via
SPM. No CocoaPods changes are needed. Add it alongside
betto_inferencing:
dependencies:
betto_inferencing: ^0.1.0-dev.3
betto_onnxrt_ios: ^0.1.0-dev.1Requires Flutter ≥ 3.27.0 and iOS ≥ 16.
1.2.2 Android
Set minSdkVersion to at least 35 in
android/app/build.gradle:
android {
defaultConfig {
minSdk = 35
}
}1.3 Installation
dependencies:
betto_inferencing: ^0.1.0-dev.3Note:
betto_inferencinguses native-assets build hooks viabetto_onnxrt. Rundart test(orflutter test) from inside the package directory (not the workspace root) so the hook fires and the ORT binary is placed correctly.
1.4 Usage
1.4.1 Embed text (download-on-demand)
import 'package:betto_inferencing/betto_inferencing.dart';
final model = await OnnxEmbeddingModel.load(
cacheDir: '/path/to/model/cache',
onProgress: (received, total) {
final pct = total > 0 ? (received * 100 ~/ total) : 0;
print('Downloading: $pct% ($received / $total bytes)');
},
);
try {
final (embedding, truncated) = await model.embed('semantic search query');
print('Dimensions: ${embedding.length}'); // 384
print('Truncated: $truncated');
} finally {
model.dispose();
}On the first call ModelDownloader fetches and
SHA-256-verifies the BGE Small En v1.5 model (~127 MB). Subsequent calls
reuse the cached files.
Note:
OnnxEmbeddingModel.loadthrowsArgumentErrorif neithermodelPathnorcacheDiris supplied — there is no bundled model asset.
1.4.2 Embed text (explicit path)
final model = await OnnxEmbeddingModel.load(
modelPath: '/path/to/model.onnx',
);1.4.3 Implement the interface
EmbeddingModel decouples the consuming application from
this package’s FFI dependency:
import 'package:betto_inferencing/betto_inferencing.dart';
class MyRetriever {
MyRetriever(this._model);
final EmbeddingModel _model;
Future<List<double>> queryVector(String text) async {
final (embedding, _) = await _model.embed(text);
return embedding.toList();
}
}1.4.4 Look up a catalog model
// Lookup throws if the model is unknown or not yet validated.
final spec = ModelCatalog.lookup('bge-small-en-v1.5');
print(spec.meta['dimensions']); // 384
// Check registration without validation gating.
print(ModelCatalog.isKnown('placeholder-model')); // true (never validated)
// Iterate all registered models (validated and not yet validated).
for (final spec in ModelCatalog.all) {
print('${spec.id} — ${spec.meta['dimensions']}d');
}
// bge-small-en-v1.5 — 384d
// multilingual-e5-small — 384d
// placeholder-model — 0d1.4.5 Cross-lingual embedding with
multilingual-e5-small
final spec = ModelCatalog.lookup('multilingual-e5-small');
final model = await OnnxEmbeddingModel.load(
spec: spec,
cacheDir: '/path/to/model/cache',
);
try {
// Index-time text: EmbeddingKind.document (the default) applies E5's
// mandatory "passage: " prefix automatically.
final (docEmbedding, _) = await model.embed(
'The cat sat on the mat.',
kind: EmbeddingKind.document,
);
// Query-time text in a different language: EmbeddingKind.query applies
// "query: " instead.
final (queryEmbedding, _) = await model.embed(
'Le chat était assis sur le tapis.',
kind: EmbeddingKind.query,
);
} finally {
model.dispose();
}1.4.6 SQ8 quantization
import 'package:betto_inferencing/betto_inferencing.dart';
final (embedding, _) = await model.embed('hello world');
final quantised = quantise(embedding); // Uint8List, 384 bytes
final restored = dequantise(quantised); // Float32List1.5 Examples
The example/ directory contains runnable examples that
walk through each feature in the package. Set BETTO_CACHE
to a persistent directory so the model (~127 MB) is downloaded only once
across all examples:
export BETTO_CACHE=$HOME/.cache/betto_examples| File | What it shows | Download required |
|---|---|---|
example.dart |
Basic load + single embed call with
download-on-demand |
Yes |
model_catalog.dart |
List all registered models, inspect ModelSpec metadata,
isKnown, and error handling for unknown / unvalidated
IDs |
No |
tokenizer.dart |
BertTokenizer standalone: encode, token ID
inspection, decode, WordPiece sub-token splitting,
truncation detection |
Yes (vocab only) |
embed_and_compare.dart |
Embed a query and several documents, rank by cosine similarity (dot product of L2-normalised vectors) | Yes |
sq8_quantisation.dart |
quantise/dequantise round-trip: 4× storage
reduction, per-element reconstruction error, similarity score
preservation |
Yes |
Run any example with:
dart run example/<name>.dart1.6 Models
| Model ID | Dimensions | Language | Status |
|---|---|---|---|
bge-small-en-v1.5 |
384 | English | ✅ Validated (~127 MB download) |
multilingual-e5-small |
384 | ~100 languages | ✅ Validated (~470 MB download) |
placeholder-model |
— | — | Internal test fixture, never valid |
multilingual-e5-small requires a
"passage: " / "query: " text prefix depending
on whether the text is being indexed or queried — pass the matching
EmbeddingKind to EmbeddingModel.embed.
bge-small-en-v1.5 has no prefix convention, so
EmbeddingKind is a no-op for it (safe to omit).
Registering bge-m3 (BAAI’s larger, 1024-dimensional
multilingual model) is tracked as future work, not yet in this catalog —
its ONNX export exceeds the 2 GB single-file limit and needs
ModelSpec/ModelDownloader support for a split
model.onnx + model.onnx_data layout first.
1.7 Why not
dart_sentencepiece_tokenizer alone
XlmRobertaTokenizer depends on dart_sentencepiece_tokenizer
(MIT licensed — compatible with this project’s Apache-2.0 license; no
NOTICE entry is needed for using it as a normal dependency)
for vocabulary loading, Unigram Viterbi decoding, and BOS/EOS
post-processing, but cannot use it as-is for the normalization step. Two
independent defects were found in its HuggingFace
tokenizer.json-loading path while integrating
multilingual-e5-small (full investigation: plan_0_06_wi11_xlmr_tokenizer.md
in the kmdb repository):
- It never applies the
Precompiledcharsmap normalizer on the JSON loading path.tokenizer.json’snormalizerfield is aSequencewhose first entry has"type": "Precompiled"and aprecompiled_charsmapkey holding a base64-encoded Darts double-array trie (the substitution table SentencePiece bakes into a trained model — fullwidth-to-ASCII folding, ellipsis expansion, etc.; not plain NFKC).dart_sentencepiece_tokenizer’shuggingface_json.dartparses this section’s other flags correctly but never passes aprecompiledCharsmapvalue through — it is silently dropped. (The charsmap data itself is populated elsewhere, but only on the separate native.modelprotobuf loading path, which this project does not use — see the plan’s Investigation section for the exact source lines.) No existing Dart implementation of this trie format could be found anywhere, so this package implements its own:CharsmapTrie(lib/src/charsmap_trie.dart), whose traversal algorithm structure is ported from the Rustspm_precompiledcrate — seeNOTICEfor attribution. - Its HuggingFace-JSON metadata parser also mis-derives
whitespace/dummy-prefix configuration for
tokenizer.jsonfiles that put it inpre_tokenizerrather thannormalizer.multilingual-e5-small’stokenizer.jsondeclares its dummy-prefix and whitespace-escaping behaviour entirely via apre_tokenizer.Metaspaceentry ({"type": "Metaspace", "replacement": "▁", "add_prefix_space": true}), which the parser never reads (it only inspectsnormalizer) — so all three of its derived flags come backfalse, making its ownSpNormalizeran unconditional pass-through for this file. This is a second, independent defect from the charsmap-drop above, found during this package’s own investigation rather than documented upstream.
XlmRobertaTokenizer works around both by composing its
own charsmap-substitution, whitespace-collapse, and Metaspace-escaping
steps before handing already-normalized text to
dart_sentencepiece_tokenizer’s public encode()
— composition, not a fork: no dart_sentencepiece_tokenizer
source is modified. If either upstream defect is fixed in a future
release, this package’s manual steps become redundant but harmless
(idempotent no-ops on already-correct input) — no urgency to remove
them, though doing so is a reasonable future cleanup.
1.8 License
Apache 2.0 — see LICENSE. Third-party attribution for ported algorithm structure is recorded in NOTICE.