1 # Onde Inference — AGENTS.md
2
3 > **AI agent reference for the `onde` repository and the `onde-swift` Swift SDK.**
4 > Keep this file accurate as the codebase evolves.
5
6 ---
7
8 ## What Is Onde?
9
10 Onde is an **on-device LLM inference SDK family** built around a shared Rust core. It targets Apple silicon (iOS, tvOS, macOS) plus Android, Windows, and Linux, and ships SDKs for Rust, Swift, Kotlin Multiplatform, Flutter, and React Native. It wraps [mistral.rs](https://github.com/EricLBuehler/mistral.rs) behind platform-friendly APIs with automatic model selection, HuggingFace Hub downloads, cache management, and Metal acceleration where available.
11
12 - **Website:** https://ondeinference.com
13 - **Rust crate:** https://crates.io/crates/onde
14 - **Swift package:** https://github.com/ondeinference/onde-swift
15 - **Maven Central:** https://central.sonatype.com/artifact/com.ondeinference/onde-inference
16 - **Flutter package:** https://pub.dev/packages/onde_inference
17 - **React Native package:** https://www.npmjs.com/package/@ondeinference/react-native
18 - **In production:** [Splitfire AB apps on the Apple AppStore](https://apps.apple.com/se/developer/splitfire-ab/id1831430993)
19
20 ---
21
22 ## Repository Layout
23
24 ```
25 onde/
26 ├── src/
27 │ ├── lib.rs # Crate root — uniffi::setup_scaffolding!()
28 │ ├── hf_cache.rs # HuggingFace Hub cache: list, download, delete, diagnose, repair
29 │ └── inference/
30 │ ├── mod.rs # Module exports + top-level re-exports
31 │ ├── engine.rs # ChatEngine — Rust-native API (generics, mpsc, tool calling)
32 │ ├── ffi.rs # OndeChatEngine — UniFFI Object (FFI-safe, Arc-wrapped)
33 │ ├── models.rs # Model ID constants + SupportedModelInfo metadata
34 │ ├── token.rs # HF token resolution: build-time literal vs cache file
35 │ └── types.rs # Shared types: ChatMessage, SamplingConfig, InferenceResult, ToolCallInfo, etc.
36 ├── sdk/
37 │ ├── dart/ # Flutter/Dart package + example app + FRB Rust bridge
38 │ ├── gem/ # Ruby native extension (Magnus)
39 │ ├── kotlin/ # Kotlin Multiplatform package (Android + JVM)
40 │ ├── react-native/ # Expo module wrapping the Rust core for iOS/Android
41 │ └── python/ # Python bindings (maturin + uniffi)
42 ├── generated/ # UniFFI-generated headers and Swift glue (git-ignored)
43 ├── uniffi-bindgen/ # Standalone bindgen binary crate (pinned uniffi =0.31.0)
44 ├── .github/
45 │ ├── workflows/
46 │ │ ├── release-sdk-swift.yml # CI: tag push → build XCFramework → GitHub Release → update onde-swift
47 │ │ ├── release-sdk-kotlin.yml # CI: tag push → build Android/JVM artifacts → publish to Maven Central
48 │ │ ├── release-sdk-dart.yml # CI: tag push → publish Flutter package to pub.dev
49 │ │ ├── release-sdk-npm.yml # CI: tag push → publish React Native package to npm
50 │ │ └── release-sdk-rust.yml # CI: tag push → publish Rust crate to crates.io
51 │ └── scripts/
52 │ └── build-swift-xcframework.sh # Local/CI XCFramework assembly script
53 ├── .cargo/config.toml # Target-specific rustflags (fp16, linker overrides)
54 ├── Cargo.toml # Platform-conditional mistralrs deps
55 ├── build.rs # tvOS ___chkstk_darwin assembly stub
56 ├── scripts/
57 │ └── tvos_chkstk.s # No-op arm64 stub for missing tvOS symbol
58 ├── uniffi.toml # UniFFI binding config
59 └── docs/
60 ├── dev.md # Developer guide (build, architecture, platform table)
61 ├── swift-package.md # Swift API reference + XCFramework build steps
62 ├── distribution.md # Release process for all registries
63 └── ruby-gem.md # Ruby gem API reference
64 ```
65
66 ```
67 onde-swift/ # Swift Package Manager wrapper repo
68 ├── Package.swift # Declares OndeFramework.xcframework binary target
69 └── Sources/
70 └── Onde/
71 └── onde.swift # UniFFI-generated Swift glue (do NOT edit manually)
72 ```
73
74 ---
75
76 ## Architecture
77
78 ### Layer Diagram
79
80 ```
81 ┌──────────────────────────────────────────────────────────┐
82 │ Swift (iOS / tvOS / macOS) Rust app / CLI / server │
83 │ import Onde use onde::inference::* │
84 └──────────┬───────────────────────────────┬───────────────┘
85 │ UniFFI FFI bindings │ Direct Rust API
86 ▼ ▼
87 ┌──────────────────────────┐ ┌─────────────────────────────┐
88 │ OndeChatEngine │ │ ChatEngine │
89 │ (ffi.rs — uniffi::Object│──│ (engine.rs — Rust-native) │
90 │ FFI-safe, Arc<Self>) │ │ generics, mpsc channels │
91 └──────────┬───────────────┘ └─────────────┬───────────────┘
92 │ │
93 └────────────────┬───────────────┘
94 ▼
95 ┌────────────────────────┐
96 │ mistralrs::Model │
97 │ GgufModelBuilder │
98 │ Metal / CUDA / CPU │
99 └────────────────────────┘
100 ```
101
102 ### Key Design Rules
103
104 1. **`ChatEngine`** (`engine.rs`) owns all Rust-idiomatic logic: `impl Into<String>`, `tokio::sync::mpsc::Receiver`, etc. Never add UniFFI annotations here.
105 2. **`OndeChatEngine`** (`ffi.rs`) is a thin `Arc`-wrapped UniFFI `Object` with concrete, FFI-safe signatures. It delegates every call to the inner `ChatEngine`.
106 3. **`StreamChunkListener`** is a `#[uniffi::export(callback_interface)]` trait — it must be passed as a parameter to **free functions** (not Object methods) because UniFFI 0.31 does not support callback_interface on Object methods.
107 4. **Free functions** in `ffi.rs` (e.g. `stream_chat_message`, `default_model_config`, `user_message`) are exported with `#[uniffi::export]` and generate top-level Swift functions.
108 5. **`uniffi::setup_scaffolding!()`** lives in `lib.rs`. Never call `uniffi::generate_scaffolding!()` from `build.rs` for the proc-macro path.
109
110 ---
111
112 ## Platform Support
113
114 | Platform | `target_os` | GPU Backend | Default Model | mistralrs features |
115 | -------- | ----------- | ----------------- | ------------- | ------------------ |
116 | macOS | `macos` | Metal | Qwen 2.5 Coder 3B (~1.93 GB) | `["metal"]` |
117 | iOS | `ios` | Metal | Qwen 2.5 Coder 1.5B (~941 MB) | `["metal"]` |
118 | tvOS | `tvos` | Metal | Qwen 2.5 Coder 1.5B (~941 MB) | `["metal"]` |
119 | Android | `android` | CPU (candle) | Qwen 2.5 Coder 1.5B (~941 MB) | `[]` + hf-hub |
120 | Windows | `windows` | CPU (CUDA in CI) | Qwen 2.5 Coder 3B (~1.93 GB) | `[]` |
121 | Linux | `linux` | CPU (CUDA in CI) | Qwen 2.5 Coder 3B (~1.93 GB) | `[]` |
122
123 - **Current default routing:** `platform_default()` now prefers the Coder variants, 1.5B on mobile and 3B on desktop.
124 - **iOS / tvOS memory constraint:** iOS gives apps about 2–3 GB. The 3B model (~1.93 GB) can still cause OOM on constrained devices, so mobile defaults stay on the 1.5B model.
125 - **tvOS tier-3 target:** requires `cargo +nightly -Z build-std`. Stable toolchain cannot build tvOS targets.
126 - **Android `hf_hub`:** `dirs::home_dir()` panics in the Android sandbox. `hf-hub` is added as an explicit dependency on Android so `HF_HOME` can be seeded programmatically via `hf_hub::api::tokio::ApiBuilder`.
127
128 ---
129
130 ## Supported Models
131
132 All model constants live in `src/inference/models.rs`. When adding a new model:
133
134 1. Add `pub const` entries for the HF repo ID, GGUF filename, and, on Android, `TOK_MODEL_ID` when needed.
135 2. Add the repo ID to `SUPPORTED_MODELS` so `list_local_hf_models` filters it.
136 3. Add a `SupportedModelInfo` entry to `SUPPORTED_MODEL_INFO` with accurate `expected_size_bytes` from the HF API `siblings[].size`.
137 4. Add a constructor to `GgufModelConfig` in `engine.rs`.
138 5. Export a free function in `ffi.rs` for UniFFI consumers when the model should be reachable from Swift and Kotlin.
139 6. If the GGUF does not ship with a built-in chat template, set `chat_template` explicitly. DeepSeek Coder is the current example.
140
141 ### Current Models
142
143 | Model | Repo | File | Size | Platforms |
144 |-------|------|------|------|-----------|
145 | Qwen 2.5 1.5B Instruct (GGUF Q4_K_M) | `bartowski/Qwen2.5-1.5B-Instruct-GGUF` | `Qwen2.5-1.5B-Instruct-Q4_K_M.gguf` | ~941 MB | All platforms |
146 | Qwen 2.5 3B Instruct (GGUF Q4_K_M) | `bartowski/Qwen2.5-3B-Instruct-GGUF` | `Qwen2.5-3B-Instruct-Q4_K_M.gguf` | ~1.93 GB | All platforms |
147 | Qwen 2.5 Coder 1.5B Instruct (GGUF Q4_K_M) | `bartowski/Qwen2.5-Coder-1.5B-Instruct-GGUF` | `Qwen2.5-Coder-1.5B-Instruct-Q4_K_M.gguf` | ~941 MB | All platforms (mobile default) |
148 | Qwen 2.5 Coder 3B Instruct (GGUF Q4_K_M) | `bartowski/Qwen2.5-Coder-3B-Instruct-GGUF` | `Qwen2.5-Coder-3B-Instruct-Q4_K_M.gguf` | ~1.93 GB | All platforms (desktop default) |
149 | Qwen 2.5 Coder 7B Instruct (GGUF Q4_K_M) | `bartowski/Qwen2.5-Coder-7B-Instruct-GGUF` | `Qwen2.5-Coder-7B-Instruct-Q4_K_M.gguf` | ~4.4 GB | Higher-memory devices |
150 | Qwen 3 0.6B (GGUF Q4_K_M) | `bartowski/Qwen_Qwen3-0.6B-GGUF` | `Qwen_Qwen3-0.6B-Q4_K_M.gguf` | ~0.5 GB | All platforms |
151 | Qwen 3 1.7B (GGUF Q4_K_M) | `bartowski/Qwen_Qwen3-1.7B-GGUF` | `Qwen_Qwen3-1.7B-Q4_K_M.gguf` | ~1.3 GB | All platforms |
152 | Qwen 3 4B (GGUF Q4_K_M) | `bartowski/Qwen_Qwen3-4B-GGUF` | `Qwen_Qwen3-4B-Q4_K_M.gguf` | ~2.7 GB | All platforms |
153 | Qwen 3 8B (GGUF Q4_K_M) | `bartowski/Qwen_Qwen3-8B-GGUF` | `Qwen_Qwen3-8B-Q4_K_M.gguf` | ~5 GB | Higher-memory devices |
154 | Qwen 3 14B (GGUF Q4_K_M) | `bartowski/Qwen_Qwen3-14B-GGUF` | `Qwen_Qwen3-14B-Q4_K_M.gguf` | ~8.4 GB | Higher-memory devices |
155 | Qwen 3 32B (GGUF Q4_K_M) | `bartowski/Qwen_Qwen3-32B-GGUF` | `Qwen_Qwen3-32B-Q4_K_M.gguf` | ~19.8 GB | High-memory desktop (32+ GB) |
156 | Qwen 3 4B Instruct 2507 (GGUF Q4_K_M) | `bartowski/Qwen_Qwen3-4B-Instruct-2507-GGUF` | `Qwen_Qwen3-4B-Instruct-2507-Q4_K_M.gguf` | ~2.5 GB | All platforms (latest non-thinking 4B) |
157 | Qwen 3 4B Thinking 2507 (GGUF Q4_K_M) | `bartowski/Qwen_Qwen3-4B-Thinking-2507-GGUF` | `Qwen_Qwen3-4B-Thinking-2507-Q4_K_M.gguf` | ~2.5 GB | All platforms (latest reasoning 4B) |
158 | Qwen 3 30B-A3B Instruct 2507 (GGUF Q4_K_M, MoE) | `bartowski/Qwen_Qwen3-30B-A3B-Instruct-2507-GGUF` | `Qwen_Qwen3-30B-A3B-Instruct-2507-Q4_K_M.gguf` | ~18.6 GB | High-memory desktop (32+ GB), `qwen3moe` |
159 | DeepSeek Coder 6.7B Instruct (GGUF Q4_K_M) | `bartowski/deepseek-coder-6.7b-instruct-GGUF` | `deepseek-coder-6.7b-instruct-Q4_K_M.gguf` | ~3.8 GB | Higher-memory devices, custom chat template |
160 | Qwen 2.5 Coder 7B Instruct (ISQ) | `Qwen/Qwen2.5-Coder-7B-Instruct` | safetensors (ISQ in-situ) | ~8 GB | macOS (ISQ pipeline) |
161
162 ---
163
164 ## Key Types (Rust ↔ Swift)
165
166 | Rust Type | Swift Type | Notes |
167 |-----------|------------|-------|
168 | `ChatRole` | `enum ChatRole` | `.system`, `.user`, `.assistant` |
169 | `ChatMessage` | `struct ChatMessage` | `role: ChatRole`, `content: String` |
170 | `SamplingConfig` | `struct SamplingConfig` | All fields `Optional` |
171 | `GgufModelConfig` | `struct GgufModelConfig` | `modelId`, `files`, `tokModelId?`, `displayName`, `approxMemory`, `chatTemplate?` |
172 | `IsqModelConfig` | `struct IsqModelConfig` | `modelId`, `isqBits: UInt8`, `displayName`, `approxMemory` |
173 | `InferenceResult` | `struct InferenceResult` | `text`, `durationSecs`, `durationDisplay`, `finishReason`, `toolCalls` |
174 | `ToolCallInfo` | `struct ToolCallInfo` | Structured tool call request emitted by the model |
175 | `StreamChunk` | `struct StreamChunk` | `delta`, `done`, `finishReason?` |
176 | `EngineStatus` | `enum EngineStatus` | `.unloaded`, `.loading`, `.ready`, `.generating`, `.error` |
177 | `EngineInfo` | `struct EngineInfo` | `status`, `modelName?`, `approxMemory?`, `historyLength: UInt64` |
178 | `InferenceError` | `enum InferenceError: Error` | `noModelLoaded`, `alreadyLoaded`, `modelBuild`, `inference`, `cancelled`, `other` |
179 | `OndeChatEngine` | `class OndeChatEngine` | Thread-safe; `Arc`-backed; constructed with `OndeChatEngine()` |
180 | `StreamChunkListener` | `protocol StreamChunkListener` | Implement `onChunk(chunk:) -> Bool` |
181
182 ---
183
184 ## HuggingFace Token
185
186 `src/inference/token.rs` resolves the HF token in priority order:
187
188 1. **Build-time literal** (`HF_TOKEN` env var baked in via `option_env!`) — required for iOS/tvOS (no filesystem token possible).
189 2. **On-disk cache** (`~/.cache/huggingface/token`) — works on macOS after `mistralrs login`.
190
191 Set `HF_TOKEN` before building:
192
193 ```bash
194 export HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
195 cargo build --release
196 ```
197
198 On sandboxed platforms, your app's setup function must also configure `HF_HOME`, `HF_HUB_CACHE`, and `TMPDIR` to point inside the app container before any `OndeChatEngine` method is called. See `docs/swift-package.md` for the full `setupInferenceEnvironment()` Swift snippet.
199
200 ---
201
202 ## Building (Rust)
203
204 ### Prerequisites
205
206 ```bash
207 # Stable toolchain (macOS, iOS, Android, Windows, Linux)
208 rustup toolchain install stable
209
210 # Nightly toolchain (tvOS tier-3 targets only)
211 rustup toolchain install nightly
212 rustup component add rust-src --toolchain nightly
213
214 # Apple targets (stable)
215 rustup target add aarch64-apple-ios aarch64-apple-ios-sim
216 rustup target add aarch64-apple-darwin x86_64-apple-darwin
217 ```
218
219 ### Commands
220
221 ```bash
222 # Verify compilation (macOS host)
223 cargo check
224
225 # Run all tests
226 cargo test
227
228 # Run inference module tests only
229 cargo test inference::
230
231 # Run FFI wrapper tests only
232 cargo test inference::ffi::
233
234 # Clippy lint
235 cargo clippy
236
237 # Format
238 cargo fmt
239 ```
240
241 ### tvOS
242
243 tvOS targets require nightly and `-Z build-std`:
244
245 ```bash
246 cargo +nightly rustc -Z build-std \
247 --target aarch64-apple-tvos --release --lib --crate-type staticlib
248 ```
249
250 ---
251
252 ## Building the Swift XCFramework
253
254 The XCFramework bundles staticlibs for iOS device, iOS simulator, tvOS device, tvOS simulator, and macOS into a single distributable that `Package.swift` references.
255
256 ### Quick build (local or CI)
257
258 ```bash
259 .github/scripts/build-swift-xcframework.sh
260 ```
261
262 Output lands in `dist/swift/`:
263 - `OndeFramework.xcframework/` — the framework tree
264 - `OndeFramework.xcframework.zip` — zipped for SPM remote binary
265 - `OndeFramework.checksum.txt` — SHA-256 for `Package.swift`
266 - `version.txt` — semver from `Cargo.toml`
267 - `Package/Sources/Onde/onde.swift` — generated UniFFI Swift glue
268
269 ### Manual steps
270
271 ```bash
272 # 1. Build the uniffi-bindgen binary (pinned to uniffi =0.31.0)
273 cargo build --manifest-path uniffi-bindgen/Cargo.toml --release
274 BINDGEN=uniffi-bindgen/target/release/uniffi-bindgen
275
276 # 2. Compile staticlibs per target (use +1.92.0 or current stable)
277 cargo +stable rustc --target aarch64-apple-ios --release --lib --crate-type staticlib
278 cargo +stable rustc --target aarch64-apple-ios-sim --release --lib --crate-type staticlib
279 cargo +stable rustc --target aarch64-apple-darwin --release --lib --crate-type staticlib
280 cargo +nightly rustc -Z build-std --target aarch64-apple-tvos --release --lib --crate-type staticlib
281 cargo +nightly rustc -Z build-std --target aarch64-apple-tvos-sim --release --lib --crate-type staticlib
282
283 # 3. Generate Swift bindings from the iOS arm64 slice
284 $BINDGEN generate target/aarch64-apple-ios/release/libonde.a \
285 --language swift \
286 --out-dir sdk/Onde/Sources/Onde \
287 --config uniffi.toml
288
289 # 4. Assemble the XCFramework (see build-swift-xcframework.sh for full xcodebuild invocation)
290 ```
291
292 ### Updating `onde-swift` after a new release
293
294 This is fully automated by CI. Publishing a GitHub Release on `onde` triggers the workflow, which:
295
296 1. Builds the XCFramework and attaches it to the release.
297 2. Rewrites `onde-swift/Package.swift` with the release `url:` + `checksum:`.
298 3. Copies the freshly generated `onde.swift` into `onde-swift/Sources/Onde/`.
299 4. Commits, tags, and pushes `onde-swift` — Swift Package Index picks it up automatically.
300
301 Manual intervention is only needed if `ONDE_SWIFT_PAT` has expired or the `onde-swift` push fails.
302
303 ---
304
305 ## Swift SDK (`onde-swift`)
306
307 ### Package.swift structure
308
309 ```
310 onde-swift/
311 ├── Package.swift # Declares OndeFramework binary target + Onde wrapper target
312 └── Sources/Onde/
313 └── onde.swift # UniFFI-generated glue (NEVER edit manually)
314 ```
315
316 `Package.swift` has two targets:
317
318 - **`OndeFramework`** — `.binaryTarget` using `url:` + `checksum:` for distribution. For local development, swap to the `path:` form documented in the `Package.swift` header comment and run `build-swift-xcframework.sh` first.
319 - **`Onde`** — `.target` depending on `OndeFramework`, used as the public import name in Swift.
320
321 ### Swift API Quick Reference
322
323 ```swift
324 import Onde
325
326 // Create engine
327 let engine = OndeChatEngine()
328
329 // Load the platform default model
330 let elapsed = try await engine.loadDefaultModel(
331 systemPrompt: "You are a helpful assistant.",
332 sampling: nil
333 )
334
335 // Or load the model assigned to your app in the Onde dashboard
336 let assignedElapsed = try await engine.loadAssignedModel(
337 appId: "your-app-id",
338 appSecret: "your-app-secret",
339 systemPrompt: "You are a helpful assistant.",
340 sampling: nil
341 )
342
343 // Multi-turn chat
344 let result = try await engine.sendMessage(message: "Hello!")
345 print(result.text)
346 print(result.toolCalls)
347
348 // Streaming (free function — callback_interface constraint in UniFFI 0.31)
349 class Handler: StreamChunkListener {
350 func onChunk(chunk: StreamChunk) -> Bool {
351 print(chunk.delta, terminator: "")
352 return !chunk.done
353 }
354 }
355 try await streamChatMessage(engine: engine, message: "Tell me a story.", listener: Handler())
356
357 // One-shot (does NOT modify conversation history)
358 let enhanced = try await engine.generate(
359 messages: [userMessage(content: "Expand: a cat in space")],
360 sampling: deterministicSamplingConfig()
361 )
362
363 // Status
364 let info = await engine.info() // EngineInfo
365
366 // History management
367 let history = await engine.history()
368 let removed = await engine.clearHistory()
369 await engine.pushHistory(message: userMessage(content: "..."))
370
371 // Cleanup
372 await engine.unloadModel()
373 ```
374
375 ### Free Functions
376
377 | Function | Returns | Notes |
378 |----------|---------|-------|
379 | `defaultModelConfig()` | `GgufModelConfig` | Platform-aware Coder default (1.5B on iOS/tvOS/Android, 3B on desktop) |
380 | `qwen251_5bConfig()` | `GgufModelConfig` | Forces Qwen 2.5 1.5B regardless of platform |
381 | `qwen253bConfig()` | `GgufModelConfig` | Forces Qwen 2.5 3B regardless of platform |
382 | `qwen306bConfig()` | `GgufModelConfig` | Qwen 3 0.6B (~0.5 GB) |
383 | `qwen317bConfig()` | `GgufModelConfig` | Qwen 3 1.7B (~1.3 GB) |
384 | `qwen34bConfig()` | `GgufModelConfig` | Qwen 3 4B (~2.7 GB) |
385 | `qwen38bConfig()` | `GgufModelConfig` | Qwen 3 8B (~5 GB) |
386 | `qwen314bConfig()` | `GgufModelConfig` | Qwen 3 14B (~8.4 GB) |
387 | `qwen332bConfig()` | `GgufModelConfig` | Qwen 3 32B (~19.8 GB) |
388 | `qwen34bInstruct2507Config()` | `GgufModelConfig` | Qwen 3 4B Instruct 2507 — latest non-thinking 4B (~2.5 GB) |
389 | `qwen34bThinking2507Config()` | `GgufModelConfig` | Qwen 3 4B Thinking 2507 — latest reasoning 4B (~2.5 GB) |
390 | `qwen330bA3bInstruct2507Config()` | `GgufModelConfig` | Qwen 3 30B-A3B Instruct 2507 MoE (~18.6 GB) |
391 | `defaultSamplingConfig()` | `SamplingConfig` | temp=0.7, top_p=0.95, max_tokens=512 |
392 | `deterministicSamplingConfig()` | `SamplingConfig` | temp=0.0, greedy |
393 | `mobileSamplingConfig()` | `SamplingConfig` | temp=0.7, max_tokens=128 |
394 | `systemMessage(content:)` | `ChatMessage` | `.system` role |
395 | `userMessage(content:)` | `ChatMessage` | `.user` role |
396 | `assistantMessage(content:)` | `ChatMessage` | `.assistant` role |
397 | `streamChatMessage(engine:message:listener:)` | `async throws` | Streaming via callback |
398
399 ---
400
401 ## UniFFI Conventions
402
403 - **Version pin:** `uniffi = "=0.31.0"` everywhere — the `onde` crate, `uniffi-bindgen/`, and `[build-dependencies]` must all use the **same** version. Mixing versions causes bindgen panics.
404 - **`uniffi::setup_scaffolding!()`** in `lib.rs` — proc-macro approach, no UDL file needed.
405 - **Object methods** use `#[uniffi::export]` on the `impl OndeChatEngine` block.
406 - **Callback interfaces** (`StreamChunkListener`) must be parameters of **free functions** only, not Object methods.
407 - **`Arc<Self>`** is the return type for `#[uniffi::constructor]`. UniFFI automatically handles this.
408 - **Async:** all async exported methods use `tokio` runtime (enabled via `uniffi = { features = ["tokio"] }`).
409 - **`uniffi.toml`**: lives at the crate root. Adjust renaming or namespace settings there before regenerating.
410
411 ---
412
413 ## HuggingFace Cache (`hf_cache.rs`)
414
415 The `hf_cache` module manages the on-device model cache, exposed via UniFFI to Swift / Kotlin as-needed.
416
417 Key public functions:
418
419 | Function | Description |
420 |----------|-------------|
421 | `list_local_hf_models()` | List downloaded models that Onde supports |
422 | `list_supported_hf_models()` | All supported models with download status |
423 | `download_model(model_id, progress_callback)` | Download a model with progress reporting |
424 | `delete_local_hf_model(model_id)` | Remove a model from the local cache |
425 | `diagnose_hf_cache()` | Inspect the cache for corruption |
426 | `repair_hf_cache_symlinks()` | Fix broken symlinks in the HF cache layout |
427 | `model_cache_path(model_id)` | Resolve the filesystem path for a model |
428 | `clean_stale_lock_files()` | Remove leftover `.lock` files from interrupted downloads |
429
430 `ModelDownloadProgress` carries `downloaded_bytes`, `total_bytes`, `progress` (0.0–1.0), and `done`.
431
432 ---
433
434 ## Sampling Presets
435
436 | Preset | `temperature` | `top_p` | `max_tokens` | Use Case |
437 |--------|--------------|---------|--------------|----------|
438 | `SamplingConfig::default()` | 0.7 | 0.95 | 512 | General creative chat |
439 | `SamplingConfig::deterministic()` | 0.0 | — | 512 | Reproducible / coding |
440 | `SamplingConfig::mobile()` | 0.7 | 0.95 | 128 | Memory/latency constrained |
441 | `SamplingConfig::coding()` | 0.0 | — | 512 | Code generation |
442 | `SamplingConfig::coding_mobile()` | 0.0 | — | 128 | Code on mobile |
443
444 ---
445
446 ## Testing
447
448 ```bash
449 # All tests (requires a macOS host with Metal)
450 cargo test
451
452 # Unit tests only (no model downloads)
453 cargo test inference::
454 cargo test inference::ffi::
455 cargo test inference::types::
456
457 # hf_cache module
458 cargo test hf_cache::
459
460 # Clippy (treat warnings as errors)
461 cargo clippy -- -D warnings
462 ```
463
464 Tests that require model downloads are integration tests and not run by default in CI. Unit tests verify:
465 - Type constructors and `Display` implementations
466 - `SamplingConfig` presets
467 - `GgufModelConfig` constructors and `platform_default()` routing
468 - `OndeChatEngine` lifecycle: `new()` starts unloaded, `send_message` without model returns `InferenceError::NoModelLoaded`, `clear_history` on empty returns 0, `unload_model` on empty returns `nil`.
469
470 ---
471
472 ## Known Issues
473
474 ### `___chkstk_darwin` linker error on tvOS
475
476 `aws-lc-sys` (transitive via `reqwest → rustls → aws-lc-rs`) references `___chkstk_darwin`, a stack probing symbol that tvOS does not export. `build.rs` compiles `scripts/tvos_chkstk.s` (a no-op `ret` stub) via the `cc` crate to satisfy the linker. **Do not delete `scripts/tvos_chkstk.s`.**
477
478 Affects: tvOS only. macOS, iOS, Android, Windows, Linux are unaffected.
479
480 ### Metal Toolchain missing (Xcode 26+)
481
482 On Xcode 26+, the Metal compiler is a separate download. If you see empty `.metallib` files (92 bytes) or `"Error while loading function: fused_glu_float"` at runtime:
483
484 ```bash
485 xcodebuild -downloadComponent MetalToolchain
486 cargo clean -p mistralrs-quant
487 cargo check
488 ```
489
490 ### Android `home_dir` sandbox panic
491
492 `dirs::home_dir()` panics in the Android sandbox. The crate-level `Cargo.toml` adds `hf-hub` as an explicit Android dependency so `HF_HOME` can be seeded via `ApiBuilder` before any hub request. Never call `home::home_dir()` or `dirs::home_dir()` on Android.
493
494 ---
495
496 ## Code Conventions
497
498 ### Rust
499
500 - **Error handling:** `anyhow` for application-level errors inside `engine.rs`; `thiserror` for the `InferenceError` enum (exported via UniFFI). Never `.unwrap()` or `.expect()` in non-test code.
501 - **Async:** `tokio` runtime. All async functions in `OndeChatEngine` are `pub async fn`.
502 - **Logging:** `log` crate macros (`log::debug!`, `log::info!`, `log::warn!`, `log::error!`). No `println!` in library code.
503 - **Platform gating:** use `#[cfg(target_os = "...")]` blocks. Match `Cargo.toml`'s target-conditional dependency sections.
504 - **Re-exports:** `mistralrs`, `hf_hub`, and `mistralrs_core` are re-exported from `lib.rs` for downstream Rust consumers. Keep these re-exports in sync with what's actually available per platform.
505 - **No `mod.rs`:** use named files (`inference/engine.rs`) not `inference/mod.rs` — except that `inference/mod.rs` exists and is the intentional module root for the `inference` module.
506
507 ### Swift / SDK
508
509 - **Never manually edit `onde-swift/Sources/Onde/onde.swift`** — it is generated by `uniffi-bindgen`. Regenerate by running the build script.
510 - **`Package.swift` binary target** uses `url:` + `checksum:` in the committed form. For local development, swap to `path:` (instructions are in the file header). Never commit the `path:` form — CI overwrites it on every release via `release-sdk-swift.yml`.
511 - **iOS/tvOS sandbox:** always call `setupInferenceEnvironment()` at app launch before any `OndeChatEngine` call.
512
513 ---
514
515 ## CI / Release Workflow
516
517 ### `release-sdk-swift.yml`
518
519 Triggered on **semver tag push** or `workflow_dispatch`. Runs on `macos-15`.
520
521 Steps:
522 1. Install stable Rust with iOS/macOS targets.
523 2. Install nightly Rust with `rust-src` for tvOS `-Z build-std`.
524 3. Run `build-swift-xcframework.sh`.
525 4. Read `version.txt` and `OndeFramework.checksum.txt` into step outputs.
526 5. Validate that the tag matches `Cargo.toml`.
527 6. Upload CI artifacts: zip, checksum, version, generated `onde.swift`.
528 7. On tag push, create a GitHub Release with the zip and checksum attached.
529 8. On tag push, check out `ondeinference/onde-swift` using `ONDE_SWIFT_PAT`.
530 9. On tag push, rewrite the `.binaryTarget` in `onde-swift/Package.swift` with the release URL and checksum.
531 10. On tag push, copy the generated `onde.swift` into `onde-swift/Sources/Onde/`.
532 11. On tag push, commit both files, tag the commit with the version, and push to `ondeinference/onde-swift`.
533
534 #### Required secret
535
536 `ONDE_SWIFT_PAT` — a GitHub Personal Access Token with **`contents: write`** scope on the `ondeinference/onde-swift` repository.
537
538 ### `release-sdk-kotlin.yml`
539
540 Triggered on **semver tag push** or `workflow_dispatch`. Runs on `macos-15` because it builds both Android artifacts and the macOS Apple Silicon JVM native library.
541
542 Steps:
543 1. Install stable Rust and the `aarch64-apple-darwin` target.
544 2. Install Android SDK / NDK and auto-discover the latest installed 29.x NDK for `cargo-ndk`.
545 3. Build Android JNI libraries for all supported ABIs.
546 4. Build the macOS Apple Silicon `libonde.dylib` used by the JVM target.
547 5. Run Gradle publication tasks for the Kotlin Multiplatform package.
548 6. Validate that the tag matches `sdk/kotlin/gradle.properties` `VERSION_NAME`.
549 7. On tag push, publish `com.ondeinference:onde-inference` to Maven Central.
550
551 #### Required secrets
552
553 - `ORG_GRADLE_PROJECT_MAVENCENTRALUSERNAME`
554 - `ORG_GRADLE_PROJECT_MAVENCENTRALPASSWORD`
555 - `ORG_GRADLE_PROJECT_SIGNINGKEYID`
556 - `ORG_GRADLE_PROJECT_SIGNINGKEY`
557 - `ORG_GRADLE_PROJECT_SIGNINGPASSWORD`
558
559 ### `release-sdk-dart.yml`
560
561 Triggered on **semver tag push** or `workflow_dispatch`.
562
563 - Publishes `onde_inference` to pub.dev.
564 - Version must match the Dart package version.
565 - The example app is part of release validation.
566
567 ### `release-sdk-npm.yml`
568
569 Triggered on **semver tag push** or `workflow_dispatch`.
570
571 - Publishes `@ondeinference/react-native` to npm.
572 - Builds the Expo module package and validates the published version.
573
574 ### `release-sdk-rust.yml`
575
576 Triggered on **semver tag push** or `workflow_dispatch`.
577
578 - Publishes `onde` to crates.io.
579 - Uses registry dependencies, not git refs, because `cargo publish` strips git patches.
580
581 ### Release Process (end-to-end)
582
583 1. Bump versions in every affected package manifest:
584 - `Cargo.toml`
585 - `sdk/kotlin/gradle.properties`
586 - `sdk/dart/pubspec.yaml`
587 - `sdk/react-native/package.json`
588 2. Merge release branches with `--no-ff`. Do not fast-forward multi-commit feature branches.
589 3. Commit, tag, and push the release.
590 4. CI publishes all five SDKs:
591 - crates.io (`onde`)
592 - Swift Package Index / GitHub Releases (`onde-swift`)
593 - Maven Central (`com.ondeinference:onde-inference`)
594 - pub.dev (`onde_inference`)
595 - npm (`@ondeinference/react-native`)
596 5. Verify the registries after publish, especially Maven Central indexing and the generated Swift release assets.
597
598 > **Note:** `onde-swift/Package.swift` is updated automatically by CI. For local development only, switch it to the `path:` form documented in the file header, then switch it back before committing.
599
600 ---
601
602 ## Distribution Registry Summary
603
604 | Registry | Name | Import |
605 |----------|------|--------|
606 | crates.io | `onde` | `onde = "1.x"` |
607 | Swift Package Index | `onde-swift` (org: `ondeinference`) | `import Onde` |
608 | Maven Central | `com.ondeinference:onde-inference` | Gradle `implementation("com.ondeinference:onde-inference:<version>")` |
609 | pub.dev | `onde_inference` | `import 'package:onde_inference/onde_inference.dart';` |
610 | npm | `@ondeinference/react-native` | `import { OndeChatEngine } from "@ondeinference/react-native"` |
611 | PyPI | `onde-inference` | `import onde_inference` |
612 | RubyGems | `onde-inference` | `require 'onde'` |
613
614 ---
615
616 *This file is for AI agent and human developer reference. Update it when the architecture, API surface, supported models, or toolchain requirements change.*
617 ```