|
1
|
<p align="center"> |
|
2
|
<h1 align="center">onde-inference</h1> |
|
3
|
<p align="center"><strong>On-device AI inference for Python</strong></p> |
|
4
|
</p> |
|
5
|
|
|
6
|
<p align="center"> |
|
7
|
<a href="https://pypi.org/project/onde-inference/"><img src="https://img.shields.io/pypi/v/onde-inference.svg" alt="PyPI version"></a> |
|
8
|
<a href="https://pypi.org/project/onde-inference/"><img src="https://img.shields.io/pypi/pyversions/onde-inference.svg" alt="Python 3.9+"></a> |
|
9
|
<a href="#license"><img src="https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg" alt="License"></a> |
|
10
|
</p> |
|
11
|
|
|
12
|
--- |
|
13
|
|
|
14
|
Run LLMs and speech-to-text locally — no cloud, no latency, no data leaving the device. Powered by Rust via [mistral.rs](https://github.com/EricLBuehler/mistral.rs). |
|
15
|
|
|
16
|
`onde-inference` is a Python binding for the [Onde](https://github.com/ondeinference/onde) Rust crate. It wraps the Rust inference engine via [UniFFI](https://mozilla.github.io/uniffi-rs/) bindings, giving you native-speed on-device inference with a simple, async Python API. |
|
17
|
|
|
18
|
## Installation |
|
19
|
|
|
20
|
```bash |
|
21
|
pip install onde-inference |
|
22
|
``` |
|
23
|
|
|
24
|
## Quick Start |
|
25
|
|
|
26
|
### Basic Chat |
|
27
|
|
|
28
|
```python |
|
29
|
import asyncio |
|
30
|
from onde_inference import OndeChatEngine, default_model_config |
|
31
|
|
|
32
|
|
|
33
|
async def main(): |
|
34
|
# Create the engine (starts with no model loaded). |
|
35
|
engine = OndeChatEngine() |
|
36
|
|
|
37
|
# Load the platform-appropriate default model. |
|
38
|
# macOS → Qwen 2.5 3B (~1.93 GB), Linux/Windows → Qwen 2.5 3B (~1.93 GB) |
|
39
|
elapsed = await engine.load_default_model( |
|
40
|
system_prompt="You are a helpful assistant.", |
|
41
|
sampling=None, |
|
42
|
) |
|
43
|
print(f"Model loaded in {elapsed:.1f}s") |
|
44
|
|
|
45
|
# Send a message — history is managed automatically. |
|
46
|
result = await engine.send_message("What is Rust?") |
|
47
|
print(result.text) |
|
48
|
print(f"Generated in {result.duration_display}") |
|
49
|
|
|
50
|
# Multi-turn conversation. |
|
51
|
follow_up = await engine.send_message("How does its ownership model work?") |
|
52
|
print(follow_up.text) |
|
53
|
|
|
54
|
# Cleanup. |
|
55
|
await engine.unload_model() |
|
56
|
|
|
57
|
|
|
58
|
asyncio.run(main()) |
|
59
|
``` |
|
60
|
|
|
61
|
### Model Configuration Helpers |
|
62
|
|
|
63
|
Choose a specific model instead of the platform default: |
|
64
|
|
|
65
|
```python |
|
66
|
from onde_inference import default_model_config, qwen25_1_5b_config, qwen25_3b_config |
|
67
|
|
|
68
|
# Platform-appropriate default (3B on desktop, 1.5B on mobile). |
|
69
|
config = default_model_config() |
|
70
|
|
|
71
|
# Explicitly pick a model. |
|
72
|
small = qwen25_1_5b_config() # Qwen 2.5 1.5B — ~941 MB |
|
73
|
medium = qwen25_3b_config() # Qwen 2.5 3B — ~1.93 GB |
|
74
|
|
|
75
|
# Load a specific model config. |
|
76
|
elapsed = await engine.load_gguf_model( |
|
77
|
config=small, |
|
78
|
system_prompt="You are a helpful assistant.", |
|
79
|
sampling=None, |
|
80
|
) |
|
81
|
``` |
|
82
|
|
|
83
|
### Sampling Configuration |
|
84
|
|
|
85
|
Control how the model generates text: |
|
86
|
|
|
87
|
```python |
|
88
|
from onde_inference import ( |
|
89
|
default_sampling_config, |
|
90
|
deterministic_sampling_config, |
|
91
|
mobile_sampling_config, |
|
92
|
) |
|
93
|
|
|
94
|
# Creative chat (temperature=0.7, top_p=0.95, max_tokens=512). |
|
95
|
creative = default_sampling_config() |
|
96
|
|
|
97
|
# Greedy decoding (temperature=0.0) — deterministic output. |
|
98
|
greedy = deterministic_sampling_config() |
|
99
|
|
|
100
|
# Conservative settings for constrained devices (max_tokens=128). |
|
101
|
mobile = mobile_sampling_config() |
|
102
|
|
|
103
|
# Pass to load or update at any time. |
|
104
|
await engine.load_default_model(system_prompt="You are helpful.", sampling=greedy) |
|
105
|
await engine.set_sampling(creative) |
|
106
|
``` |
|
107
|
|
|
108
|
### Streaming |
|
109
|
|
|
110
|
Stream tokens as they are generated using a callback class that implements `StreamChunkListener`: |
|
111
|
|
|
112
|
```python |
|
113
|
import asyncio |
|
114
|
from onde_inference import ( |
|
115
|
OndeChatEngine, |
|
116
|
StreamChunkListener, |
|
117
|
StreamChunk, |
|
118
|
stream_chat_message, |
|
119
|
) |
|
120
|
|
|
121
|
|
|
122
|
class MyStreamHandler(StreamChunkListener): |
|
123
|
def on_chunk(self, chunk: StreamChunk) -> bool: |
|
124
|
"""Called for each token. Return True to continue, False to cancel.""" |
|
125
|
if chunk.delta: |
|
126
|
print(chunk.delta, end="", flush=True) |
|
127
|
if chunk.done: |
|
128
|
print() # newline at the end |
|
129
|
return True |
|
130
|
|
|
131
|
|
|
132
|
async def main(): |
|
133
|
engine = OndeChatEngine() |
|
134
|
await engine.load_default_model( |
|
135
|
system_prompt="You are a helpful assistant.", |
|
136
|
sampling=None, |
|
137
|
) |
|
138
|
|
|
139
|
# stream_chat_message is a free function (UniFFI requirement). |
|
140
|
await stream_chat_message( |
|
141
|
engine=engine, |
|
142
|
message="Tell me a short story about a robot learning to paint.", |
|
143
|
listener=MyStreamHandler(), |
|
144
|
) |
|
145
|
|
|
146
|
await engine.unload_model() |
|
147
|
|
|
148
|
|
|
149
|
asyncio.run(main()) |
|
150
|
``` |
|
151
|
|
|
152
|
### Message Helpers |
|
153
|
|
|
154
|
Build message lists for one-shot generation without modifying conversation history: |
|
155
|
|
|
156
|
```python |
|
157
|
from onde_inference import system_message, user_message, assistant_message |
|
158
|
|
|
159
|
messages = [ |
|
160
|
system_message("You expand short prompts into detailed descriptions."), |
|
161
|
user_message("a cat in space"), |
|
162
|
] |
|
163
|
|
|
164
|
result = await engine.generate(messages=messages, sampling=None) |
|
165
|
print(result.text) |
|
166
|
``` |
|
167
|
|
|
168
|
### Engine Introspection |
|
169
|
|
|
170
|
```python |
|
171
|
# Check engine state. |
|
172
|
info = await engine.info() |
|
173
|
print(info.status) # "ready", "unloaded", "generating", etc. |
|
174
|
print(info.model_name) # "Qwen 2.5 3B" or None |
|
175
|
print(info.history_length) # number of conversation turns |
|
176
|
|
|
177
|
# Access or clear history. |
|
178
|
history = await engine.history() |
|
179
|
cleared = await engine.clear_history() |
|
180
|
|
|
181
|
# Check if a model is loaded. |
|
182
|
if await engine.is_loaded(): |
|
183
|
print("Ready to chat!") |
|
184
|
``` |
|
185
|
|
|
186
|
## Platform Support |
|
187
|
|
|
188
|
| Platform | Accelerator | Notes | |
|
189
|
|----------|-------------|-------| |
|
190
|
| **macOS** | Metal GPU | Apple Silicon and Intel Macs | |
|
191
|
| **Linux** | CPU / CUDA | NVIDIA GPU acceleration with CUDA builds | |
|
192
|
| **Windows** | CPU / CUDA | NVIDIA GPU acceleration with CUDA builds | |
|
193
|
|
|
194
|
## Supported Models |
|
195
|
|
|
196
|
| Model | Size | Best For | |
|
197
|
|-------|------|----------| |
|
198
|
| **Qwen 2.5 1.5B** (GGUF Q4_K_M) | ~941 MB | Lightweight / memory-constrained environments | |
|
199
|
| **Qwen 2.5 3B** (GGUF Q4_K_M) | ~1.93 GB | Balanced quality and performance on desktop | |
|
200
|
|
|
201
|
Models are automatically downloaded from HuggingFace Hub on first use and cached locally. |
|
202
|
|
|
203
|
## How It Works |
|
204
|
|
|
205
|
`onde-inference` is a thin Python layer over the **Onde** Rust crate. [UniFFI](https://mozilla.github.io/uniffi-rs/) generates Python bindings from the Rust source, so every call goes directly into native compiled code — there is no Python inference loop. Async methods use Python's `asyncio` (UniFFI generates async Python wrappers for Rust async functions backed by Tokio). |
|
206
|
|
|
207
|
## Links |
|
208
|
|
|
209
|
- **Source**: [github.com/ondeinference/onde](https://github.com/ondeinference/onde) |
|
210
|
- **Rust crate**: [crates.io/crates/onde](https://crates.io/crates/onde) |
|
211
|
- **Documentation**: [docs.rs/onde](https://docs.rs/onde) |
|
212
|
- **Inference engine**: [mistral.rs](https://github.com/EricLBuehler/mistral.rs) |
|
213
|
|
|
214
|
## License |
|
215
|
|
|
216
|
Licensed under either of: |
|
217
|
|
|
218
|
- Apache License, Version 2.0 ([LICENSE-APACHE](https://github.com/ondeinference/onde/blob/main/LICENSE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0>) |
|
219
|
- MIT License ([LICENSE-MIT](https://github.com/ondeinference/onde/blob/main/LICENSE-MIT) or <http://opensource.org/licenses/MIT>) |
|
220
|
|
|
221
|
at your option. |
|
222
|
|
|
223
|
## Copyright |
|
224
|
|
|
225
|
Copyright 2026 [Onde Inference](https://ondeinference.com) |