xybrid-sdk 0.1.1

Developer-facing API for hybrid cloud-edge AI inference: load/run/stream models with declarative routing.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
//! Model source definitions for xybrid-sdk.
//!
//! This module defines `ModelSource`, which specifies where to load a model from:
//! - Registry: Resolve via registry API and download from HuggingFace (recommended)
//! - Bundle: Load from local .xyb file
//! - Directory: Load from local model directory (development)
//! - HuggingFace: Download directly from a HuggingFace Hub repository
//! - LegacyRegistry: (deprecated) Direct URL-based download

use std::path::PathBuf;

/// Source for loading a model.
///
/// Determines where the model files come from before loading.
#[derive(Debug, Clone)]
pub enum ModelSource {
    /// Load via registry API resolution (recommended).
    ///
    /// Uses `RegistryClient` to resolve the model ID to the best variant for the
    /// current platform, then downloads from HuggingFace with caching and
    /// SHA256 verification.
    ///
    /// # Example
    /// ```no_run
    /// # use xybrid_sdk::ModelSource;
    /// let _ = ModelSource::Registry {
    ///     id: "kokoro-82m".to_string(),
    ///     platform: None, // Auto-detect
    /// };
    /// ```
    Registry {
        /// Model ID (e.g., "kokoro-82m", "whisper-tiny")
        id: String,
        /// Target platform (auto-detected if None)
        platform: Option<String>,
    },

    /// Load from legacy HTTP registry with direct URL construction.
    ///
    /// # Deprecated
    /// Use `ModelSource::Registry` instead. This variant uses direct URL construction
    /// which is less flexible than registry API resolution.
    ///
    /// # Example
    /// ```no_run
    /// # #![allow(deprecated)]
    /// # use xybrid_sdk::ModelSource;
    /// let _ = ModelSource::LegacyRegistry {
    ///     url: "http://localhost:8080".to_string(),
    ///     model_id: "whisper-tiny".to_string(),
    ///     version: "1.0".to_string(),
    ///     platform: None, // Auto-detect
    /// };
    /// ```
    #[deprecated(since = "0.0.17", note = "Use ModelSource::Registry instead")]
    LegacyRegistry {
        /// Registry base URL
        url: String,
        /// Model identifier
        model_id: String,
        /// Model version
        version: String,
        /// Target platform (auto-detected if None)
        platform: Option<String>,
    },

    /// Load from local .xyb bundle file.
    ///
    /// # Example
    /// ```no_run
    /// # use xybrid_sdk::ModelSource;
    /// # use std::path::PathBuf;
    /// let _ = ModelSource::Bundle {
    ///     path: PathBuf::from("models/whisper-tiny.xyb"),
    /// };
    /// ```
    Bundle {
        /// Path to the .xyb bundle file
        path: PathBuf,
    },

    /// Load from local model directory (development mode).
    ///
    /// The directory must contain `model_metadata.json` and model files.
    ///
    /// # Example
    /// ```no_run
    /// # use xybrid_sdk::ModelSource;
    /// # use std::path::PathBuf;
    /// let _ = ModelSource::Directory {
    ///     path: PathBuf::from("/path/to/whisper-model"),
    /// };
    /// ```
    Directory {
        /// Path to model directory containing model_metadata.json
        path: PathBuf,
    },

    /// Load from HuggingFace Hub repository.
    ///
    /// Downloads model files from the HuggingFace Hub and caches them locally
    /// at `~/.xybrid/cache/hf/{repo}/`. Subsequent calls use the cached files.
    ///
    /// The repository must contain a `model_metadata.json` file, or one will be
    /// auto-generated in a future version.
    ///
    /// # Example
    /// ```no_run
    /// # use xybrid_sdk::ModelSource;
    /// let _ = ModelSource::HuggingFace {
    ///     repo: "xybrid-ai/kokoro-82m".to_string(),
    ///     revision: None, // Uses default branch
    ///     variant: None, // Auto-selects Q4_K_M for GGUF repos
    /// };
    /// ```
    HuggingFace {
        /// HuggingFace repository ID (e.g., "xybrid-ai/kokoro-82m")
        repo: String,
        /// Git revision (branch, tag, or commit hash). Uses default branch if None.
        revision: Option<String>,
        /// Preferred GGUF quantization variant (e.g., "Q4_K_M", "Q8_0", "F16").
        /// If None, defaults to Q4_K_M when multiple GGUF files are available.
        variant: Option<String>,
    },
}

