Skip to main content

skippy_ffi/
lib.rs

1pub const ABI_VERSION_MAJOR: u32 = 0;
2pub const ABI_VERSION_MINOR: u32 = 1;
3pub const ABI_VERSION_PATCH: u32 = 35;
4pub const FEATURE_BACKEND_DEVICES: u64 = 1 << 23;
5pub const FEATURE_RUNTIME_EVENTS: u64 = 1 << 24;
6pub const FEATURE_NATIVE_MTP_N1: u64 = 1 << 25;
7pub const FEATURE_NGRAM_CACHE_DRAFT: u64 = 1 << 26;
8pub const FEATURE_INKLING_MTP_MM: u64 = 1 << 27;
9
10#[cfg(feature = "dynamic-runtime")]
11mod dynamic_library;
12
13#[repr(C)]
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct AbiVersion {
16    pub major: u32,
17    pub minor: u32,
18    pub patch: u32,
19}
20
21/// Whether a native runtime reporting `version` can back this binary's ABI
22/// bindings. Required symbol signatures and by-value struct layouts may change
23/// between patches, so the loader requires an exact ABI match.
24pub const fn runtime_abi_supported(version: AbiVersion) -> bool {
25    version.major == ABI_VERSION_MAJOR
26        && version.minor == ABI_VERSION_MINOR
27        && version.patch == ABI_VERSION_PATCH
28}
29
30use std::ffi::{c_char, c_int, c_void};
31
32pub type LlamaLogCallback =
33    Option<unsafe extern "C" fn(level: c_int, text: *const c_char, user_data: *mut c_void)>;
34pub type MtmdProgressCallback =
35    Option<unsafe extern "C" fn(progress: f32, user_data: *mut c_void) -> bool>;
36pub type SkippyRuntimeEventCallback =
37    Option<unsafe extern "C" fn(event: *const SkippyRuntimeEventV1, user_data: *mut c_void)>;
38
39#[repr(transparent)]
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41pub struct SkippyRuntimeEventCategory(pub u32);
42
43impl SkippyRuntimeEventCategory {
44    pub const MODEL_OPEN: Self = Self(1);
45    pub const BACKEND: Self = Self(2);
46    pub const SESSION: Self = Self(3);
47    pub const KV: Self = Self(4);
48    pub const WARNING: Self = Self(5);
49}
50
51#[repr(transparent)]
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
53pub struct SkippyRuntimeEventKind(pub u32);
54
55impl SkippyRuntimeEventKind {
56    pub const MODEL_OPEN_STARTED: Self = Self(1);
57    pub const MODEL_OPEN_PROGRESS: Self = Self(2);
58    pub const BACKEND_DEVICE_SELECTED: Self = Self(3);
59    pub const MODEL_OPEN_FINISHED: Self = Self(4);
60    pub const MODEL_OPEN_FAILED_HANDLED: Self = Self(5);
61}
62
63#[repr(transparent)]
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
65pub struct SkippyRuntimeEventEmitterKind(pub u32);
66
67impl SkippyRuntimeEventEmitterKind {
68    pub const UNKNOWN: Self = Self(0);
69    pub const OPEN_THREAD: Self = Self(1);
70    pub const WORKER_THREAD: Self = Self(2);
71}
72
73#[repr(transparent)]
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
75pub struct SkippyRuntimeEventProgressUnit(pub u32);
76
77impl SkippyRuntimeEventProgressUnit {
78    pub const NONE: Self = Self(0);
79    pub const BYTES: Self = Self(1);
80    pub const ITEMS: Self = Self(2);
81    pub const TENSORS: Self = Self(3);
82    pub const STEPS: Self = Self(4);
83}
84
85#[repr(transparent)]
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
87pub struct SkippyRuntimeEventFailureCode(pub u32);
88
89impl SkippyRuntimeEventFailureCode {
90    pub const NONE: Self = Self(0);
91    pub const INVALID_ARGUMENT: Self = Self(1);
92    pub const IO_ERROR: Self = Self(2);
93    pub const MODEL_ERROR: Self = Self(3);
94    pub const RUNTIME_ERROR: Self = Self(4);
95    pub const BACKEND_ERROR: Self = Self(5);
96    pub const CANCELLED: Self = Self(6);
97    pub const INTERNAL_ERROR: Self = Self(7);
98}
99
100#[repr(C)]
101#[derive(Debug, Clone, Copy)]
102pub struct SkippyRuntimeEventV1 {
103    pub abi_version: u32,
104    pub struct_size: u32,
105    pub category: SkippyRuntimeEventCategory,
106    pub kind: SkippyRuntimeEventKind,
107    pub emitter: SkippyRuntimeEventEmitterKind,
108    pub reserved0: u32,
109    pub sequence: u64,
110    pub timestamp_mono_ns: u64,
111    pub model_id: u64,
112    pub stage_id: u64,
113    pub session_id: u64,
114    pub progress_current: u64,
115    pub progress_total: u64,
116    pub progress_unit: SkippyRuntimeEventProgressUnit,
117    pub failure_code: SkippyRuntimeEventFailureCode,
118    pub status: Status,
119    pub reserved1: u32,
120    pub detail_ptr: *const c_char,
121    pub detail_len: u64,
122}
123
124#[repr(C)]
125#[derive(Debug, Clone, Copy)]
126pub struct SkippyRuntimeEventReporterV1 {
127    pub abi_version: u32,
128    pub struct_size: u32,
129    pub callback: SkippyRuntimeEventCallback,
130    pub user_data: *mut c_void,
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134#[repr(i32)]
135pub enum Status {
136    Ok = 0,
137    Error = 1,
138    InvalidArgument = 2,
139    Unsupported = 3,
140    BufferTooSmall = 4,
141    IoError = 5,
142    ModelError = 6,
143    RuntimeError = 7,
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147#[repr(i32)]
148pub enum LoadMode {
149    RuntimeSlice = 0,
150    LayerPackage = 1,
151    ArtifactSlice = 2,
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155#[repr(i32)]
156pub enum TensorRole {
157    Unknown = 0,
158    Metadata = 1,
159    Tokenizer = 2,
160    Embedding = 3,
161    Layer = 4,
162    FinalNorm = 5,
163    Output = 6,
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167#[repr(i32)]
168pub enum ActivationDType {
169    Unknown = 0,
170    F32 = 1,
171    F16 = 2,
172    Bf16 = 3,
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176#[repr(i32)]
177pub enum ActivationLayout {
178    Opaque = 0,
179    TokenMajor = 1,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183#[repr(i32)]
184pub enum BackendDeviceType {
185    Cpu = 0,
186    Gpu = 1,
187    IGpu = 2,
188    Accel = 3,
189    Meta = 4,
190}
191
192pub const BACKEND_DEVICE_CAP_ASYNC: u64 = 1 << 0;
193pub const BACKEND_DEVICE_CAP_HOST_BUFFER: u64 = 1 << 1;
194pub const BACKEND_DEVICE_CAP_BUFFER_FROM_HOST_PTR: u64 = 1 << 2;
195pub const BACKEND_DEVICE_CAP_EVENTS: u64 = 1 << 3;
196
197#[repr(C)]
198pub struct Error {
199    pub status: Status,
200    pub message: *const c_char,
201}
202
203#[repr(C)]
204#[derive(Debug, Clone, Copy)]
205pub struct RuntimeConfig {
206    pub stage_index: i32,
207    pub layer_start: i32,
208    pub layer_end: i32,
209    pub ctx_size: i32,
210    pub lane_count: i32,
211    pub n_batch: i32,
212    pub n_ubatch: i32,
213    pub n_threads: i32,
214    pub n_threads_batch: i32,
215    pub n_gpu_layers: i32,
216    pub has_mmap_override: bool,
217    pub use_mmap: bool,
218    pub use_mlock: bool,
219    pub cache_type_k: i32,
220    pub cache_type_v: i32,
221    pub flash_attn_type: i32,
222    pub load_mode: LoadMode,
223    pub disable_repack: bool,
224    pub use_mmap_prefetch: bool,
225    pub use_mmap_buffer: bool,
226    pub filter_tensors_on_load: bool,
227    pub include_embeddings: bool,
228    pub include_output: bool,
229    pub selected_backend_device: *const c_char,
230    pub glm_dsa_policy_profile: i32,
231    pub glm_dsa_policy_flags: u32,
232    pub glm_dsa_short_prefill_max_tokens: i32,
233    pub glm_dsa_direct_sparse_decode_max_top_k: i32,
234    pub glm_dsa_dense_sparse_mask_max_bytes: u64,
235    pub glm_dsa_compact_flash_min_kv: i32,
236}
237
238#[repr(C)]
239#[derive(Debug, Clone, Copy)]
240pub struct BackendDevice {
241    pub version: u32,
242    pub name: *const c_char,
243    pub description: *const c_char,
244    pub device_id: *const c_char,
245    pub memory_free: u64,
246    pub memory_total: u64,
247    pub device_type: BackendDeviceType,
248    pub caps: u64,
249}
250
251#[repr(C)]
252pub struct Model {
253    _private: [u8; 0],
254}
255
256#[repr(C)]
257pub struct NgramCache {
258    _private: [u8; 0],
259}
260
261#[repr(C)]
262pub struct Session {
263    _private: [u8; 0],
264}
265
266#[repr(C)]
267pub struct ModelInfo {
268    _private: [u8; 0],
269}
270
271pub type SkippyModelAttachMtpDraftModelFn = unsafe extern "C" fn(
272    target_model: *mut Model,
273    path: *const c_char,
274    config: *const RuntimeConfig,
275    out_error: *mut *mut Error,
276) -> Status;
277
278pub type SkippyDecodeStepSampledMtpFn = unsafe extern "C" fn(
279    session: *mut Session,
280    token_id: i32,
281    sampling: *const SamplingConfig,
282    out_predicted_token: *mut i32,
283    max_draft_tokens: usize,
284    out_mtp_draft: *mut NativeMtpDraft,
285    out_error: *mut *mut Error,
286) -> Status;
287
288#[repr(C)]
289#[derive(Debug, Clone, Copy)]
290pub struct ChatMessage {
291    pub role: *const c_char,
292    pub content: *const c_char,
293}
294
295#[repr(C)]
296pub struct SlicePlan {
297    _private: [u8; 0],
298}
299
300#[repr(C)]
301pub struct MtmdContext {
302    _private: [u8; 0],
303}
304
305#[repr(C)]
306pub struct MtmdBitmap {
307    _private: [u8; 0],
308}
309
310#[repr(C)]
311pub struct MtmdInputChunks {
312    _private: [u8; 0],
313}
314
315#[repr(C)]
316#[derive(Debug, Clone, Copy, PartialEq, Eq)]
317pub enum MtmdInputChunkType {
318    Text = 0,
319    Image = 1,
320    Audio = 2,
321}
322
323#[repr(C)]
324#[derive(Debug, Clone, Copy)]
325pub struct MtmdDecoderPos {
326    pub t: u32,
327    pub x: u32,
328    pub y: u32,
329    pub z: u32,
330}
331
332#[repr(C)]
333#[derive(Debug, Clone, Copy)]
334pub struct MtmdInputText {
335    pub text: *const c_char,
336    pub add_special: bool,
337    pub parse_special: bool,
338}
339
340#[repr(C)]
341#[derive(Debug, Clone, Copy)]
342pub struct MtmdContextParams {
343    pub use_gpu: bool,
344    pub print_timings: bool,
345    pub n_threads: c_int,
346    pub image_marker: *const c_char,
347    pub media_marker: *const c_char,
348    pub flash_attn_type: c_int,
349    pub warmup: bool,
350    pub image_min_tokens: c_int,
351    pub image_max_tokens: c_int,
352    pub cb_eval: *mut c_void,
353    pub cb_eval_user_data: *mut c_void,
354    pub batch_max_tokens: c_int,
355    pub progress_callback: MtmdProgressCallback,
356    pub progress_callback_user_data: *mut c_void,
357}
358
359#[repr(C)]
360#[derive(Debug, Clone, Copy)]
361pub struct TensorInfo {
362    pub name: *const c_char,
363    pub layer_index: i32,
364    pub role: TensorRole,
365    pub ggml_type: u32,
366    pub byte_size: u64,
367    pub element_count: u64,
368}
369
370#[repr(C)]
371#[derive(Debug, Clone, Copy)]
372pub struct ActivationDesc {
373    pub version: u32,
374    pub dtype: ActivationDType,
375    pub layout: ActivationLayout,
376    pub producer_stage_index: i32,
377    pub layer_start: i32,
378    pub layer_end: i32,
379    pub token_count: u32,
380    pub sequence_count: u32,
381    pub payload_bytes: u64,
382    pub flags: u64,
383}
384
385pub const ACTIVATION_FLAG_INKLING_MTP_EMBD: u64 = 1 << 2;
386
387#[repr(C)]
388#[derive(Debug, Clone, Copy)]
389pub struct LogitBias {
390    pub token_id: i32,
391    pub bias: f32,
392}
393
394#[derive(Debug, Clone, Copy, PartialEq, Eq)]
395#[repr(i32)]
396pub enum LlamaFileType {
397    AllF32 = 0,
398    MostlyF16 = 1,
399    MostlyQ4_0 = 2,
400    MostlyQ4_1 = 3,
401    MostlyQ8_0 = 7,
402    MostlyQ5_0 = 8,
403    MostlyQ5_1 = 9,
404    MostlyQ2K = 10,
405    MostlyQ3KS = 11,
406    MostlyQ3KM = 12,
407    MostlyQ3KL = 13,
408    MostlyQ4KS = 14,
409    MostlyQ4KM = 15,
410    MostlyQ5KS = 16,
411    MostlyQ5KM = 17,
412    MostlyQ6K = 18,
413    MostlyIQ2XXS = 19,
414    MostlyIQ2XS = 20,
415    MostlyQ2KS = 21,
416    MostlyIQ3XS = 22,
417    MostlyIQ3XXS = 23,
418    MostlyIQ1S = 24,
419    MostlyIQ4NL = 25,
420    MostlyIQ3S = 26,
421    MostlyIQ3M = 27,
422    MostlyIQ2S = 28,
423    MostlyIQ2M = 29,
424    MostlyIQ4XS = 30,
425    MostlyIQ1M = 31,
426    MostlyBf16 = 32,
427    MostlyTQ1_0 = 36,
428    MostlyTQ2_0 = 37,
429    MostlyMxfp4Moe = 38,
430    MostlyNvfp4 = 39,
431    MostlyQ1_0 = 40,
432    MostlyQ2_0 = 41,
433}
434
435#[derive(Debug, Clone, Copy, PartialEq, Eq)]
436#[repr(i32)]
437pub enum GgmlType {
438    F32 = 0,
439    F16 = 1,
440    Q4_0 = 2,
441    Q4_1 = 3,
442    Q5_0 = 6,
443    Q5_1 = 7,
444    Q8_0 = 8,
445    Q8_1 = 9,
446    Q2K = 10,
447    Q3K = 11,
448    Q4K = 12,
449    Q5K = 13,
450    Q6K = 14,
451    Q8K = 15,
452    IQ2XXS = 16,
453    IQ2XS = 17,
454    IQ3XXS = 18,
455    IQ1S = 19,
456    IQ4NL = 20,
457    IQ3S = 21,
458    IQ2S = 22,
459    IQ4XS = 23,
460    I8 = 24,
461    I16 = 25,
462    I32 = 26,
463    I64 = 27,
464    F64 = 28,
465    IQ1M = 29,
466    Bf16 = 30,
467    TQ1_0 = 34,
468    TQ2_0 = 35,
469    Mxfp4 = 39,
470    Nvfp4 = 40,
471    Q1_0 = 41,
472    Q2_0 = 42,
473    Count = 43,
474}
475
476#[derive(Debug, Clone, Copy, PartialEq, Eq)]
477#[repr(i32)]
478pub enum LlamaModelKvOverrideType {
479    Int = 0,
480    Float = 1,
481    Bool = 2,
482    Str = 3,
483}
484
485#[repr(C)]
486#[derive(Clone, Copy)]
487pub union LlamaModelKvOverrideValue {
488    pub val_i64: i64,
489    pub val_f64: f64,
490    pub val_bool: bool,
491    pub val_str: [c_char; 128],
492}
493
494#[repr(C)]
495#[derive(Clone, Copy)]
496pub struct LlamaModelKvOverride {
497    pub tag: LlamaModelKvOverrideType,
498    pub key: [c_char; 128],
499    pub value: LlamaModelKvOverrideValue,
500}
501
502#[repr(C)]
503#[derive(Debug, Clone, Copy)]
504pub struct LlamaModelTensorOverride {
505    pub pattern: *const c_char,
506    pub tensor_type: GgmlType,
507}
508
509#[repr(C)]
510#[derive(Debug, Clone, Copy)]
511pub struct LlamaModelImatrixData {
512    pub name: *const c_char,
513    pub data: *const f32,
514    pub size: usize,
515}
516
517#[repr(C)]
518#[derive(Debug, Clone, Copy)]
519pub struct LlamaModelQuantizeParams {
520    pub nthread: i32,
521    pub ftype: LlamaFileType,
522    pub output_tensor_type: GgmlType,
523    pub token_embedding_type: GgmlType,
524    pub allow_requantize: bool,
525    pub quantize_output_tensor: bool,
526    pub only_copy: bool,
527    pub pure: bool,
528    pub keep_split: bool,
529    pub dry_run: bool,
530    pub imatrix: *const LlamaModelImatrixData,
531    pub kv_overrides: *const LlamaModelKvOverride,
532    pub tt_overrides: *const LlamaModelTensorOverride,
533    pub prune_layers: *const i32,
534}
535
536#[repr(C)]
537#[derive(Debug, Clone, Copy)]
538pub struct SamplingConfig {
539    pub version: u32,
540    pub flags: u32,
541    pub seed: u32,
542    pub top_k: i32,
543    pub penalty_last_n: i32,
544    pub temperature: f32,
545    pub top_p: f32,
546    pub presence_penalty: f32,
547    pub frequency_penalty: f32,
548    pub repeat_penalty: f32,
549    pub logit_bias_count: u32,
550    pub min_p: f32,
551    pub logit_bias: [LogitBias; 256],
552}
553
554#[repr(C)]
555#[derive(Debug, Clone, Copy, Default)]
556pub struct KvPageDesc {
557    pub version: u32,
558    pub layer_start: i32,
559    pub layer_end: i32,
560    pub token_start: u64,
561    pub token_count: u64,
562    pub layer_count: u32,
563    pub k_type: u32,
564    pub v_type: u32,
565    pub k_row_bytes: u32,
566    pub v_row_bytes: u32,
567    pub v_element_bytes: u32,
568    pub payload_bytes: u64,
569    pub flags: u64,
570}
571
572pub const NATIVE_MTP_MAX_DRAFT_TOKENS: usize = 8;
573
574#[repr(C)]
575#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
576pub struct NativeMtpDraft {
577    pub version: u32,
578    pub available: bool,
579    pub token_count: i32,
580    pub token_ids: [i32; NATIVE_MTP_MAX_DRAFT_TOKENS],
581    pub proposal_compute_us: i64,
582}
583
584#[repr(C)]
585#[derive(Debug, Clone, Copy, Default, PartialEq)]
586pub struct TokenSignal {
587    pub entropy: f32,
588    pub top_logprob: f32,
589    pub second_logprob: f32,
590    pub margin: f32,
591    pub top_token: i32,
592    pub second_token: i32,
593}
594
595#[repr(C)]
596#[derive(Debug, Clone, Copy, Default, PartialEq)]
597pub struct GenerationSignalWindow {
598    pub token_count: u32,
599    pub mean_entropy: f32,
600    pub max_entropy: f32,
601    pub mean_margin: f32,
602    pub min_margin: f32,
603    pub high_entropy_count: u32,
604    pub repetition_count: u32,
605}
606
607#[cfg(not(feature = "dynamic-runtime"))]
608/// Mark the statically linked native runtime as already available.
609pub fn native_runtime_loaded() -> bool {
610    true
611}
612
613#[cfg(not(feature = "dynamic-runtime"))]
614/// No-op for statically linked builds.
615///
616/// # Safety
617///
618/// Static builds resolve the native ABI at process link/load time, so this
619/// function does not dereference the supplied path or mutate loader state.
620pub unsafe fn load_native_runtime_library(
621    _path: impl AsRef<std::path::Path>,
622) -> Result<(), NativeRuntimeLoadError> {
623    Ok(())
624}
625
626#[cfg(not(feature = "dynamic-runtime"))]
627/// No-op for statically linked builds.
628///
629/// # Safety
630///
631/// Static builds resolve the native ABI at process link/load time, so this
632/// function does not dereference the supplied paths or mutate loader state.
633pub unsafe fn load_native_runtime_libraries<I, P>(_paths: I) -> Result<(), NativeRuntimeLoadError>
634where
635    I: IntoIterator<Item = P>,
636    P: AsRef<std::path::Path>,
637{
638    Ok(())
639}
640
641#[derive(Debug)]
642pub enum NativeRuntimeLoadError {
643    Load(String),
644    AlreadyLoaded,
645}
646
647impl std::fmt::Display for NativeRuntimeLoadError {
648    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
649        match self {
650            Self::Load(message) => write!(f, "{message}"),
651            Self::AlreadyLoaded => write!(f, "native runtime library is already loaded"),
652        }
653    }
654}
655
656impl std::error::Error for NativeRuntimeLoadError {}
657
658#[cfg(feature = "dynamic-runtime")]
659mod dynamic {
660    use super::*;
661    use libloading::Library;
662    use std::sync::OnceLock;
663
664    static SYMBOLS: OnceLock<Symbols> = OnceLock::new();
665
666    pub fn native_runtime_loaded() -> bool {
667        SYMBOLS.get().is_some()
668    }
669
670    /// Load a native runtime library and resolve the Skippy ABI symbols from it.
671    ///
672    /// # Safety
673    ///
674    /// The caller must ensure the library is a MeshLLM native runtime built for
675    /// the running MeshLLM version and Skippy ABI. Loading an arbitrary library
676    /// can bind incompatible C symbols and cause undefined behavior in later FFI
677    /// calls.
678    pub unsafe fn load_native_runtime_library(
679        path: impl AsRef<std::path::Path>,
680    ) -> Result<(), NativeRuntimeLoadError> {
681        let symbols = unsafe { Symbols::load_paths(&[path.as_ref()]) }?;
682        SYMBOLS
683            .set(symbols)
684            .map_err(|_| NativeRuntimeLoadError::AlreadyLoaded)
685    }
686
687    /// Load native runtime libraries and resolve Skippy ABI symbols from them.
688    ///
689    /// Libraries are loaded in the provided order and symbols are resolved by
690    /// searching from the last library backwards, so dependencies should appear
691    /// before the primary `libllama`/`llama.dll` entry.
692    ///
693    /// # Safety
694    ///
695    /// The caller must ensure every library belongs to the same MeshLLM native
696    /// runtime artifact for the running MeshLLM version and Skippy ABI. Loading
697    /// arbitrary libraries can bind incompatible C symbols and cause undefined
698    /// behavior in later FFI calls.
699    pub unsafe fn load_native_runtime_libraries<I, P>(
700        paths: I,
701    ) -> Result<(), NativeRuntimeLoadError>
702    where
703        I: IntoIterator<Item = P>,
704        P: AsRef<std::path::Path>,
705    {
706        let collected = paths
707            .into_iter()
708            .map(|path| path.as_ref().to_path_buf())
709            .collect::<Vec<_>>();
710        let symbols = unsafe { Symbols::load_paths(&collected) }?;
711        SYMBOLS
712            .set(symbols)
713            .map_err(|_| NativeRuntimeLoadError::AlreadyLoaded)
714    }
715
716    fn symbols() -> &'static Symbols {
717        SYMBOLS
718            .get()
719            .expect("MeshLLM native runtime library has not been loaded")
720    }
721
722    type SkippyAbiVersionFn = unsafe extern "C" fn() -> AbiVersion;
723
724    fn check_runtime_abi(libraries: &[Library]) -> Result<(), NativeRuntimeLoadError> {
725        let mut abi_version = None;
726        for library in libraries.iter().rev() {
727            if let Ok(symbol) =
728                unsafe { library.get::<SkippyAbiVersionFn>(b"skippy_abi_version\0") }
729            {
730                abi_version = Some(unsafe { symbol() });
731                break;
732            }
733        }
734        let Some(version) = abi_version else {
735            return Err(NativeRuntimeLoadError::Load(
736                "native runtime symbol not found: skippy_abi_version".to_string(),
737            ));
738        };
739        if !runtime_abi_supported(version) {
740            return Err(NativeRuntimeLoadError::Load(format!(
741                "native runtime ABI {}.{}.{} is not compatible with required ABI {}.{}.{}",
742                version.major,
743                version.minor,
744                version.patch,
745                ABI_VERSION_MAJOR,
746                ABI_VERSION_MINOR,
747                ABI_VERSION_PATCH,
748            )));
749        }
750        Ok(())
751    }
752
753    macro_rules! dynamic_symbols {
754        ($($name:ident($($arg:ident: $arg_ty:ty),* $(,)?) $(-> $ret:ty)?;)+) => {
755            #[allow(non_snake_case)]
756            struct Symbols {
757                _libraries: Vec<Library>,
758                $($name: unsafe extern "C" fn($($arg_ty),*) $(-> $ret)?,)+
759            }
760
761            impl Symbols {
762                unsafe fn load_paths<P>(paths: &[P]) -> Result<Self, NativeRuntimeLoadError>
763                where
764                    P: AsRef<std::path::Path>,
765                {
766                    if paths.is_empty() {
767                        return Err(NativeRuntimeLoadError::Load(
768                            "native runtime did not provide any libraries".to_string(),
769                        ));
770                    }
771                    let mut libraries = Vec::with_capacity(paths.len());
772                    for path in paths {
773                        libraries.push(
774                            unsafe { crate::dynamic_library::load(path.as_ref()) }
775                                .map_err(|err| NativeRuntimeLoadError::Load(err.to_string()))?,
776                        );
777                    }
778                    check_runtime_abi(&libraries)?;
779                    $(
780                        let mut $name = None;
781                        for library in libraries.iter().rev() {
782                            if let Ok(symbol) = unsafe {
783                                library.get::<unsafe extern "C" fn($($arg_ty),*) $(-> $ret)?>(
784                                    concat!(stringify!($name), "\0").as_bytes(),
785                                )
786                            } {
787                                $name = Some(*symbol);
788                                break;
789                            }
790                        };
791                        let $name = $name.ok_or_else(|| {
792                            NativeRuntimeLoadError::Load(format!(
793                                "native runtime symbol not found: {}",
794                                stringify!($name),
795                            ))
796                        })?;
797                    )+
798                    Ok(Self {
799                        _libraries: libraries,
800                        $($name,)+
801                    })
802                }
803            }
804
805            $(
806                #[allow(clippy::missing_safety_doc, clippy::too_many_arguments)]
807                pub unsafe fn $name($($arg: $arg_ty),*) $(-> $ret)? {
808                    unsafe { (symbols().$name)($($arg),*) }
809                }
810            )+
811        };
812    }
813
814    dynamic_symbols! {
815        llama_log_set(log_callback: LlamaLogCallback, user_data: *mut c_void);
816        ggml_log_set(log_callback: LlamaLogCallback, user_data: *mut c_void);
817        llama_model_quantize_default_params() -> LlamaModelQuantizeParams;
818        llama_model_quantize(fname_inp: *const c_char, fname_out: *const c_char, params: *const LlamaModelQuantizeParams) -> u32;
819        skippy_error_free(error: *mut Error);
820        skippy_ngram_cache_create(ngram_min: u16, ngram_max: u16, out_cache: *mut *mut NgramCache, out_error: *mut *mut Error) -> Status;
821        skippy_ngram_cache_free(cache: *mut NgramCache);
822        skippy_ngram_cache_reset(cache: *mut NgramCache, token_ids: *const i32, token_count: usize, out_error: *mut *mut Error) -> Status;
823        skippy_ngram_cache_append(cache: *mut NgramCache, token_ids: *const i32, token_count: usize, out_error: *mut *mut Error) -> Status;
824        skippy_ngram_cache_draft(cache: *mut NgramCache, continuation_prefix: *const i32, continuation_prefix_count: usize, max_draft_tokens: u16, output_tokens: *mut i32, output_token_capacity: usize, out_token_count: *mut usize, out_error: *mut *mut Error) -> Status;
825        skippy_backend_device_count(out_count: *mut usize, out_error: *mut *mut Error) -> Status;
826        skippy_backend_device_at(index: usize, out_device: *mut BackendDevice, out_error: *mut *mut Error) -> Status;
827        skippy_model_open(path: *const c_char, config: *const RuntimeConfig, out_model: *mut *mut Model, out_error: *mut *mut Error) -> Status;
828        skippy_model_open_from_parts(paths: *const *const c_char, path_count: usize, config: *const RuntimeConfig, out_model: *mut *mut Model, out_error: *mut *mut Error) -> Status;
829        skippy_model_free(model: *mut Model, out_error: *mut *mut Error) -> Status;
830        skippy_model_llama_model(model: *const Model) -> *const Opaque;
831        skippy_session_create(model: *mut Model, out_session: *mut *mut Session, out_error: *mut *mut Error) -> Status;
832        skippy_session_create_from_resident_prefix(model: *mut Model, cache_seq_id: i32, token_ids: *const i32, token_count: usize, out_session: *mut *mut Session, out_error: *mut *mut Error) -> Status;
833        skippy_session_llama_context(session: *mut Session) -> *mut Opaque;
834        skippy_session_position(session: *const Session) -> i32;
835        skippy_session_batch_size(session: *const Session) -> i32;
836        skippy_session_begin_external_decode(session: *mut Session, out_error: *mut *mut Error) -> Status;
837        skippy_session_end_external_decode(session: *mut Session, out_error: *mut *mut Error) -> Status;
838        skippy_session_set_position(session: *mut Session, n_past: i32, out_error: *mut *mut Error) -> Status;
839        skippy_session_sample_current(session: *mut Session, sampling: *const SamplingConfig, out_predicted_token: *mut i32, out_error: *mut *mut Error) -> Status;
840        skippy_session_configure_chat_sampling(session: *mut Session, sampling: *const SamplingConfig, metadata_json: *const c_char, prompt_token_count: u64, out_error: *mut *mut Error) -> Status;
841        skippy_session_reset(session: *mut Session, out_error: *mut *mut Error) -> Status;
842        skippy_session_free(session: *mut Session, out_error: *mut *mut Error) -> Status;
843        skippy_prefill_chunk(session: *mut Session, token_ids: *const i32, token_count: usize, input_activations: *const c_void, input_activation_bytes: usize, output_activations: *mut c_void, output_activation_capacity: usize, out_output_activation_bytes: *mut usize, out_error: *mut *mut Error) -> Status;
844        skippy_verify_tokens(session: *mut Session, token_ids: *const i32, token_count: usize, output_tokens: *mut i32, output_token_capacity: usize, out_token_count: *mut usize, out_error: *mut *mut Error) -> Status;
845        skippy_decode_step_sampled(session: *mut Session, token_id: i32, sampling: *const SamplingConfig, input_activation: *const c_void, input_activation_bytes: usize, output_activation: *mut c_void, output_activation_capacity: usize, out_output_activation_bytes: *mut usize, out_predicted_token: *mut i32, out_error: *mut *mut Error) -> Status;
846        skippy_decode_batch_sampled(sessions: *const *mut Session, token_ids: *const i32, sampling: *const *const SamplingConfig, request_count: usize, out_predicted_tokens: *mut i32, predicted_token_capacity: usize, out_error: *mut *mut Error) -> Status;
847        skippy_prefill_chunk_frame(session: *mut Session, token_ids: *const i32, token_count: usize, input_desc: *const ActivationDesc, input_payload: *const c_void, output_desc: *mut ActivationDesc, output_payload: *mut c_void, output_payload_capacity: usize, out_output_payload_bytes: *mut usize, out_error: *mut *mut Error) -> Status;
848        skippy_prefill_chunk_frame_sampled(session: *mut Session, token_ids: *const i32, token_count: usize, sampling: *const SamplingConfig, input_desc: *const ActivationDesc, input_payload: *const c_void, output_desc: *mut ActivationDesc, output_payload: *mut c_void, output_payload_capacity: usize, out_output_payload_bytes: *mut usize, out_predicted_token: *mut i32, out_error: *mut *mut Error) -> Status;
849        skippy_prefill_chunk_frame_with_positions(session: *mut Session, token_ids: *const i32, token_count: usize, positions: *const i32, position_count: usize, input_desc: *const ActivationDesc, input_payload: *const c_void, output_desc: *mut ActivationDesc, output_payload: *mut c_void, output_payload_capacity: usize, out_output_payload_bytes: *mut usize, out_error: *mut *mut Error) -> Status;
850        skippy_prefill_chunk_frame_sampled_with_positions(session: *mut Session, token_ids: *const i32, token_count: usize, positions: *const i32, position_count: usize, sampling: *const SamplingConfig, input_desc: *const ActivationDesc, input_payload: *const c_void, output_desc: *mut ActivationDesc, output_payload: *mut c_void, output_payload_capacity: usize, out_output_payload_bytes: *mut usize, out_predicted_token: *mut i32, out_error: *mut *mut Error) -> Status;
851        skippy_decode_step_frame_sampled(session: *mut Session, token_id: i32, sampling: *const SamplingConfig, input_desc: *const ActivationDesc, input_payload: *const c_void, output_desc: *mut ActivationDesc, output_payload: *mut c_void, output_payload_capacity: usize, out_output_payload_bytes: *mut usize, out_predicted_token: *mut i32, out_error: *mut *mut Error) -> Status;
852        skippy_decode_step_frame_sampled_mtp(session: *mut Session, token_id: i32, sampling: *const SamplingConfig, input_desc: *const ActivationDesc, input_payload: *const c_void, output_desc: *mut ActivationDesc, output_payload: *mut c_void, output_payload_capacity: usize, out_output_payload_bytes: *mut usize, out_predicted_token: *mut i32, max_draft_tokens: usize, out_mtp_draft: *mut NativeMtpDraft, out_error: *mut *mut Error) -> Status;
853        skippy_decode_step_frame_batch_sampled(sessions: *const *mut Session, token_ids: *const i32, sampling: *const *const SamplingConfig, input_descs: *const *const ActivationDesc, input_payloads: *const *const c_void, output_descs: *mut ActivationDesc, output_payloads: *const *mut c_void, output_payload_capacities: *const usize, out_output_payload_bytes: *mut usize, out_predicted_tokens: *mut i32, predicted_token_capacity: usize, request_count: usize, out_error: *mut *mut Error) -> Status;
854        skippy_verify_tokens_frame_sampled(session: *mut Session, token_ids: *const i32, token_count: usize, sampling: *const SamplingConfig, input_desc: *const ActivationDesc, input_payload: *const c_void, output_desc: *mut ActivationDesc, output_payload: *mut c_void, output_payload_capacity: usize, out_output_payload_bytes: *mut usize, output_tokens: *mut i32, output_token_capacity: usize, out_token_count: *mut usize, max_draft_tokens: usize, out_mtp_draft: *mut NativeMtpDraft, out_error: *mut *mut Error) -> Status;
855        skippy_session_copy_output_activation_frame(session: *mut Session, token_count: usize, output_desc: *mut ActivationDesc, output_payload: *mut c_void, output_payload_capacity: usize, out_output_payload_bytes: *mut usize, out_error: *mut *mut Error) -> Status;
856        skippy_session_last_token_signal(session: *mut Session, out_signal: *mut TokenSignal, out_error: *mut *mut Error) -> Status;
857        skippy_session_signal_window(session: *mut Session, window_tokens: u32, out_window: *mut GenerationSignalWindow, out_error: *mut *mut Error) -> Status;
858        skippy_trim_session(session: *mut Session, token_count: u64, out_error: *mut *mut Error) -> Status;
859        skippy_retire_verify_checkpoint(session: *mut Session, token_start: u64, token_count: u64, out_error: *mut *mut Error) -> Status;
860        skippy_export_state(session: *mut Session, layer_start: i32, layer_end: i32, output: *mut c_void, output_capacity: usize, out_bytes: *mut usize, out_error: *mut *mut Error) -> Status;
861        skippy_import_state(session: *mut Session, layer_start: i32, layer_end: i32, input: *const c_void, input_bytes: usize, out_error: *mut *mut Error) -> Status;
862        skippy_export_full_state(session: *mut Session, layer_start: i32, layer_end: i32, output: *mut c_void, output_capacity: usize, out_bytes: *mut usize, out_error: *mut *mut Error) -> Status;
863        skippy_import_full_state(session: *mut Session, layer_start: i32, layer_end: i32, input: *const c_void, input_bytes: usize, out_error: *mut *mut Error) -> Status;
864        skippy_export_kv_page(session: *mut Session, layer_start: i32, layer_end: i32, token_start: u64, token_count: u64, out_desc: *mut KvPageDesc, output: *mut c_void, output_capacity: usize, out_bytes: *mut usize, out_error: *mut *mut Error) -> Status;
865        skippy_import_kv_page(session: *mut Session, desc: *const KvPageDesc, input: *const c_void, input_bytes: usize, out_error: *mut *mut Error) -> Status;
866        skippy_export_recurrent_state(session: *mut Session, output: *mut c_void, output_capacity: usize, out_bytes: *mut usize, out_error: *mut *mut Error) -> Status;
867        skippy_import_recurrent_state(session: *mut Session, input: *const c_void, input_bytes: usize, out_error: *mut *mut Error) -> Status;
868        skippy_session_save_prefix(session: *mut Session, cache_seq_id: i32, token_count: u64, out_error: *mut *mut Error) -> Status;
869        skippy_session_restore_prefix(session: *mut Session, cache_seq_id: i32, token_ids: *const i32, token_count: usize, out_error: *mut *mut Error) -> Status;
870        skippy_session_drop_sequence(session: *mut Session, seq_id: i32, out_error: *mut *mut Error) -> Status;
871        skippy_tokenize(model: *mut Model, text: *const c_char, add_special: bool, output_tokens: *mut i32, output_token_capacity: usize, out_token_count: *mut usize, out_error: *mut *mut Error) -> Status;
872        skippy_detokenize(model: *mut Model, tokens: *const i32, token_count: usize, output_text: *mut c_char, output_text_capacity: usize, out_text_bytes: *mut usize, out_error: *mut *mut Error) -> Status;
873        skippy_token_is_eog(model: *mut Model, token_id: i32, out_is_eog: *mut bool, out_error: *mut *mut Error) -> Status;
874        skippy_model_info_open(path: *const c_char, out_info: *mut *mut ModelInfo, out_error: *mut *mut Error) -> Status;
875        skippy_model_info_free(info: *mut ModelInfo, out_error: *mut *mut Error) -> Status;
876        skippy_model_info_tensor_count(info: *mut ModelInfo, out_count: *mut usize, out_error: *mut *mut Error) -> Status;
877        skippy_model_info_tensor_at(info: *mut ModelInfo, index: usize, out_tensor: *mut TensorInfo, out_error: *mut *mut Error) -> Status;
878        skippy_slice_plan_create(info: *mut ModelInfo, out_plan: *mut *mut SlicePlan, out_error: *mut *mut Error) -> Status;
879        skippy_slice_plan_free(plan: *mut SlicePlan, out_error: *mut *mut Error) -> Status;
880        skippy_slice_plan_add_layer_range(plan: *mut SlicePlan, stage_index: i32, layer_start: i32, layer_end: i32, include_embeddings: bool, include_output: bool, out_error: *mut *mut Error) -> Status;
881        skippy_write_slice_gguf(info: *mut ModelInfo, plan: *const SlicePlan, stage_index: i32, output_path: *const c_char, out_error: *mut *mut Error) -> Status;
882        skippy_write_gguf_from_parts(input_paths: *const *const c_char, input_count: usize, output_path: *const c_char, out_error: *mut *mut Error) -> Status;
883        mtmd_default_marker() -> *const c_char;
884        mtmd_helper_log_set(log_callback: LlamaLogCallback, user_data: *mut c_void);
885        mtmd_context_params_default() -> MtmdContextParams;
886        mtmd_init_from_file(mmproj_fname: *const c_char, text_model: *const Opaque, ctx_params: MtmdContextParams) -> *mut MtmdContext;
887        mtmd_free(ctx: *mut MtmdContext);
888        mtmd_helper_bitmap_init_from_buf(ctx: *mut MtmdContext, buf: *const u8, len: usize) -> *mut MtmdBitmap;
889        mtmd_bitmap_free(bitmap: *mut MtmdBitmap);
890        mtmd_input_chunks_init() -> *mut MtmdInputChunks;
891        mtmd_input_chunks_free(chunks: *mut MtmdInputChunks);
892        mtmd_tokenize(ctx: *mut MtmdContext, output: *mut MtmdInputChunks, text: *const MtmdInputText, bitmaps: *const *const MtmdBitmap, n_bitmaps: usize) -> c_int;
893        mtmd_helper_get_n_tokens(chunks: *const MtmdInputChunks) -> usize;
894        mtmd_helper_get_n_pos(chunks: *const MtmdInputChunks) -> i32;
895        mtmd_input_chunks_size(chunks: *const MtmdInputChunks) -> usize;
896        mtmd_input_chunks_get(chunks: *const MtmdInputChunks, index: usize) -> *const Opaque;
897        mtmd_decode_use_mrope(ctx: *const MtmdContext) -> bool;
898        mtmd_input_chunk_get_type(chunk: *const Opaque) -> MtmdInputChunkType;
899        mtmd_input_chunk_get_n_tokens(chunk: *const Opaque) -> usize;
900        mtmd_input_chunk_get_tokens_text(chunk: *const Opaque, out_count: *mut usize) -> *const i32;
901        mtmd_input_chunk_get_tokens_image(chunk: *const Opaque) -> *const Opaque;
902        mtmd_helper_image_get_decoder_pos(image: *const Opaque, pos_0: i32, out_pos: *mut MtmdDecoderPos);
903        mtmd_helper_eval_chunks(ctx: *mut MtmdContext, lctx: *mut Opaque, chunks: *const MtmdInputChunks, n_past: i32, seq_id: i32, n_batch: i32, logits_last: bool, new_n_past: *mut i32) -> c_int;
904        mtmd_helper_eval_chunk_single(ctx: *mut MtmdContext, lctx: *mut Opaque, chunk: *const Opaque, n_past: i32, seq_id: i32, n_batch: i32, logits_last: bool, new_n_past: *mut i32) -> c_int;
905    }
906
907    // -----------------------------------------------------------------------
908    // Optional symbols — not required for library load.
909    // Older runtimes may lack these and callers must check availability first.
910    // -----------------------------------------------------------------------
911
912    type SkippyAbiFeaturesFn = unsafe extern "C" fn() -> u64;
913    type SkippyModelOpenWithEventsFn = unsafe extern "C" fn(
914        path: *const c_char,
915        config: *const RuntimeConfig,
916        reporter: *const SkippyRuntimeEventReporterV1,
917        out_model: *mut *mut Model,
918        out_error: *mut *mut Error,
919    ) -> Status;
920    type SkippyModelOpenFromPartsWithEventsFn = unsafe extern "C" fn(
921        paths: *const *const c_char,
922        path_count: usize,
923        config: *const RuntimeConfig,
924        reporter: *const SkippyRuntimeEventReporterV1,
925        out_model: *mut *mut Model,
926        out_error: *mut *mut Error,
927    ) -> Status;
928    type SkippyApplyChatTemplateFn = unsafe extern "C" fn(
929        model: *mut Model,
930        messages: *const ChatMessage,
931        message_count: usize,
932        add_assistant: bool,
933        override_enable_thinking: bool,
934        enable_thinking: bool,
935        output_text: *mut c_char,
936        output_text_capacity: usize,
937        out_text_bytes: *mut usize,
938        out_error: *mut *mut Error,
939    ) -> Status;
940    type SkippyApplyChatTemplateJsonFn = unsafe extern "C" fn(
941        model: *mut Model,
942        messages_json: *const c_char,
943        tools_json: *const c_char,
944        tool_choice_json: *const c_char,
945        add_assistant: bool,
946        override_enable_thinking: bool,
947        enable_thinking: bool,
948        parallel_tool_calls: bool,
949        reasoning_format: *const c_char,
950        output_text: *mut c_char,
951        output_text_capacity: usize,
952        out_text_bytes: *mut usize,
953        output_metadata_json: *mut c_char,
954        output_metadata_json_capacity: usize,
955        out_metadata_json_bytes: *mut usize,
956        out_error: *mut *mut Error,
957    ) -> Status;
958    type SkippyParseChatResponseJsonFn = unsafe extern "C" fn(
959        generated_text: *const c_char,
960        metadata_json: *const c_char,
961        is_partial: bool,
962        output_message_json: *mut c_char,
963        output_message_json_capacity: usize,
964        out_message_json_bytes: *mut usize,
965        out_error: *mut *mut Error,
966    ) -> Status;
967
968    impl Symbols {
969        fn lookup_optional<Sym>(&self, name: &[u8]) -> Option<Sym>
970        where
971            Sym: Copy + 'static,
972        {
973            for library in self._libraries.iter().rev() {
974                if let Ok(sym) = unsafe { library.get::<Sym>(name) } {
975                    return Some(*sym);
976                }
977            }
978            None
979        }
980    }
981
982    pub fn skippy_abi_features_optional() -> Option<SkippyAbiFeaturesFn> {
983        static CACHE: OnceLock<Option<SkippyAbiFeaturesFn>> = OnceLock::new();
984        *CACHE.get_or_init(|| {
985            symbols().lookup_optional::<SkippyAbiFeaturesFn>(b"skippy_abi_features\0")
986        })
987    }
988
989    pub fn skippy_model_open_with_events_fn() -> Option<SkippyModelOpenWithEventsFn> {
990        static CACHE: OnceLock<Option<SkippyModelOpenWithEventsFn>> = OnceLock::new();
991        *CACHE.get_or_init(|| {
992            symbols()
993                .lookup_optional::<SkippyModelOpenWithEventsFn>(b"skippy_model_open_with_events\0")
994        })
995    }
996
997    pub fn skippy_model_attach_mtp_draft_model_fn() -> Option<SkippyModelAttachMtpDraftModelFn> {
998        static CACHE: OnceLock<Option<SkippyModelAttachMtpDraftModelFn>> = OnceLock::new();
999        *CACHE.get_or_init(|| {
1000            symbols().lookup_optional::<SkippyModelAttachMtpDraftModelFn>(
1001                b"skippy_model_attach_mtp_draft_model\0",
1002            )
1003        })
1004    }
1005
1006    pub fn skippy_decode_step_sampled_mtp_fn() -> Option<SkippyDecodeStepSampledMtpFn> {
1007        static CACHE: OnceLock<Option<SkippyDecodeStepSampledMtpFn>> = OnceLock::new();
1008        *CACHE.get_or_init(|| {
1009            symbols().lookup_optional::<SkippyDecodeStepSampledMtpFn>(
1010                b"skippy_decode_step_sampled_mtp\0",
1011            )
1012        })
1013    }
1014
1015    pub fn skippy_model_open_from_parts_with_events_fn()
1016    -> Option<SkippyModelOpenFromPartsWithEventsFn> {
1017        static CACHE: OnceLock<Option<SkippyModelOpenFromPartsWithEventsFn>> = OnceLock::new();
1018        *CACHE.get_or_init(|| {
1019            symbols().lookup_optional::<SkippyModelOpenFromPartsWithEventsFn>(
1020                b"skippy_model_open_from_parts_with_events\0",
1021            )
1022        })
1023    }
1024
1025    fn skippy_apply_chat_template_fn() -> Option<SkippyApplyChatTemplateFn> {
1026        static CACHE: OnceLock<Option<SkippyApplyChatTemplateFn>> = OnceLock::new();
1027        *CACHE.get_or_init(|| {
1028            symbols().lookup_optional::<SkippyApplyChatTemplateFn>(b"skippy_apply_chat_template\0")
1029        })
1030    }
1031
1032    fn skippy_apply_chat_template_json_fn() -> Option<SkippyApplyChatTemplateJsonFn> {
1033        static CACHE: OnceLock<Option<SkippyApplyChatTemplateJsonFn>> = OnceLock::new();
1034        *CACHE.get_or_init(|| {
1035            symbols().lookup_optional::<SkippyApplyChatTemplateJsonFn>(
1036                b"skippy_apply_chat_template_json\0",
1037            )
1038        })
1039    }
1040
1041    fn skippy_parse_chat_response_json_fn() -> Option<SkippyParseChatResponseJsonFn> {
1042        static CACHE: OnceLock<Option<SkippyParseChatResponseJsonFn>> = OnceLock::new();
1043        *CACHE.get_or_init(|| {
1044            symbols().lookup_optional::<SkippyParseChatResponseJsonFn>(
1045                b"skippy_parse_chat_response_json\0",
1046            )
1047        })
1048    }
1049
1050    #[allow(clippy::missing_safety_doc, clippy::too_many_arguments)]
1051    pub unsafe fn skippy_apply_chat_template(
1052        model: *mut Model,
1053        messages: *const ChatMessage,
1054        message_count: usize,
1055        add_assistant: bool,
1056        override_enable_thinking: bool,
1057        enable_thinking: bool,
1058        output_text: *mut c_char,
1059        output_text_capacity: usize,
1060        out_text_bytes: *mut usize,
1061        out_error: *mut *mut Error,
1062    ) -> Status {
1063        let Some(function) = skippy_apply_chat_template_fn() else {
1064            return Status::Unsupported;
1065        };
1066        unsafe {
1067            function(
1068                model,
1069                messages,
1070                message_count,
1071                add_assistant,
1072                override_enable_thinking,
1073                enable_thinking,
1074                output_text,
1075                output_text_capacity,
1076                out_text_bytes,
1077                out_error,
1078            )
1079        }
1080    }
1081
1082    #[allow(clippy::missing_safety_doc, clippy::too_many_arguments)]
1083    pub unsafe fn skippy_apply_chat_template_json(
1084        model: *mut Model,
1085        messages_json: *const c_char,
1086        tools_json: *const c_char,
1087        tool_choice_json: *const c_char,
1088        add_assistant: bool,
1089        override_enable_thinking: bool,
1090        enable_thinking: bool,
1091        parallel_tool_calls: bool,
1092        reasoning_format: *const c_char,
1093        output_text: *mut c_char,
1094        output_text_capacity: usize,
1095        out_text_bytes: *mut usize,
1096        output_metadata_json: *mut c_char,
1097        output_metadata_json_capacity: usize,
1098        out_metadata_json_bytes: *mut usize,
1099        out_error: *mut *mut Error,
1100    ) -> Status {
1101        let Some(function) = skippy_apply_chat_template_json_fn() else {
1102            return Status::Unsupported;
1103        };
1104        unsafe {
1105            function(
1106                model,
1107                messages_json,
1108                tools_json,
1109                tool_choice_json,
1110                add_assistant,
1111                override_enable_thinking,
1112                enable_thinking,
1113                parallel_tool_calls,
1114                reasoning_format,
1115                output_text,
1116                output_text_capacity,
1117                out_text_bytes,
1118                output_metadata_json,
1119                output_metadata_json_capacity,
1120                out_metadata_json_bytes,
1121                out_error,
1122            )
1123        }
1124    }
1125
1126    #[allow(clippy::missing_safety_doc, clippy::too_many_arguments)]
1127    pub unsafe fn skippy_parse_chat_response_json(
1128        generated_text: *const c_char,
1129        metadata_json: *const c_char,
1130        is_partial: bool,
1131        output_message_json: *mut c_char,
1132        output_message_json_capacity: usize,
1133        out_message_json_bytes: *mut usize,
1134        out_error: *mut *mut Error,
1135    ) -> Status {
1136        let Some(function) = skippy_parse_chat_response_json_fn() else {
1137            return Status::Unsupported;
1138        };
1139        unsafe {
1140            function(
1141                generated_text,
1142                metadata_json,
1143                is_partial,
1144                output_message_json,
1145                output_message_json_capacity,
1146                out_message_json_bytes,
1147                out_error,
1148            )
1149        }
1150    }
1151
1152    #[allow(clippy::missing_safety_doc, clippy::too_many_arguments)]
1153    pub unsafe fn skippy_decode_step_sampled_mtp(
1154        session: *mut Session,
1155        token_id: i32,
1156        sampling: *const SamplingConfig,
1157        out_predicted_token: *mut i32,
1158        max_draft_tokens: usize,
1159        out_mtp_draft: *mut NativeMtpDraft,
1160        out_error: *mut *mut Error,
1161    ) -> Status {
1162        let Some(function) = skippy_decode_step_sampled_mtp_fn() else {
1163            return Status::Unsupported;
1164        };
1165        unsafe {
1166            function(
1167                session,
1168                token_id,
1169                sampling,
1170                out_predicted_token,
1171                max_draft_tokens,
1172                out_mtp_draft,
1173                out_error,
1174            )
1175        }
1176    }
1177}
1178
1179#[cfg(feature = "dynamic-runtime")]
1180pub use dynamic::*;
1181
1182#[cfg(feature = "dynamic-runtime")]
1183/// Returns the skippy ABI feature bitmask.
1184/// Requires the native runtime to be loaded first (checked by caller).
1185pub fn skippy_abi_features() -> u64 {
1186    try_abi_features().expect("skippy_abi_features not available in loaded runtime")
1187}
1188
1189/// Returns the Skippy ABI feature bitmask when the loaded dynamic runtime
1190/// exports feature probing.
1191#[cfg(feature = "dynamic-runtime")]
1192pub fn try_abi_features() -> Option<u64> {
1193    dynamic::skippy_abi_features_optional().map(|features| unsafe { features() })
1194}
1195
1196/// Returns the active Skippy ABI feature bitmask through a safe Rust wrapper.
1197#[cfg(feature = "dynamic-runtime")]
1198pub fn abi_features() -> u64 {
1199    skippy_abi_features()
1200}
1201
1202/// Returns the statically linked Skippy ABI feature bitmask.
1203#[cfg(not(feature = "dynamic-runtime"))]
1204pub fn try_abi_features() -> Option<u64> {
1205    // SAFETY: the statically linked ABI exposes this nullary query with no
1206    // caller-owned pointers or lifetime requirements.
1207    Some(unsafe { skippy_abi_features() })
1208}
1209
1210/// Returns the statically linked Skippy ABI feature bitmask.
1211#[cfg(not(feature = "dynamic-runtime"))]
1212pub fn abi_features() -> u64 {
1213    // SAFETY: the statically linked ABI exposes this nullary query with no
1214    // caller-owned pointers or lifetime requirements.
1215    unsafe { skippy_abi_features() }
1216}
1217
1218#[cfg(not(feature = "dynamic-runtime"))]
1219unsafe extern "C" {
1220    pub fn llama_log_set(log_callback: LlamaLogCallback, user_data: *mut c_void);
1221
1222    pub fn ggml_log_set(log_callback: LlamaLogCallback, user_data: *mut c_void);
1223
1224    pub fn llama_model_quantize_default_params() -> LlamaModelQuantizeParams;
1225
1226    pub fn llama_model_quantize(
1227        fname_inp: *const c_char,
1228        fname_out: *const c_char,
1229        params: *const LlamaModelQuantizeParams,
1230    ) -> u32;
1231
1232    pub fn skippy_abi_features() -> u64;
1233
1234    pub fn skippy_error_free(error: *mut Error);
1235
1236    pub fn skippy_ngram_cache_create(
1237        ngram_min: u16,
1238        ngram_max: u16,
1239        out_cache: *mut *mut NgramCache,
1240        out_error: *mut *mut Error,
1241    ) -> Status;
1242
1243    pub fn skippy_ngram_cache_free(cache: *mut NgramCache);
1244
1245    pub fn skippy_ngram_cache_reset(
1246        cache: *mut NgramCache,
1247        token_ids: *const i32,
1248        token_count: usize,
1249        out_error: *mut *mut Error,
1250    ) -> Status;
1251
1252    pub fn skippy_ngram_cache_append(
1253        cache: *mut NgramCache,
1254        token_ids: *const i32,
1255        token_count: usize,
1256        out_error: *mut *mut Error,
1257    ) -> Status;
1258
1259    pub fn skippy_ngram_cache_draft(
1260        cache: *mut NgramCache,
1261        continuation_prefix: *const i32,
1262        continuation_prefix_count: usize,
1263        max_draft_tokens: u16,
1264        output_tokens: *mut i32,
1265        output_token_capacity: usize,
1266        out_token_count: *mut usize,
1267        out_error: *mut *mut Error,
1268    ) -> Status;
1269
1270    pub fn skippy_backend_device_count(out_count: *mut usize, out_error: *mut *mut Error)
1271    -> Status;
1272
1273    pub fn skippy_backend_device_at(
1274        index: usize,
1275        out_device: *mut BackendDevice,
1276        out_error: *mut *mut Error,
1277    ) -> Status;
1278
1279    pub fn skippy_model_open(
1280        path: *const c_char,
1281        config: *const RuntimeConfig,
1282        out_model: *mut *mut Model,
1283        out_error: *mut *mut Error,
1284    ) -> Status;
1285
1286    pub fn skippy_model_open_from_parts(
1287        paths: *const *const c_char,
1288        path_count: usize,
1289        config: *const RuntimeConfig,
1290        out_model: *mut *mut Model,
1291        out_error: *mut *mut Error,
1292    ) -> Status;
1293
1294    pub fn skippy_model_attach_mtp_draft_model(
1295        target_model: *mut Model,
1296        path: *const c_char,
1297        config: *const RuntimeConfig,
1298        out_error: *mut *mut Error,
1299    ) -> Status;
1300
1301    pub fn skippy_model_free(model: *mut Model, out_error: *mut *mut Error) -> Status;
1302
1303    pub fn skippy_model_llama_model(model: *const Model) -> *const Opaque;
1304
1305    pub fn skippy_session_create(
1306        model: *mut Model,
1307        out_session: *mut *mut Session,
1308        out_error: *mut *mut Error,
1309    ) -> Status;
1310
1311    pub fn skippy_session_create_from_resident_prefix(
1312        model: *mut Model,
1313        cache_seq_id: i32,
1314        token_ids: *const i32,
1315        token_count: usize,
1316        out_session: *mut *mut Session,
1317        out_error: *mut *mut Error,
1318    ) -> Status;
1319
1320    pub fn skippy_session_llama_context(session: *mut Session) -> *mut Opaque;
1321
1322    pub fn skippy_session_position(session: *const Session) -> i32;
1323
1324    pub fn skippy_session_batch_size(session: *const Session) -> i32;
1325
1326    pub fn skippy_session_begin_external_decode(
1327        session: *mut Session,
1328        out_error: *mut *mut Error,
1329    ) -> Status;
1330
1331    pub fn skippy_session_end_external_decode(
1332        session: *mut Session,
1333        out_error: *mut *mut Error,
1334    ) -> Status;
1335
1336    pub fn skippy_session_set_position(
1337        session: *mut Session,
1338        n_past: i32,
1339        out_error: *mut *mut Error,
1340    ) -> Status;
1341
1342    pub fn skippy_session_sample_current(
1343        session: *mut Session,
1344        sampling: *const SamplingConfig,
1345        out_predicted_token: *mut i32,
1346        out_error: *mut *mut Error,
1347    ) -> Status;
1348
1349    pub fn skippy_session_configure_chat_sampling(
1350        session: *mut Session,
1351        sampling: *const SamplingConfig,
1352        metadata_json: *const c_char,
1353        prompt_token_count: u64,
1354        out_error: *mut *mut Error,
1355    ) -> Status;
1356
1357    pub fn skippy_session_reset(session: *mut Session, out_error: *mut *mut Error) -> Status;
1358
1359    pub fn skippy_session_free(session: *mut Session, out_error: *mut *mut Error) -> Status;
1360
1361    pub fn skippy_prefill_chunk(
1362        session: *mut Session,
1363        token_ids: *const i32,
1364        token_count: usize,
1365        input_activations: *const c_void,
1366        input_activation_bytes: usize,
1367        output_activations: *mut c_void,
1368        output_activation_capacity: usize,
1369        out_output_activation_bytes: *mut usize,
1370        out_error: *mut *mut Error,
1371    ) -> Status;
1372
1373    pub fn skippy_verify_tokens(
1374        session: *mut Session,
1375        token_ids: *const i32,
1376        token_count: usize,
1377        output_tokens: *mut i32,
1378        output_token_capacity: usize,
1379        out_token_count: *mut usize,
1380        out_error: *mut *mut Error,
1381    ) -> Status;
1382
1383    pub fn skippy_decode_step_sampled(
1384        session: *mut Session,
1385        token_id: i32,
1386        sampling: *const SamplingConfig,
1387        input_activation: *const c_void,
1388        input_activation_bytes: usize,
1389        output_activation: *mut c_void,
1390        output_activation_capacity: usize,
1391        out_output_activation_bytes: *mut usize,
1392        out_predicted_token: *mut i32,
1393        out_error: *mut *mut Error,
1394    ) -> Status;
1395
1396    pub fn skippy_decode_step_sampled_mtp(
1397        session: *mut Session,
1398        token_id: i32,
1399        sampling: *const SamplingConfig,
1400        out_predicted_token: *mut i32,
1401        max_draft_tokens: usize,
1402        out_mtp_draft: *mut NativeMtpDraft,
1403        out_error: *mut *mut Error,
1404    ) -> Status;
1405
1406    pub fn skippy_decode_batch_sampled(
1407        sessions: *const *mut Session,
1408        token_ids: *const i32,
1409        sampling: *const *const SamplingConfig,
1410        request_count: usize,
1411        out_predicted_tokens: *mut i32,
1412        predicted_token_capacity: usize,
1413        out_error: *mut *mut Error,
1414    ) -> Status;
1415
1416    pub fn skippy_prefill_chunk_frame(
1417        session: *mut Session,
1418        token_ids: *const i32,
1419        token_count: usize,
1420        input_desc: *const ActivationDesc,
1421        input_payload: *const c_void,
1422        output_desc: *mut ActivationDesc,
1423        output_payload: *mut c_void,
1424        output_payload_capacity: usize,
1425        out_output_payload_bytes: *mut usize,
1426        out_error: *mut *mut Error,
1427    ) -> Status;
1428
1429    pub fn skippy_prefill_chunk_frame_sampled(
1430        session: *mut Session,
1431        token_ids: *const i32,
1432        token_count: usize,
1433        sampling: *const SamplingConfig,
1434        input_desc: *const ActivationDesc,
1435        input_payload: *const c_void,
1436        output_desc: *mut ActivationDesc,
1437        output_payload: *mut c_void,
1438        output_payload_capacity: usize,
1439        out_output_payload_bytes: *mut usize,
1440        out_predicted_token: *mut i32,
1441        out_error: *mut *mut Error,
1442    ) -> Status;
1443
1444    pub fn skippy_prefill_chunk_frame_with_positions(
1445        session: *mut Session,
1446        token_ids: *const i32,
1447        token_count: usize,
1448        positions: *const i32,
1449        position_count: usize,
1450        input_desc: *const ActivationDesc,
1451        input_payload: *const c_void,
1452        output_desc: *mut ActivationDesc,
1453        output_payload: *mut c_void,
1454        output_payload_capacity: usize,
1455        out_output_payload_bytes: *mut usize,
1456        out_error: *mut *mut Error,
1457    ) -> Status;
1458
1459    pub fn skippy_prefill_chunk_frame_sampled_with_positions(
1460        session: *mut Session,
1461        token_ids: *const i32,
1462        token_count: usize,
1463        positions: *const i32,
1464        position_count: usize,
1465        sampling: *const SamplingConfig,
1466        input_desc: *const ActivationDesc,
1467        input_payload: *const c_void,
1468        output_desc: *mut ActivationDesc,
1469        output_payload: *mut c_void,
1470        output_payload_capacity: usize,
1471        out_output_payload_bytes: *mut usize,
1472        out_predicted_token: *mut i32,
1473        out_error: *mut *mut Error,
1474    ) -> Status;
1475
1476    pub fn skippy_verify_tokens_frame_sampled(
1477        session: *mut Session,
1478        token_ids: *const i32,
1479        token_count: usize,
1480        sampling: *const SamplingConfig,
1481        input_desc: *const ActivationDesc,
1482        input_payload: *const c_void,
1483        output_desc: *mut ActivationDesc,
1484        output_payload: *mut c_void,
1485        output_payload_capacity: usize,
1486        out_output_payload_bytes: *mut usize,
1487        output_tokens: *mut i32,
1488        output_token_capacity: usize,
1489        out_token_count: *mut usize,
1490        max_draft_tokens: usize,
1491        out_mtp_draft: *mut NativeMtpDraft,
1492        out_error: *mut *mut Error,
1493    ) -> Status;
1494
1495    pub fn skippy_decode_step_frame_sampled(
1496        session: *mut Session,
1497        token_id: i32,
1498        sampling: *const SamplingConfig,
1499        input_desc: *const ActivationDesc,
1500        input_payload: *const c_void,
1501        output_desc: *mut ActivationDesc,
1502        output_payload: *mut c_void,
1503        output_payload_capacity: usize,
1504        out_output_payload_bytes: *mut usize,
1505        out_predicted_token: *mut i32,
1506        out_error: *mut *mut Error,
1507    ) -> Status;
1508
1509    pub fn skippy_decode_step_frame_sampled_mtp(
1510        session: *mut Session,
1511        token_id: i32,
1512        sampling: *const SamplingConfig,
1513        input_desc: *const ActivationDesc,
1514        input_payload: *const c_void,
1515        output_desc: *mut ActivationDesc,
1516        output_payload: *mut c_void,
1517        output_payload_capacity: usize,
1518        out_output_payload_bytes: *mut usize,
1519        out_predicted_token: *mut i32,
1520        max_draft_tokens: usize,
1521        out_mtp_draft: *mut NativeMtpDraft,
1522        out_error: *mut *mut Error,
1523    ) -> Status;
1524
1525    pub fn skippy_decode_step_frame_batch_sampled(
1526        sessions: *const *mut Session,
1527        token_ids: *const i32,
1528        sampling: *const *const SamplingConfig,
1529        input_descs: *const *const ActivationDesc,
1530        input_payloads: *const *const c_void,
1531        output_descs: *mut ActivationDesc,
1532        output_payloads: *const *mut c_void,
1533        output_payload_capacities: *const usize,
1534        out_output_payload_bytes: *mut usize,
1535        out_predicted_tokens: *mut i32,
1536        predicted_token_capacity: usize,
1537        request_count: usize,
1538        out_error: *mut *mut Error,
1539    ) -> Status;
1540
1541    pub fn skippy_session_copy_output_activation_frame(
1542        session: *mut Session,
1543        token_count: usize,
1544        output_desc: *mut ActivationDesc,
1545        output_payload: *mut c_void,
1546        output_payload_capacity: usize,
1547        out_output_payload_bytes: *mut usize,
1548        out_error: *mut *mut Error,
1549    ) -> Status;
1550
1551    pub fn skippy_session_last_token_signal(
1552        session: *mut Session,
1553        out_signal: *mut TokenSignal,
1554        out_error: *mut *mut Error,
1555    ) -> Status;
1556
1557    pub fn skippy_session_signal_window(
1558        session: *mut Session,
1559        window_tokens: u32,
1560        out_window: *mut GenerationSignalWindow,
1561        out_error: *mut *mut Error,
1562    ) -> Status;
1563
1564    pub fn skippy_trim_session(
1565        session: *mut Session,
1566        token_count: u64,
1567        out_error: *mut *mut Error,
1568    ) -> Status;
1569
1570    pub fn skippy_retire_verify_checkpoint(
1571        session: *mut Session,
1572        token_start: u64,
1573        token_count: u64,
1574        out_error: *mut *mut Error,
1575    ) -> Status;
1576
1577    pub fn skippy_export_state(
1578        session: *mut Session,
1579        layer_start: i32,
1580        layer_end: i32,
1581        output: *mut c_void,
1582        output_capacity: usize,
1583        out_bytes: *mut usize,
1584        out_error: *mut *mut Error,
1585    ) -> Status;
1586
1587    pub fn skippy_import_state(
1588        session: *mut Session,
1589        layer_start: i32,
1590        layer_end: i32,
1591        input: *const c_void,
1592        input_bytes: usize,
1593        out_error: *mut *mut Error,
1594    ) -> Status;
1595
1596    pub fn skippy_export_full_state(
1597        session: *mut Session,
1598        layer_start: i32,
1599        layer_end: i32,
1600        output: *mut c_void,
1601        output_capacity: usize,
1602        out_bytes: *mut usize,
1603        out_error: *mut *mut Error,
1604    ) -> Status;
1605
1606    pub fn skippy_import_full_state(
1607        session: *mut Session,
1608        layer_start: i32,
1609        layer_end: i32,
1610        input: *const c_void,
1611        input_bytes: usize,
1612        out_error: *mut *mut Error,
1613    ) -> Status;
1614
1615    pub fn skippy_export_kv_page(
1616        session: *mut Session,
1617        layer_start: i32,
1618        layer_end: i32,
1619        token_start: u64,
1620        token_count: u64,
1621        out_desc: *mut KvPageDesc,
1622        output: *mut c_void,
1623        output_capacity: usize,
1624        out_bytes: *mut usize,
1625        out_error: *mut *mut Error,
1626    ) -> Status;
1627
1628    pub fn skippy_import_kv_page(
1629        session: *mut Session,
1630        desc: *const KvPageDesc,
1631        input: *const c_void,
1632        input_bytes: usize,
1633        out_error: *mut *mut Error,
1634    ) -> Status;
1635
1636    pub fn skippy_export_recurrent_state(
1637        session: *mut Session,
1638        output: *mut c_void,
1639        output_capacity: usize,
1640        out_bytes: *mut usize,
1641        out_error: *mut *mut Error,
1642    ) -> Status;
1643
1644    pub fn skippy_import_recurrent_state(
1645        session: *mut Session,
1646        input: *const c_void,
1647        input_bytes: usize,
1648        out_error: *mut *mut Error,
1649    ) -> Status;
1650
1651    pub fn skippy_session_save_prefix(
1652        session: *mut Session,
1653        cache_seq_id: i32,
1654        token_count: u64,
1655        out_error: *mut *mut Error,
1656    ) -> Status;
1657
1658    pub fn skippy_session_restore_prefix(
1659        session: *mut Session,
1660        cache_seq_id: i32,
1661        token_ids: *const i32,
1662        token_count: usize,
1663        out_error: *mut *mut Error,
1664    ) -> Status;
1665
1666    pub fn skippy_session_drop_sequence(
1667        session: *mut Session,
1668        seq_id: i32,
1669        out_error: *mut *mut Error,
1670    ) -> Status;
1671
1672    pub fn skippy_tokenize(
1673        model: *mut Model,
1674        text: *const c_char,
1675        add_special: bool,
1676        output_tokens: *mut i32,
1677        output_token_capacity: usize,
1678        out_token_count: *mut usize,
1679        out_error: *mut *mut Error,
1680    ) -> Status;
1681
1682    pub fn skippy_detokenize(
1683        model: *mut Model,
1684        tokens: *const i32,
1685        token_count: usize,
1686        output_text: *mut c_char,
1687        output_text_capacity: usize,
1688        out_text_bytes: *mut usize,
1689        out_error: *mut *mut Error,
1690    ) -> Status;
1691
1692    pub fn skippy_token_is_eog(
1693        model: *mut Model,
1694        token_id: i32,
1695        out_is_eog: *mut bool,
1696        out_error: *mut *mut Error,
1697    ) -> Status;
1698
1699    pub fn skippy_apply_chat_template(
1700        model: *mut Model,
1701        messages: *const ChatMessage,
1702        message_count: usize,
1703        add_assistant: bool,
1704        override_enable_thinking: bool,
1705        enable_thinking: bool,
1706        output_text: *mut c_char,
1707        output_text_capacity: usize,
1708        out_text_bytes: *mut usize,
1709        out_error: *mut *mut Error,
1710    ) -> Status;
1711
1712    pub fn skippy_apply_chat_template_json(
1713        model: *mut Model,
1714        messages_json: *const c_char,
1715        tools_json: *const c_char,
1716        tool_choice_json: *const c_char,
1717        add_assistant: bool,
1718        override_enable_thinking: bool,
1719        enable_thinking: bool,
1720        parallel_tool_calls: bool,
1721        reasoning_format: *const c_char,
1722        output_text: *mut c_char,
1723        output_text_capacity: usize,
1724        out_text_bytes: *mut usize,
1725        output_metadata_json: *mut c_char,
1726        output_metadata_json_capacity: usize,
1727        out_metadata_json_bytes: *mut usize,
1728        out_error: *mut *mut Error,
1729    ) -> Status;
1730
1731    pub fn skippy_parse_chat_response_json(
1732        generated_text: *const c_char,
1733        metadata_json: *const c_char,
1734        is_partial: bool,
1735        output_message_json: *mut c_char,
1736        output_message_json_capacity: usize,
1737        out_message_json_bytes: *mut usize,
1738        out_error: *mut *mut Error,
1739    ) -> Status;
1740
1741    pub fn skippy_model_info_open(
1742        path: *const c_char,
1743        out_info: *mut *mut ModelInfo,
1744        out_error: *mut *mut Error,
1745    ) -> Status;
1746
1747    pub fn skippy_model_info_free(info: *mut ModelInfo, out_error: *mut *mut Error) -> Status;
1748
1749    pub fn skippy_model_info_tensor_count(
1750        info: *mut ModelInfo,
1751        out_count: *mut usize,
1752        out_error: *mut *mut Error,
1753    ) -> Status;
1754
1755    pub fn skippy_model_info_tensor_at(
1756        info: *mut ModelInfo,
1757        index: usize,
1758        out_tensor: *mut TensorInfo,
1759        out_error: *mut *mut Error,
1760    ) -> Status;
1761
1762    pub fn skippy_slice_plan_create(
1763        info: *mut ModelInfo,
1764        out_plan: *mut *mut SlicePlan,
1765        out_error: *mut *mut Error,
1766    ) -> Status;
1767
1768    pub fn skippy_slice_plan_free(plan: *mut SlicePlan, out_error: *mut *mut Error) -> Status;
1769
1770    pub fn skippy_slice_plan_add_layer_range(
1771        plan: *mut SlicePlan,
1772        stage_index: i32,
1773        layer_start: i32,
1774        layer_end: i32,
1775        include_embeddings: bool,
1776        include_output: bool,
1777        out_error: *mut *mut Error,
1778    ) -> Status;
1779
1780    pub fn skippy_write_slice_gguf(
1781        info: *mut ModelInfo,
1782        plan: *const SlicePlan,
1783        stage_index: i32,
1784        output_path: *const c_char,
1785        out_error: *mut *mut Error,
1786    ) -> Status;
1787
1788    pub fn skippy_write_gguf_from_parts(
1789        input_paths: *const *const c_char,
1790        input_count: usize,
1791        output_path: *const c_char,
1792        out_error: *mut *mut Error,
1793    ) -> Status;
1794
1795    pub fn mtmd_default_marker() -> *const c_char;
1796
1797    pub fn mtmd_helper_log_set(log_callback: LlamaLogCallback, user_data: *mut c_void);
1798
1799    pub fn mtmd_context_params_default() -> MtmdContextParams;
1800
1801    pub fn mtmd_init_from_file(
1802        mmproj_fname: *const c_char,
1803        text_model: *const Opaque,
1804        ctx_params: MtmdContextParams,
1805    ) -> *mut MtmdContext;
1806
1807    pub fn mtmd_free(ctx: *mut MtmdContext);
1808
1809    pub fn mtmd_helper_bitmap_init_from_buf(
1810        ctx: *mut MtmdContext,
1811        buf: *const u8,
1812        len: usize,
1813    ) -> *mut MtmdBitmap;
1814
1815    pub fn mtmd_bitmap_free(bitmap: *mut MtmdBitmap);
1816
1817    pub fn mtmd_input_chunks_init() -> *mut MtmdInputChunks;
1818
1819    pub fn mtmd_input_chunks_free(chunks: *mut MtmdInputChunks);
1820
1821    pub fn mtmd_tokenize(
1822        ctx: *mut MtmdContext,
1823        output: *mut MtmdInputChunks,
1824        text: *const MtmdInputText,
1825        bitmaps: *const *const MtmdBitmap,
1826        n_bitmaps: usize,
1827    ) -> c_int;
1828
1829    pub fn mtmd_helper_get_n_tokens(chunks: *const MtmdInputChunks) -> usize;
1830
1831    pub fn mtmd_helper_get_n_pos(chunks: *const MtmdInputChunks) -> i32;
1832
1833    pub fn mtmd_input_chunks_size(chunks: *const MtmdInputChunks) -> usize;
1834
1835    pub fn mtmd_input_chunks_get(chunks: *const MtmdInputChunks, index: usize) -> *const Opaque;
1836
1837    pub fn mtmd_decode_use_mrope(ctx: *const MtmdContext) -> bool;
1838
1839    pub fn mtmd_input_chunk_get_type(chunk: *const Opaque) -> MtmdInputChunkType;
1840
1841    pub fn mtmd_input_chunk_get_n_tokens(chunk: *const Opaque) -> usize;
1842
1843    pub fn mtmd_input_chunk_get_tokens_text(
1844        chunk: *const Opaque,
1845        out_count: *mut usize,
1846    ) -> *const i32;
1847
1848    pub fn mtmd_input_chunk_get_tokens_image(chunk: *const Opaque) -> *const Opaque;
1849
1850    pub fn mtmd_helper_image_get_decoder_pos(
1851        image: *const Opaque,
1852        pos_0: i32,
1853        out_pos: *mut MtmdDecoderPos,
1854    );
1855
1856    pub fn mtmd_helper_eval_chunks(
1857        ctx: *mut MtmdContext,
1858        lctx: *mut Opaque,
1859        chunks: *const MtmdInputChunks,
1860        n_past: i32,
1861        seq_id: i32,
1862        n_batch: i32,
1863        logits_last: bool,
1864        new_n_past: *mut i32,
1865    ) -> c_int;
1866
1867    pub fn mtmd_helper_eval_chunk_single(
1868        ctx: *mut MtmdContext,
1869        lctx: *mut Opaque,
1870        chunk: *const Opaque,
1871        n_past: i32,
1872        seq_id: i32,
1873        n_batch: i32,
1874        logits_last: bool,
1875        new_n_past: *mut i32,
1876    ) -> c_int;
1877}
1878
1879#[cfg(not(feature = "dynamic-runtime"))]
1880pub fn skippy_model_attach_mtp_draft_model_fn() -> Option<SkippyModelAttachMtpDraftModelFn> {
1881    Some(skippy_model_attach_mtp_draft_model)
1882}
1883
1884#[cfg(not(feature = "dynamic-runtime"))]
1885pub fn skippy_decode_step_sampled_mtp_fn() -> Option<SkippyDecodeStepSampledMtpFn> {
1886    Some(skippy_decode_step_sampled_mtp)
1887}
1888
1889pub type Opaque = c_void;
1890
1891#[cfg(test)]
1892mod tests {
1893    use super::*;
1894    use std::mem::{offset_of, size_of};
1895
1896    const fn version(major: u32, minor: u32, patch: u32) -> AbiVersion {
1897        AbiVersion {
1898            major,
1899            minor,
1900            patch,
1901        }
1902    }
1903
1904    #[test]
1905    fn accepts_current_patch_runtime() {
1906        assert!(runtime_abi_supported(version(
1907            ABI_VERSION_MAJOR,
1908            ABI_VERSION_MINOR,
1909            ABI_VERSION_PATCH,
1910        )));
1911    }
1912
1913    #[test]
1914    fn rejects_other_patch_runtimes() {
1915        assert!(!runtime_abi_supported(version(
1916            ABI_VERSION_MAJOR,
1917            ABI_VERSION_MINOR,
1918            ABI_VERSION_PATCH + 1,
1919        )));
1920        assert!(!runtime_abi_supported(version(
1921            ABI_VERSION_MAJOR,
1922            ABI_VERSION_MINOR,
1923            ABI_VERSION_PATCH - 1,
1924        )));
1925    }
1926
1927    #[test]
1928    fn rejects_major_and_minor_mismatches() {
1929        assert!(!runtime_abi_supported(version(
1930            ABI_VERSION_MAJOR + 1,
1931            ABI_VERSION_MINOR,
1932            ABI_VERSION_PATCH,
1933        )));
1934        assert!(!runtime_abi_supported(version(
1935            ABI_VERSION_MAJOR,
1936            ABI_VERSION_MINOR + 1,
1937            ABI_VERSION_PATCH,
1938        )));
1939    }
1940
1941    #[test]
1942    #[cfg(target_pointer_width = "64")]
1943    fn mtmd_context_params_matches_native_layout() {
1944        assert_eq!(size_of::<MtmdContextParams>(), 80);
1945        assert_eq!(offset_of!(MtmdContextParams, batch_max_tokens), 56);
1946        assert_eq!(offset_of!(MtmdContextParams, progress_callback), 64);
1947        assert_eq!(
1948            offset_of!(MtmdContextParams, progress_callback_user_data),
1949            72
1950        );
1951    }
1952
1953    #[test]
1954    #[cfg(not(feature = "dynamic-runtime"))]
1955    fn native_mtmd_defaults_cross_the_ffi_boundary() {
1956        let params = unsafe { mtmd_context_params_default() };
1957
1958        assert_eq!(params.batch_max_tokens, 1024);
1959        assert!(params.progress_callback.is_none());
1960        assert!(params.progress_callback_user_data.is_null());
1961    }
1962}