|
1
|
<p align="center"> |
|
2
|
<img src="https://raw.githubusercontent.com/ondeinference/onde/main/assets/onde-inference-logo.svg" alt="Onde Inference" width="96"> |
|
3
|
</p> |
|
4
|
|
|
5
|
<h1 align="center">Onde Inference Kotlin SDK</h1> |
|
6
|
|
|
7
|
<p align="center"> |
|
8
|
<strong>Run LLMs on-device from Kotlin with <a href="https://ondeinference.com/">Onde Inference</a>. No cloud, no API key, and no user data leaving the device.</strong> |
|
9
|
</p> |
|
10
|
|
|
11
|
<p align="center"> |
|
12
|
<a href="https://central.sonatype.com/artifact/com.ondeinference/onde-inference"><img src="https://img.shields.io/maven-central/v/com.ondeinference/onde-inference?style=flat-square&color=235843&labelColor=17211D&label=maven" alt="Maven Central"></a> |
|
13
|
<a href="https://ondeinference.com"><img src="https://img.shields.io/badge/ondeinference.com-235843?style=flat-square&labelColor=17211D" alt="Website"></a> |
|
14
|
<a href="https://github.com/ondeinference/onde/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-235843?style=flat-square&labelColor=17211D" alt="License"></a> |
|
15
|
</p> |
|
16
|
|
|
17
|
<p align="center"> |
|
18
|
<a href="https://github.com/ondeinference/onde">Rust SDK</a> · <a href="https://github.com/ondeinference/onde-swift">Swift SDK</a> · <a href="https://pub.dev/packages/onde_inference">Flutter SDK</a> · <a href="https://www.npmjs.com/package/@ondeinference/react-native">React Native SDK</a> · <a href="https://ondeinference.com">Website</a> |
|
19
|
</p> |
|
20
|
|
|
21
|
--- |
|
22
|
|
|
23
|
## What is Onde? |
|
24
|
|
|
25
|
Onde is an on-device LLM inference SDK for Kotlin apps. It wraps [mistral.rs](https://github.com/EricLBuehler/mistral.rs) in a Kotlin-friendly API, with model downloads from Hugging Face, local cache management, and native inference under the hood. |
|
26
|
|
|
27
|
- **Runs locally**: no cloud, no API key, no user data leaving the device |
|
28
|
- **Kotlin-friendly**: suspend functions and `Flow<StreamChunk>` for streaming |
|
29
|
- **Rust underneath**: powered by [uniffi-rs](https://github.com/mozilla/uniffi-rs) |
|
30
|
- **Published on Maven Central**: add one Gradle dependency and you're done |
|
31
|
|
|
32
|
This package is built as a Kotlin Multiplatform library. Today that means Android and JVM support, with the same Rust core underneath both. For [on-device AI inference for Kotlin and Android](https://ondeinference.com/sdk), the SDK page is the quickest way to check package docs and platform scope. If you want to test model downloads or fine-tune outputs before moving into app code, use [Onde CLI](https://github.com/ondeinference/onde-cli). |
|
33
|
|
|
34
|
--- |
|
35
|
|
|
36
|
## Installation |
|
37
|
|
|
38
|
Add to your app's `build.gradle.kts`: |
|
39
|
|
|
40
|
```kotlin |
|
41
|
dependencies { |
|
42
|
implementation("com.ondeinference:onde-inference:0.1.3") |
|
43
|
} |
|
44
|
``` |
|
45
|
|
|
46
|
Add `INTERNET` permission to `AndroidManifest.xml` (required for the initial model download): |
|
47
|
|
|
48
|
```xml |
|
49
|
<uses-permission android:name="android.permission.INTERNET" /> |
|
50
|
``` |
|
51
|
|
|
52
|
**Minimum requirements** |
|
53
|
- Android 8.0 (API 26) or higher |
|
54
|
- About 1.1 GB of free storage for the default model (Qwen 2.5 1.5B Q4_K_M) |
|
55
|
|
|
56
|
--- |
|
57
|
|
|
58
|
## Quick start on Android |
|
59
|
|
|
60
|
```kotlin |
|
61
|
import com.ondeinference.onde.OndeInference |
|
62
|
import kotlinx.coroutines.launch |
|
63
|
|
|
64
|
class MainActivity : AppCompatActivity() { |
|
65
|
|
|
66
|
private val onde by lazy { OndeInference(applicationContext) } |
|
67
|
|
|
68
|
override fun onCreate(savedInstanceState: Bundle?) { |
|
69
|
super.onCreate(savedInstanceState) |
|
70
|
|
|
71
|
lifecycleScope.launch { |
|
72
|
// 1. Load the default model (Qwen 2.5 1.5B on Android, ~941 MB) |
|
73
|
// It downloads from Hugging Face the first time you run it. |
|
74
|
val elapsed = onde.loadDefaultModel( |
|
75
|
systemPrompt = "You are a helpful, concise assistant." |
|
76
|
) |
|
77
|
println("Model loaded in ${elapsed}s") |
|
78
|
|
|
79
|
// 2. Chat |
|
80
|
val result = onde.chat("What is the capital of Sweden?") |
|
81
|
println(result.text) // → "The capital of Sweden is Stockholm." |
|
82
|
|
|
83
|
// 3. Clean up |
|
84
|
onde.unload() |
|
85
|
} |
|
86
|
} |
|
87
|
|
|
88
|
override fun onDestroy() { |
|
89
|
super.onDestroy() |
|
90
|
onde.close() |
|
91
|
} |
|
92
|
} |
|
93
|
``` |
|
94
|
|
|
95
|
--- |
|
96
|
|
|
97
|
## Streaming |
|
98
|
|
|
99
|
If you want tokens as they arrive, use `Flow`: |
|
100
|
|
|
101
|
```kotlin |
|
102
|
lifecycleScope.launch { |
|
103
|
onde.stream("Write a haiku about the ocean.").collect { chunk -> |
|
104
|
// Append each token delta to your UI |
|
105
|
textView.append(chunk.delta) |
|
106
|
if (chunk.done) println("\n[done]") |
|
107
|
} |
|
108
|
} |
|
109
|
``` |
|
110
|
|
|
111
|
--- |
|
112
|
|
|
113
|
## Multi-turn conversation |
|
114
|
|
|
115
|
Onde keeps conversation history for you: |
|
116
|
|
|
117
|
```kotlin |
|
118
|
lifecycleScope.launch { |
|
119
|
onde.loadDefaultModel(systemPrompt = "You are a Rust tutor.") |
|
120
|
|
|
121
|
val r1 = onde.chat("What is ownership?") |
|
122
|
println(r1.text) |
|
123
|
|
|
124
|
val r2 = onde.chat("Can you give me a code example?") |
|
125
|
println(r2.text) // knows the context from r1 |
|
126
|
|
|
127
|
// Clear history without unloading the model |
|
128
|
onde.clearHistory() |
|
129
|
} |
|
130
|
``` |
|
131
|
|
|
132
|
--- |
|
133
|
|
|
134
|
## Load a specific model |
|
135
|
|
|
136
|
```kotlin |
|
137
|
import com.ondeinference.onde.OndeModels |
|
138
|
|
|
139
|
lifecycleScope.launch { |
|
140
|
// Qwen 2.5 1.5B, the default on Android |
|
141
|
val elapsed = onde.loadModel( |
|
142
|
config = OndeModels.qwen25_1_5b(), |
|
143
|
systemPrompt = "You are a coding assistant." |
|
144
|
) |
|
145
|
} |
|
146
|
``` |
|
147
|
|
|
148
|
--- |
|
149
|
|
|
150
|
## Sampling configuration |
|
151
|
|
|
152
|
```kotlin |
|
153
|
import com.ondeinference.onde.OndeSampling |
|
154
|
|
|
155
|
lifecycleScope.launch { |
|
156
|
// Default chat settings: temperature 0.7, top_p 0.95, max 512 tokens |
|
157
|
onde.loadDefaultModel(sampling = OndeSampling.default()) |
|
158
|
|
|
159
|
// Deterministic output, useful for factual answers and code |
|
160
|
onde.loadDefaultModel(sampling = OndeSampling.deterministic()) |
|
161
|
|
|
162
|
// Shorter responses, usually better for lower-end devices |
|
163
|
onde.loadDefaultModel(sampling = OndeSampling.mobile()) |
|
164
|
} |
|
165
|
``` |
|
166
|
|
|
167
|
--- |
|
168
|
|
|
169
|
## One-shot generation (without changing chat history) |
|
170
|
|
|
171
|
```kotlin |
|
172
|
import com.ondeinference.onde.OndeMessage |
|
173
|
|
|
174
|
lifecycleScope.launch { |
|
175
|
val result = onde.generate( |
|
176
|
messages = listOf( |
|
177
|
OndeMessage.system("You are a summariser."), |
|
178
|
OndeMessage.user("Summarise this text: $longArticle"), |
|
179
|
), |
|
180
|
sampling = OndeSampling.deterministic() |
|
181
|
) |
|
182
|
println(result.text) |
|
183
|
} |
|
184
|
``` |
|
185
|
|
|
186
|
--- |
|
187
|
|
|
188
|
## Engine status |
|
189
|
|
|
190
|
```kotlin |
|
191
|
lifecycleScope.launch { |
|
192
|
val info = onde.info() |
|
193
|
println("Status: ${info.status}") // Ready / Loading / Unloaded / Error |
|
194
|
println("Model: ${info.modelName}") |
|
195
|
println("Memory: ${info.approxMemory}") |
|
196
|
println("History: ${info.historyLength} turns") |
|
197
|
} |
|
198
|
``` |
|
199
|
|
|
200
|
--- |
|
201
|
|
|
202
|
## File system layout |
|
203
|
|
|
204
|
Onde stores model files in the app's internal `filesDir`. You do not need external |
|
205
|
storage permissions. `INTERNET` is only needed for the initial download. |
|
206
|
|
|
207
|
``` |
|
208
|
<filesDir>/ |
|
209
|
├── models/ |
|
210
|
│ └── hub/ |
|
211
|
│ └── models--bartowski--Qwen2.5-1.5B-Instruct-GGUF/ |
|
212
|
│ └── ... |
|
213
|
└── tmp/ |
|
214
|
``` |
|
215
|
|
|
216
|
You can inspect the cache location at runtime: |
|
217
|
|
|
218
|
```kotlin |
|
219
|
println(onde.modelCacheDir.absolutePath) |
|
220
|
``` |
|
221
|
|
|
222
|
--- |
|
223
|
|
|
224
|
## How it fits together |
|
225
|
|
|
226
|
``` |
|
227
|
┌───────────────────────────────────────┐ |
|
228
|
│ Kotlin app / Android activity │ |
|
229
|
│ import com.ondeinference.onde.* │ |
|
230
|
└──────────────┬────────────────────────┘ |
|
231
|
│ suspend / Flow |
|
232
|
▼ |
|
233
|
┌───────────────────────────────────────┐ |
|
234
|
│ OndeInference.kt (this SDK) │ |
|
235
|
│ Android filesystem setup │ |
|
236
|
│ Dispatchers.IO dispatch │ |
|
237
|
└──────────────┬────────────────────────┘ |
|
238
|
│ UniFFI-generated Kotlin (onde.kt) |
|
239
|
▼ |
|
240
|
┌───────────────────────────────────────┐ |
|
241
|
│ libonde.so (Rust, per ABI) │ |
|
242
|
│ mistral.rs + Candle (CPU) │ |
|
243
|
│ HuggingFace Hub download │ |
|
244
|
└───────────────────────────────────────┘ |
|
245
|
``` |
|
246
|
|
|
247
|
On Android, the Rust engine is compiled for four ABIs and bundled as JNI libs: |
|
248
|
|
|
249
|
| ABI | Architecture | |
|
250
|
|-----|-------------| |
|
251
|
| `arm64-v8a` | 64-bit ARM (modern Android phones) | |
|
252
|
| `armeabi-v7a` | 32-bit ARM (older phones) | |
|
253
|
| `x86_64` | Android emulators (Intel/AMD) | |
|
254
|
| `x86` | 32-bit emulators | |
|
255
|
|
|
256
|
--- |
|
257
|
|
|
258
|
## Building from source |
|
259
|
|
|
260
|
### Prerequisites |
|
261
|
|
|
262
|
```bash |
|
263
|
# Rust Android targets |
|
264
|
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android |
|
265
|
|
|
266
|
# cargo-ndk — manages NDK toolchain for cross-compilation |
|
267
|
cargo install cargo-ndk |
|
268
|
|
|
269
|
# Android NDK (via Android Studio or SDK manager) |
|
270
|
export ANDROID_NDK_HOME=$HOME/Library/Android/sdk/ndk/26.1.10909125 |
|
271
|
``` |
|
272
|
|
|
273
|
### Build the native libraries |
|
274
|
|
|
275
|
```bash |
|
276
|
# From the repo root |
|
277
|
./sdk/kotlin/scripts/build-android.sh |
|
278
|
``` |
|
279
|
|
|
280
|
This produces `.so` files in `sdk/kotlin/lib/src/androidMain/jniLibs/`. |
|
281
|
|
|
282
|
### Generate Kotlin UniFFI bindings |
|
283
|
|
|
284
|
```bash |
|
285
|
./sdk/kotlin/scripts/generate-bindings.sh |
|
286
|
``` |
|
287
|
|
|
288
|
This generates `onde.kt` in `sdk/kotlin/lib/src/generated/kotlin/`. The generated file |
|
289
|
is gitignored, so regenerate it whenever the Rust API changes. |
|
290
|
|
|
291
|
### Build the Android artifact |
|
292
|
|
|
293
|
```bash |
|
294
|
cd sdk/kotlin |
|
295
|
./gradlew :lib:assembleRelease |
|
296
|
# Output: lib/build/outputs/aar/lib-release.aar |
|
297
|
``` |
|
298
|
|
|
299
|
--- |
|
300
|
|
|
301
|
## Publishing to Maven Central |
|
302
|
|
|
303
|
Publishing is handled by CI. It requires: |
|
304
|
|
|
305
|
| Secret | Description | |
|
306
|
|--------|-------------| |
|
307
|
| `MAVEN_CENTRAL_USERNAME` | Sonatype Central Portal username | |
|
308
|
| `MAVEN_CENTRAL_PASSWORD` | Sonatype Central Portal password | |
|
309
|
| `SIGNING_KEY_ID` | PGP key ID (last 8 hex chars) | |
|
310
|
| `SIGNING_KEY` | ASCII-armored PGP private key | |
|
311
|
| `SIGNING_PASSWORD` | PGP key passphrase | |
|
312
|
|
|
313
|
Tag a release to trigger the workflow: |
|
314
|
|
|
315
|
```bash |
|
316
|
# Bump version in Cargo.toml and gradle.properties first |
|
317
|
git tag 0.1.4 && git push origin 0.1.4 |
|
318
|
``` |
|
319
|
|
|
320
|
The CI workflow builds the native libraries, generates bindings, packages the Android |
|
321
|
artifact, signs it, and publishes to Maven Central automatically. |
|
322
|
|
|
323
|
--- |
|
324
|
|
|
325
|
## License |
|
326
|
|
|
327
|
MIT OR Apache-2.0. See [LICENSE](../../LICENSE). |
|
328
|
|
|
329
|
## Copyright |
|
330
|
|
|
331
|
© 2026 [Onde Inference](https://ondeinference.com) (Splitfire AB). |