impl ModelSource {
    /// Create a registry source with auto-detected platform (recommended).
    ///
    /// Uses the registry API to resolve the model ID to the best variant
    /// for the current platform.
    ///
    /// # Example
    /// ```no_run
    /// # use xybrid_sdk::ModelSource;
    /// let source = ModelSource::registry("kokoro-82m");
    /// ```
    pub fn registry(id: impl Into<String>) -> Self {
        ModelSource::Registry {
            id: id.into(),
            platform: None,
        }
    }

    /// Create a registry source with explicit platform.
    ///
    /// # Example
    /// ```no_run
    /// # use xybrid_sdk::ModelSource;
    /// let source = ModelSource::registry_with_platform("kokoro-82m", "macos-arm64");
    /// ```
    pub fn registry_with_platform(id: impl Into<String>, platform: impl Into<String>) -> Self {
        ModelSource::Registry {
            id: id.into(),
            platform: Some(platform.into()),
        }
    }

    /// Create a legacy registry source with auto-detected platform.
    ///
    /// # Deprecated
    /// Use `ModelSource::registry()` instead.
    #[deprecated(since = "0.0.17", note = "Use ModelSource::registry() instead")]
    #[allow(deprecated)]
    pub fn legacy_registry(
        url: impl Into<String>,
        model_id: impl Into<String>,
        version: impl Into<String>,
    ) -> Self {
        ModelSource::LegacyRegistry {
            url: url.into(),
            model_id: model_id.into(),
            version: version.into(),
            platform: None,
        }
    }

    /// Create a legacy registry source with explicit platform.
    ///
    /// # Deprecated
    /// Use `ModelSource::registry_with_platform()` instead.
    #[deprecated(
        since = "0.0.17",
        note = "Use ModelSource::registry_with_platform() instead"
    )]
    #[allow(deprecated)]
    pub fn legacy_registry_with_platform(
        url: impl Into<String>,
        model_id: impl Into<String>,
        version: impl Into<String>,
        platform: impl Into<String>,
    ) -> Self {
        ModelSource::LegacyRegistry {
            url: url.into(),
            model_id: model_id.into(),
            version: version.into(),
            platform: Some(platform.into()),
        }
    }

    /// Create a bundle source.
    pub fn bundle(path: impl Into<PathBuf>) -> Self {
        ModelSource::Bundle { path: path.into() }
    }

    /// Create a directory source.
    pub fn directory(path: impl Into<PathBuf>) -> Self {
        ModelSource::Directory { path: path.into() }
    }

    /// Create a HuggingFace Hub source with default revision.
    pub fn huggingface(repo: impl Into<String>) -> Self {
        ModelSource::HuggingFace {
            repo: repo.into(),
            revision: None,
            variant: None,
        }
    }

    /// Create a HuggingFace Hub source with explicit revision.
    pub fn huggingface_with_revision(repo: impl Into<String>, revision: impl Into<String>) -> Self {
        ModelSource::HuggingFace {
            repo: repo.into(),
            revision: Some(revision.into()),
            variant: None,
        }
    }

    /// Create a HuggingFace Hub source with explicit variant (GGUF quantization).
    ///
    /// The variant selects which GGUF file to download from repos with multiple
    /// quantization options (e.g., "Q4_K_M", "Q8_0", "F16").
    pub fn huggingface_with_variant(repo: impl Into<String>, variant: impl Into<String>) -> Self {
        ModelSource::HuggingFace {
            repo: repo.into(),
            revision: None,
            variant: Some(variant.into()),
        }
    }

    /// Parse a HuggingFace repo string that may include a variant suffix.
    ///
    /// Supports the format `"org/repo:variant"` (e.g., `"LiquidAI/LFM2.5-350M-GGUF:Q8_0"`).
    /// If no colon is present, returns the repo as-is with no variant.
    pub fn parse_huggingface(input: &str) -> Self {
        if let Some((repo, variant)) = input.rsplit_once(':') {
            // Avoid treating "https://..." as variant syntax
            if repo.contains('/') && !repo.contains("://") {
                ModelSource::HuggingFace {
                    repo: repo.to_string(),
                    revision: None,
                    variant: Some(variant.to_string()),
                }
            } else {
                ModelSource::huggingface(input)
            }
        } else {
            ModelSource::huggingface(input)
        }
    }

    /// Get the source type as a string.
    #[allow(deprecated)]
    pub fn source_type(&self) -> &'static str {
        match self {
            ModelSource::Registry { .. } => "registry",
            ModelSource::LegacyRegistry { .. } => "legacy_registry",
            ModelSource::Bundle { .. } => "bundle",
            ModelSource::Directory { .. } => "directory",
            ModelSource::HuggingFace { .. } => "huggingface",
        }
    }

    /// Get the model ID (if available from source).
    #[allow(deprecated)]
    pub fn model_id(&self) -> Option<&str> {
        match self {
            ModelSource::Registry { id, .. } => Some(id),
            ModelSource::LegacyRegistry { model_id, .. } => Some(model_id),
            ModelSource::HuggingFace { repo, .. } => Some(repo),
            _ => None,
        }
    }

    /// Get the version (if available from source).
    ///
    /// Note: Registry sources don't have a version - version is resolved by the registry API.
    #[allow(deprecated)]
    pub fn version(&self) -> Option<&str> {
        match self {
            ModelSource::LegacyRegistry { version, .. } => Some(version),
            ModelSource::HuggingFace { revision, .. } => revision.as_deref(),
            _ => None,
        }
    }

    /// Get the preferred GGUF variant (if specified).
    pub fn variant(&self) -> Option<&str> {
        match self {
            ModelSource::HuggingFace { variant, .. } => variant.as_deref(),
            _ => None,
        }
    }
}

