@ondeinference / ed / commits / f1a5d11

chore: add agentic resources (.agents/AGENTS.md, symlinks)

Canonical agent instructions live in .agents/AGENTS.md. Root AGENTS.md and CLAUDE.md are symlinks pointing there. Content is tuned to the onde-ed workspace: crate map, architecture rules, LLM provider config, FFI/Tauri boundaries, and commit conventions. Co-Authored-By: siGit Code v1.6.6-acp <noreply@sigit.si>

Seto Elkahfi committed Oct 10, 2026 at 08:17 UTC f1a5d116fad1b65720fd54c9575e04b77100332d
5 files changed +84 -1820
.agents/AGENTS.md
+82 -586
@@ -1,617 +1,113 @@
1 -# Onde Inference — AGENTS.md
1 +# onde-ed – Agent Instructions
2
3 -> **AI agent reference for the `onde` repository and the `onde-swift` Swift SDK.**
4 -> Keep this file accurate as the codebase evolves.
3 +## What this repo is
4 +`onde-ed` is a **Rust Cargo workspace** providing the embeddable agentic core
5 +used across Onde products (macOS, iOS, Tauri, CLI). The workspace is organised
6 +into focused crates; do not merge them.
7
8 ---
9
8 -## What Is Onde?
10 +## Crate map
11
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)
12 +| Crate | Purpose |
13 +|---|---|
14 +| `ed-agent` | Core library: `Ed` struct, lifecycle, `send`/`stream`/`run`, tool approval, `EventSink`, `SessionStore` seams |
15 +| `ed-acp` | ACP v1 server: `Profile` trait, `serve`/`serve_stdio`, sessions, LLM streaming, tool routing, MCP |
16 +| `ed-mcp` | MCP integration over `rmcp`: tool namespacing (`mcp__<server>__<tool>`, max 64 bytes), `SharedServer`, `McpExecutor`, timeouts, cancellation |
17 +| `ed-acp-tui` | ratatui TUI frontend for ACP agents over stdio |
18 +| `ed-acp-testkit` | Mock Onde Cloud endpoint + conformance test suite |
19 +| `ed-agent-ffi` | UniFFI bridge → Apple XCFramework (`FfiEd`, `FfiToolExecutor`, `FfiApprovalHandler`, `FfiEventListener`) |
20 +| `ed-agent-tauri` | Tauri command bindings that emit events to the webview |
21
22 ---
23
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 ----
24 +## Non-negotiable architecture rules
25
76 -## Architecture
26 +1. **One `Ed` instance, one engine, one model.** `Ed` is `Send + Sync`, not
27 + `Clone`. Never try to clone or share it across threads without `Arc`.
28
78 -### Layer Diagram
29 +2. **`Profile` is the product seam.** A product crate (or binary) implements
30 + `Profile` (`info`, `system_prompt`, `toolsets`, `builtin_mcp_servers`).
31 + `ed-acp` handles the ACP server, session management, and LLM glue. Do not
32 + put product-specific logic inside `ed-acp`.
33
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 ----
34 +3. **Tool risk gates approval.** `ToolRisk::ReadOnly` vs `ToolRisk::Mutating`
35 + determines whether the approval handler is invoked. Per-session grants are
36 + tracked via `ApprovalDecision::AllowForSession`. Do not bypass this gate.
37
112 -## Platform Support
38 +4. **MCP tool names ≤ 64 bytes.** Names are sanitised to
39 + `mcp__<server>__<tool>`. Anything longer is truncated. Do not assume the
40 + original tool name survived intact.
41
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) | `[]` |
42 +5. **FFI types are wrappers, not source of truth.** `FfiEd` delegates to
43 + `Ed`; `FfiToolExecutor` bridges a foreign callback into the
44 + `ToolExecutor` trait. Keep FFI types thin.
45
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`.
46 +6. **Tauri and FFI are separate bindings.** `ed-agent-tauri` and
47 + `ed-agent-ffi` both wrap `ed-agent` but must not depend on each other.
48
49 ---
50
130 -## Supported Models
51 +## LLM providers
52
132 -All model constants live in `src/inference/models.rs`. When adding a new model:
53 +Configured via `ed-acp/src/llm.rs`. Three variants: `Onde`, `OpenAi`, `Local`.
54
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.
55 +| Env var | Effect |
56 +|---|---|
57 +| `<PREFIX>_PROVIDER` | `onde` / `openai` / `local` |
58 +| `ONDE_API_KEY` | Cloud key for `Onde` provider |
59 +| `OPENAI_API_KEY` | Key for `OpenAi` provider |
60
141 -### Current Models
61 +Default cloud model: `onde-kkk`.
62 +Default local model: `Qwen3-4B` (desktop) / `Qwen3-1.7B` (mobile).
63
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) |
64 +`LlmEnv` drives config-dir, env-file loading, and variable namespacing per
65 +product. Do not hard-code env vars; always go through `LlmEnv`.
66
67 ---
68
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 -```
69 +## Working in this repo
70
375 -### Free Functions
71 +### Before changing anything
72 +- Read the relevant `lib.rs` and the trait/struct you are modifying first.
73 +- Check whether a public API is re-exported from `ed-agent`'s `lib.rs` before
74 + adding a new one; keep the public surface minimal.
75 +- If touching `ed-agent-ffi`, remember it must compile for Apple targets
76 + (macOS + iOS); avoid anything that does not cross the FFI boundary cleanly.
77
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 ----
78 +### Making changes
79 +- Keep crate boundaries: adding a dependency from a lower-level crate
80 + (`ed-agent`) on a higher-level one (`ed-acp`) is forbidden.
81 +- New tool definitions belong in the crate that owns the domain, exposed via
82 + `ToolExecutor` or MCP server, not inlined into `Ed`.
83 +- Prefer `async_trait` + `Arc<dyn Trait>` for seams; avoid generics on `Ed`
84 + itself.
85 +- When adding an FFI type, add the matching `From<Ffi*>` / `From<*>` impls
86 + immediately so both sides stay in sync.
87
602 -## Distribution Registry Summary
88 +### After changes
89 +- Run `cargo check --workspace` to catch compile errors across all crates.
90 +- Run `cargo test --workspace` before committing.
91 +- For FFI changes, note that a full XCFramework build requires the Apple SDK
92 + toolchain; CI handles that — local `cargo check` is sufficient to validate
93 + Rust correctness.
94
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'` |
95 +### Commit messages
96 +- Scope to the affected crate(s): `ed-agent: …`, `ed-acp: …`, `ed-mcp: …`.
97 +- Keep the subject line under 72 chars.
98 +- Always end the commit with:
99 + ```
100 + Co-Authored-By: siGit Code v1.6.6-acp <noreply@sigit.si>
101 + ```
102
103 ---
104
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 -```
105 +## Things to avoid
106 +- Do not add `#[allow(unused)]` blanket suppressions — fix the unused item.
107 +- Do not store `tokio::runtime::Runtime` inside an FFI object; use `block_on`
108 + only at the outermost FFI boundary.
109 +- Do not panic inside async tasks — convert errors to `AgentError` and surface
110 + them through the event sink or return type.
111 +- Do not hard-code model names anywhere outside `ed-acp/src/llm.rs`.
112 +- Do not bypass `ToolRisk` approval for any mutating tool, even in tests — use
113 + an `AllowAll` test double instead.
AGENTS.md deleted
-617
@@ -1,617 +0,0 @@
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 -```
AGENTS.md new
+1
@@ -0,0 +1 @@
1 +.agents/AGENTS.md
\ No newline at end of file
CLAUDE.md deleted
-617
@@ -1,617 +0,0 @@
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 -```
CLAUDE.md new
+1
@@ -0,0 +1 @@
1 +.agents/AGENTS.md
\ No newline at end of file