onde-inference 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ [package]
2
+ name = "onde-ruby"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ authors = ["Seto Elkahfi <setoelkahfi@gmail.com>"]
6
+ publish = false
7
+
8
+ [lib]
9
+ name = "onde_ruby"
10
+ crate-type = ["cdylib"]
11
+
12
+ [dependencies]
13
+ magnus = { version = "0.7" }
14
+ serde = { version = "1.0", features = ["derive"] }
15
+ serde_json = "1"
16
+
17
+ # The onde crate — referenced via relative path from the monorepo.
18
+ # We only pull in the pure-Rust surface (hf_cache, inference::models,
19
+ # inference::types) so we do NOT enable the `whisper` feature or any
20
+ # platform-specific mistralrs re-exports that require GPU SDKs.
21
+ #
22
+ # NOTE: onde's Cargo.toml gates mistralrs behind cfg(target_os = …) so on
23
+ # the host OS (macOS/Linux) it will be pulled in automatically. If this
24
+ # causes build issues in CI, consider adding a `ruby` feature to the onde
25
+ # crate that disables the heavy deps.
26
+ onde = { path = "../../../", default-features = false }
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rb_sys/mkmf"
5
+
6
+ create_rust_makefile("onde/onde_ruby")
@@ -0,0 +1,272 @@
1
+ //! Ruby native extension for the Onde on-device inference engine.
2
+ //!
3
+ //! This crate uses [magnus](https://github.com/matsadler/magnus) to expose
4
+ //! Onde's HuggingFace cache management, supported model metadata, and
5
+ //! inference types to Ruby as the `Onde` module.
6
+ //!
7
+ //! ## Ruby usage
8
+ //!
9
+ //! ```ruby
10
+ //! require "onde"
11
+ //!
12
+ //! # List models cached locally on disk.
13
+ //! response = Onde.list_local_models
14
+ //! response["models"].each do |m|
15
+ //! puts "#{m["model_id"]} — #{m["size_display"]}"
16
+ //! end
17
+ //!
18
+ //! # List all supported models (with download status).
19
+ //! Onde.list_supported_models["models"].each do |m|
20
+ //! status = m["is_downloaded"] ? "✓" : "✗"
21
+ //! puts "[#{status}] #{m["name"]} (#{m["org"]}) — #{m["expected_size_display"]}"
22
+ //! end
23
+ //!
24
+ //! # Delete a cached model.
25
+ //! Onde.delete_model("bartowski/Qwen2.5-1.5B-Instruct-GGUF")
26
+ //!
27
+ //! # Inspect supported model IDs.
28
+ //! Onde::SUPPORTED_MODELS # => ["black-forest-labs/FLUX.1-schnell", ...]
29
+ //!
30
+ //! # Access model metadata.
31
+ //! Onde.model_info("bartowski/Qwen2.5-1.5B-Instruct-GGUF")
32
+ //! # => { "id" => "bartowski/…", "name" => "Qwen 2.5 1.5B (GGUF)", … }
33
+ //!
34
+ //! # Sampling config helpers.
35
+ //! Onde.default_sampling_config
36
+ //! Onde.deterministic_sampling_config
37
+ //! Onde.mobile_sampling_config
38
+ //! ```
39
+
40
+ use magnus::{function, prelude::*, Error, RHash, Ruby};
41
+
42
+ use onde::hf_cache;
43
+ use onde::inference::models::{SUPPORTED_MODELS, SUPPORTED_MODEL_INFO};
44
+ use onde::inference::types::SamplingConfig;
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Helpers — convert Rust structs to Ruby hashes via serde_json
48
+ // ---------------------------------------------------------------------------
49
+
50
+ /// Serialize any `serde::Serialize` value into a Ruby Hash (or Array of
51
+ /// Hashes) by round-tripping through serde_json. This keeps the binding
52
+ /// layer thin — we don't need to define Ruby classes for every Onde struct.
53
+ fn to_ruby_value<T: serde::Serialize>(ruby: &Ruby, value: &T) -> Result<magnus::Value, Error> {
54
+ let json = serde_json::to_value(value).map_err(|e| {
55
+ Error::new(
56
+ ruby.exception_runtime_error(),
57
+ format!("serialization error: {e}"),
58
+ )
59
+ })?;
60
+ json_to_ruby(ruby, &json)
61
+ }
62
+
63
+ fn json_to_ruby(ruby: &Ruby, value: &serde_json::Value) -> Result<magnus::Value, Error> {
64
+ match value {
65
+ serde_json::Value::Null => Ok(ruby.qnil().as_value()),
66
+ serde_json::Value::Bool(b) => {
67
+ if *b {
68
+ Ok(ruby.qtrue().as_value())
69
+ } else {
70
+ Ok(ruby.qfalse().as_value())
71
+ }
72
+ }
73
+ serde_json::Value::Number(n) => {
74
+ if let Some(i) = n.as_i64() {
75
+ Ok(ruby.integer_from_i64(i).as_value())
76
+ } else if let Some(f) = n.as_f64() {
77
+ Ok(ruby.float_from_f64(f).as_value())
78
+ } else {
79
+ Ok(ruby.qnil().as_value())
80
+ }
81
+ }
82
+ serde_json::Value::String(s) => Ok(ruby.str_new(s).as_value()),
83
+ serde_json::Value::Array(arr) => {
84
+ let ary = ruby.ary_new_capa(arr.len());
85
+ for item in arr {
86
+ ary.push(json_to_ruby(ruby, item)?)?;
87
+ }
88
+ Ok(ary.as_value())
89
+ }
90
+ serde_json::Value::Object(map) => {
91
+ let hash = ruby.hash_new();
92
+ for (k, v) in map {
93
+ hash.aset(ruby.str_new(k), json_to_ruby(ruby, v)?)?;
94
+ }
95
+ Ok(hash.as_value())
96
+ }
97
+ }
98
+ }
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // Exported Ruby methods
102
+ // ---------------------------------------------------------------------------
103
+
104
+ /// `Onde.list_local_models` → Hash
105
+ ///
106
+ /// Scans the local HuggingFace hub cache and returns all downloaded models
107
+ /// that the inference engine supports.
108
+ ///
109
+ /// Returns a Hash with keys: `"models"`, `"cache_path"`,
110
+ /// `"total_size_bytes"`, `"total_size_display"`.
111
+ fn list_local_models() -> Result<magnus::Value, Error> {
112
+ let ruby = Ruby::get().expect("called outside Ruby");
113
+ let response = hf_cache::list_local_hf_models();
114
+ to_ruby_value(&ruby, &response)
115
+ }
116
+
117
+ /// `Onde.list_supported_models` → Hash
118
+ ///
119
+ /// Returns all models the engine supports, together with flags indicating
120
+ /// whether each one is fully downloaded, partially downloaded, or absent.
121
+ ///
122
+ /// Returns a Hash with key `"models"` containing an Array of model Hashes.
123
+ fn list_supported_models() -> Result<magnus::Value, Error> {
124
+ let ruby = Ruby::get().expect("called outside Ruby");
125
+ let response = hf_cache::list_supported_hf_models();
126
+ to_ruby_value(&ruby, &response)
127
+ }
128
+
129
+ /// `Onde.delete_model(model_id)` → nil
130
+ ///
131
+ /// Delete a locally cached HuggingFace model.
132
+ /// `model_id` is the full identifier, e.g. `"black-forest-labs/FLUX.1-schnell"`.
133
+ ///
134
+ /// Raises `RuntimeError` if the model is not found or deletion fails.
135
+ fn delete_model(model_id: String) -> Result<magnus::Value, Error> {
136
+ let ruby = Ruby::get().expect("called outside Ruby");
137
+ hf_cache::delete_local_hf_model(model_id)
138
+ .map_err(|e| Error::new(ruby.exception_runtime_error(), e))?;
139
+ Ok(ruby.qnil().as_value())
140
+ }
141
+
142
+ /// `Onde.model_info(model_id)` → Hash or nil
143
+ ///
144
+ /// Look up rich metadata for a supported model by its ID.
145
+ /// Returns `nil` if the model ID is not in the supported list.
146
+ fn model_info(model_id: String) -> Result<magnus::Value, Error> {
147
+ let ruby = Ruby::get().expect("called outside Ruby");
148
+
149
+ let info = SUPPORTED_MODEL_INFO.iter().find(|i| i.id == model_id);
150
+
151
+ match info {
152
+ None => Ok(ruby.qnil().as_value()),
153
+ Some(i) => {
154
+ let hash = ruby.hash_new();
155
+ hash.aset(ruby.str_new("id"), ruby.str_new(i.id))?;
156
+ hash.aset(ruby.str_new("name"), ruby.str_new(i.name))?;
157
+ hash.aset(ruby.str_new("org"), ruby.str_new(i.org))?;
158
+ hash.aset(ruby.str_new("description"), ruby.str_new(i.description))?;
159
+ hash.aset(
160
+ ruby.str_new("expected_size_bytes"),
161
+ ruby.integer_from_u64(i.expected_size_bytes),
162
+ )?;
163
+ Ok(hash.as_value())
164
+ }
165
+ }
166
+ }
167
+
168
+ /// `Onde.supported_model_ids` → Array of String
169
+ ///
170
+ /// Returns the list of all supported model IDs.
171
+ fn supported_model_ids() -> Result<magnus::Value, Error> {
172
+ let ruby = Ruby::get().expect("called outside Ruby");
173
+ let ary = ruby.ary_new_capa(SUPPORTED_MODELS.len());
174
+ for id in SUPPORTED_MODELS {
175
+ ary.push(ruby.str_new(id))?;
176
+ }
177
+ Ok(ary.as_value())
178
+ }
179
+
180
+ /// `Onde.default_sampling_config` → Hash
181
+ fn default_sampling_config() -> Result<magnus::Value, Error> {
182
+ let ruby = Ruby::get().expect("called outside Ruby");
183
+ to_ruby_value(&ruby, &SamplingConfig::default())
184
+ }
185
+
186
+ /// `Onde.deterministic_sampling_config` → Hash
187
+ fn deterministic_sampling_config() -> Result<magnus::Value, Error> {
188
+ let ruby = Ruby::get().expect("called outside Ruby");
189
+ to_ruby_value(&ruby, &SamplingConfig::deterministic())
190
+ }
191
+
192
+ /// `Onde.mobile_sampling_config` → Hash
193
+ fn mobile_sampling_config() -> Result<magnus::Value, Error> {
194
+ let ruby = Ruby::get().expect("called outside Ruby");
195
+ to_ruby_value(&ruby, &SamplingConfig::mobile())
196
+ }
197
+
198
+ /// `Onde.cache_path` → String or nil
199
+ ///
200
+ /// Returns the resolved HuggingFace cache directory path, or nil if it
201
+ /// cannot be determined (e.g. `$HOME` is unset).
202
+ fn cache_path() -> Result<magnus::Value, Error> {
203
+ let ruby = Ruby::get().expect("called outside Ruby");
204
+ let response = hf_cache::list_local_hf_models();
205
+ if response.cache_path.is_empty() {
206
+ Ok(ruby.qnil().as_value())
207
+ } else {
208
+ Ok(ruby.str_new(&response.cache_path).as_value())
209
+ }
210
+ }
211
+
212
+ // ---------------------------------------------------------------------------
213
+ // Init — called by Ruby when `require "onde/onde"` loads the shared library
214
+ // ---------------------------------------------------------------------------
215
+
216
+ #[magnus::init]
217
+ fn init(ruby: &Ruby) -> Result<(), Error> {
218
+ let module = ruby.define_module("Onde")?;
219
+
220
+ // -- Singleton methods ----------------------------------------------------
221
+ module.define_singleton_method("list_local_models", function!(list_local_models, 0))?;
222
+ module.define_singleton_method("list_supported_models", function!(list_supported_models, 0))?;
223
+ module.define_singleton_method("delete_model", function!(delete_model, 1))?;
224
+ module.define_singleton_method("model_info", function!(model_info, 1))?;
225
+ module.define_singleton_method("supported_model_ids", function!(supported_model_ids, 0))?;
226
+ module.define_singleton_method(
227
+ "default_sampling_config",
228
+ function!(default_sampling_config, 0),
229
+ )?;
230
+ module.define_singleton_method(
231
+ "deterministic_sampling_config",
232
+ function!(deterministic_sampling_config, 0),
233
+ )?;
234
+ module.define_singleton_method(
235
+ "mobile_sampling_config",
236
+ function!(mobile_sampling_config, 0),
237
+ )?;
238
+ module.define_singleton_method("cache_path", function!(cache_path, 0))?;
239
+
240
+ // -- Constants ------------------------------------------------------------
241
+
242
+ // Onde::NATIVE_VERSION (Rust crate version for parity checks).
243
+ module.const_set("NATIVE_VERSION", ruby.str_new(env!("CARGO_PKG_VERSION")))?;
244
+
245
+ // Onde::SUPPORTED_MODELS — frozen Array of model ID strings.
246
+ let model_ids = ruby.ary_new_capa(SUPPORTED_MODELS.len());
247
+ for id in SUPPORTED_MODELS {
248
+ model_ids.push(ruby.str_new(id))?;
249
+ }
250
+ model_ids.freeze();
251
+ module.const_set("SUPPORTED_MODELS", model_ids)?;
252
+
253
+ // Onde::SUPPORTED_MODEL_INFO — frozen Array of frozen Hashes.
254
+ let info_ary = ruby.ary_new_capa(SUPPORTED_MODEL_INFO.len());
255
+ for info in SUPPORTED_MODEL_INFO {
256
+ let hash: RHash = ruby.hash_new();
257
+ hash.aset(ruby.str_new("id"), ruby.str_new(info.id))?;
258
+ hash.aset(ruby.str_new("name"), ruby.str_new(info.name))?;
259
+ hash.aset(ruby.str_new("org"), ruby.str_new(info.org))?;
260
+ hash.aset(ruby.str_new("description"), ruby.str_new(info.description))?;
261
+ hash.aset(
262
+ ruby.str_new("expected_size_bytes"),
263
+ ruby.integer_from_u64(info.expected_size_bytes),
264
+ )?;
265
+ hash.freeze();
266
+ info_ary.push(hash)?;
267
+ }
268
+ info_ary.freeze();
269
+ module.const_set("SUPPORTED_MODEL_INFO", info_ary)?;
270
+
271
+ Ok(())
272
+ }
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Onde
4
+ VERSION = '0.1.0'
5
+ end
data/lib/onde.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'onde/version'
4
+ require_relative 'onde/onde_ruby'
5
+
6
+ module Onde
7
+ class Error < StandardError; end
8
+ end
@@ -0,0 +1,2 @@
1
+ [toolchain]
2
+ channel = "1.92"
metadata ADDED
@@ -0,0 +1,56 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: onde-inference
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Seto Elkahfi
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Ruby bindings for the Onde inference engine. Run LLMs and speech-to-text
13
+ locally with automatic HuggingFace model management, cache inspection, and GPU acceleration.
14
+ email:
15
+ - setoelkahfi@gmail.com
16
+ executables: []
17
+ extensions:
18
+ - ext/onde-ruby/Cargo.toml
19
+ extra_rdoc_files: []
20
+ files:
21
+ - Cargo.lock
22
+ - Cargo.toml
23
+ - README.md
24
+ - ext/onde-ruby/Cargo.lock
25
+ - ext/onde-ruby/Cargo.toml
26
+ - ext/onde-ruby/extconf.rb
27
+ - ext/onde-ruby/src/lib.rs
28
+ - lib/onde.rb
29
+ - lib/onde/version.rb
30
+ - rust-toolchain.toml
31
+ homepage: https://ondeinference.com
32
+ licenses:
33
+ - MIT
34
+ metadata:
35
+ homepage_uri: https://ondeinference.com
36
+ source_code_uri: https://github.com/ondeinference/onde
37
+ changelog_uri: https://github.com/ondeinference/onde/blob/main/CHANGELOG.md
38
+ cargo_crate_name: onde-ruby
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 3.0.0
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: 3.3.11
52
+ requirements: []
53
+ rubygems_version: 3.7.2
54
+ specification_version: 4
55
+ summary: On-device AI inference for Ruby — powered by Rust.
56
+ test_files: []