/// Detect the current platform for registry downloads.
///
/// Delegates to [`crate::platform::current_platform`] — the single source of
/// truth for the `(target_os, target_arch)` → platform-string mapping.
/// Previously this carried its own duplicate `#[cfg]` ladder, which risked
/// drifting from `platform.rs`: this function feeds the registry download
/// path (`RegistryClient::resolve`, `ModelLoader::load_from_legacy_registry`)
/// while `platform::current_platform` feeds device telemetry, so a platform
/// added to one ladder but not the other would make the registry request a
/// different platform's bundle than the device reports — with no compile
/// error. One ladder, no drift (audit slice 3c).
pub fn detect_platform() -> String {
    crate::platform::current_platform().to_string()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_registry_source() {
        let source = ModelSource::registry("kokoro-82m");
        assert_eq!(source.source_type(), "registry");
        assert_eq!(source.model_id(), Some("kokoro-82m"));
        assert_eq!(source.version(), None); // Registry sources resolve version via API
    }

    #[test]
    fn test_registry_source_with_platform() {
        let source = ModelSource::registry_with_platform("whisper-tiny", "macos-arm64");
        assert_eq!(source.source_type(), "registry");
        assert_eq!(source.model_id(), Some("whisper-tiny"));
    }

    #[test]
    #[allow(deprecated)]
    fn test_legacy_registry_source() {
        let source = ModelSource::legacy_registry("http://localhost:8080", "whisper", "1.0");
        assert_eq!(source.source_type(), "legacy_registry");
        assert_eq!(source.model_id(), Some("whisper"));
        assert_eq!(source.version(), Some("1.0"));
    }

    #[test]
    fn test_bundle_source() {
        let source = ModelSource::bundle("models/test.xyb");
        assert_eq!(source.source_type(), "bundle");
        assert_eq!(source.model_id(), None);
    }

    #[test]
    fn test_directory_source() {
        let source = ModelSource::directory("/tmp/test-model");
        assert_eq!(source.source_type(), "directory");
    }

    #[test]
    fn test_huggingface_source() {
        let source = ModelSource::huggingface("xybrid-ai/kokoro-82m");
        assert_eq!(source.source_type(), "huggingface");
        assert_eq!(source.model_id(), Some("xybrid-ai/kokoro-82m"));
        assert_eq!(source.version(), None);
        assert_eq!(source.variant(), None);
    }

    #[test]
    fn test_huggingface_source_with_revision() {
        let source = ModelSource::huggingface_with_revision("xybrid-ai/kokoro-82m", "v1.0");
        assert_eq!(source.source_type(), "huggingface");
        assert_eq!(source.model_id(), Some("xybrid-ai/kokoro-82m"));
        assert_eq!(source.version(), Some("v1.0"));
    }

    #[test]
    fn test_huggingface_source_with_variant() {
        let source = ModelSource::huggingface_with_variant("LiquidAI/LFM2.5-350M-GGUF", "Q8_0");
        assert_eq!(source.model_id(), Some("LiquidAI/LFM2.5-350M-GGUF"));
        assert_eq!(source.variant(), Some("Q8_0"));
    }

    #[test]
    fn test_parse_huggingface_with_variant() {
        let source = ModelSource::parse_huggingface("LiquidAI/LFM2.5-350M-GGUF:Q8_0");
        assert_eq!(source.model_id(), Some("LiquidAI/LFM2.5-350M-GGUF"));
        assert_eq!(source.variant(), Some("Q8_0"));
    }

    #[test]
    fn test_parse_huggingface_without_variant() {
        let source = ModelSource::parse_huggingface("xybrid-ai/kokoro-82m");
        assert_eq!(source.model_id(), Some("xybrid-ai/kokoro-82m"));
        assert_eq!(source.variant(), None);
    }

    #[test]
    fn test_detect_platform() {
        let platform = detect_platform();
        assert!(!platform.is_empty());
    }
}