Skip to main content

baseRT_sys/
lib.rs

1//! Raw FFI bindings for the BaseRT C API.
2//!
3//! These are unsafe, low-level bindings. Prefer the `baseRT` crate for safe usage.
4
5#![allow(non_camel_case_types)]
6
7use std::os::raw::{c_char, c_float, c_int, c_void};
8
9/// Opaque model handle.
10pub type baseRT_model_t = *mut c_void;
11
12/// Model configuration extracted from weight file metadata.
13///
14/// This mirrors the FULL `BaseRTModelConfig` in include/baseRT/types.h. The
15/// struct is returned BY VALUE from `baseRT_get_config`, so it MUST match the C
16/// layout exactly — a truncated copy makes the C side write past the Rust
17/// allocation (memory corruption). Earlier revisions of this binding dropped
18/// `sliding_window` and every field after the encoder block; do not reintroduce
19/// that. New fields in C are appended at the end (`#[repr(C)]` keeps offsets).
20#[repr(C)]
21#[derive(Debug, Clone, Copy)]
22pub struct BaseRTModelConfig {
23    // Decoder (or decoder-only LLM) parameters
24    pub dim: u32,
25    pub n_layers: u32,
26    pub n_heads: u32,
27    pub n_kv_heads: u32,
28    pub head_dim: u32,
29    pub q_dim: u32,
30    pub kv_dim: u32,
31    pub ffn_dim: u32,
32    pub vocab_size: u32,
33    pub max_seq_len: u32,
34    pub norm_eps: c_float,
35    pub rope_theta: c_float,
36    pub sliding_window_pattern: u32,
37    pub sliding_window: u32,
38    pub rope_local_theta: c_float,
39    pub architecture: [c_char; 32],
40
41    // Encoder parameters (0 = decoder-only model)
42    pub enc_n_layers: u32,
43    pub enc_n_heads: u32,
44    pub enc_dim: u32,
45    pub enc_ffn_dim: u32,
46    pub n_mels: u32,
47    pub enc_max_seq_len: u32,
48
49    // Gemma 4-specific fields
50    pub n_embd_per_layer: u32,
51    pub n_layer_kv_from_start: u32,
52    pub logit_softcap: c_float,
53    pub attention_scale: c_float,
54    pub head_dim_swa: u32,
55    pub head_dim_global: u32,
56    pub global_rope_partial_factor: c_float,
57    pub swa_layers: [u8; 64],
58    pub ffn_dims: [u32; 128],
59    pub n_kv_heads_per_layer: [u32; 128],
60
61    // Qwen3.5 / 3.6 hybrid linear-attention (Gated DeltaNet)
62    pub attn_output_gate: u8,
63    pub _qwen35_pad: [u8; 3],
64    pub partial_rotary_factor: c_float,
65    pub full_attention_interval: u32,
66    pub linear_attn_layers: [u8; 64],
67    pub gdn_num_k_heads: u32,
68    pub gdn_num_v_heads: u32,
69    pub gdn_key_head_dim: u32,
70    pub gdn_value_head_dim: u32,
71    pub gdn_conv_kernel: u32,
72
73    // Nemotron-H hybrid Mamba-2 SSM
74    pub ssm_state_size: u32,
75    pub ssm_conv_kernel: u32,
76    pub ssm_num_groups: u32,
77    pub ssm_inner_size: u32,
78    pub ssm_num_heads: u32,
79
80    // Mixture-of-Experts (0 = dense)
81    pub n_experts: u32,
82    pub n_experts_used: u32,
83    pub n_experts_shared: u32,
84    pub expert_ffn_dim: u32,
85    pub expert_gating: u8,
86    pub norm_topk_prob: u8,
87    pub _moe_pad: [u8; 2],
88    pub expert_weights_scale: c_float,
89
90    // Vision tower (all zero = none)
91    pub vision_n_layers: u32,
92    pub vision_dim: u32,
93    pub vision_n_heads: u32,
94    pub vision_head_dim: u32,
95    pub vision_ffn_dim: u32,
96    pub vision_patch_size: u32,
97    pub vision_image_size: u32,
98    pub vision_pooling_kernel: u32,
99    pub vision_soft_tokens: u32,
100    pub vision_norm_eps: c_float,
101    pub vision_rope_theta: c_float,
102    pub vision_pos_embed_size: u32,
103    pub image_token_id: u32,
104    pub boi_token_id: u32,
105    pub eoi_token_id: u32,
106
107    // Vision-tower family selector + Qwen3-VL-style extras
108    pub vision_arch: u32,
109    pub vision_spatial_merge: u32,
110    pub vision_temporal_patch: u32,
111    pub vision_out_dim: u32,
112
113    // Audio tower (all zero = none)
114    pub audio_n_layers: u32,
115    pub audio_dim: u32,
116    pub audio_n_heads: u32,
117    pub audio_head_dim: u32,
118    pub audio_ffn_dim: u32,
119    pub audio_output_proj_dim: u32,
120    pub audio_chunk_size: u32,
121    pub audio_left_context: u32,
122    pub audio_conv_kernel: u32,
123    pub audio_soft_tokens: u32,
124    pub audio_logit_softcap: c_float,
125    pub audio_norm_eps: c_float,
126    pub audio_gradient_clip: c_float,
127    pub audio_residual_weight: c_float,
128    pub audio_ms_per_token: c_float,
129    pub audio_sscp_channels: [u32; 2],
130    pub audio_token_id: u32,
131    pub boa_token_id: u32,
132    pub eoa_token_id: u32,
133    pub mrope_section: [u32; 3],
134    pub mrope_interleaved: u8,
135    pub _mrope_pad: [u8; 3],
136    pub rope_scaling_factor: c_float,
137    pub rope_low_freq_factor: c_float,
138    pub rope_high_freq_factor: c_float,
139    pub rope_orig_max_pos: u32,
140    pub rope_scaling_type: u32,
141    // Muse Glimmer (0 / empty = not applicable)
142    pub qk_scale_factor: c_float,
143    pub output_multiplier: c_float,
144    pub post_norm_eps: c_float,
145    pub nope_layers: [u8; 64],
146    pub embed_norm_eps: c_float,
147    pub vision_window_layers: [u8; 64],
148    pub vision_window_size: u32,
149    pub vision_pos_embed_h: u32,
150    pub vision_pos_embed_w: u32,
151    pub vision_adapter_dim: u32,
152    pub video_token_id: u32,
153    // GLM 5.2 / glm-dsa
154    pub q_lora_rank: u32,
155    pub kv_lora_rank: u32,
156    pub qk_nope_head_dim: u32,
157    pub qk_rope_head_dim: u32,
158    pub v_head_dim: u32,
159    pub routed_scaling_factor: c_float,
160    pub first_k_dense_replace: u32,
161    pub nextn_predict_layers: u32,
162    pub indexer_head_count: u32,
163    pub indexer_key_length: u32,
164    pub indexer_top_k: u32,
165    // gpt-oss
166    pub rope_yarn_beta_fast: f32,
167    pub rope_yarn_beta_slow: f32,
168    pub swiglu_limit: f32,
169    pub swiglu_alpha: f32,
170    pub attention_sinks: u8,
171    pub attention_bias: u8,
172    pub rope_yarn_truncate: u8,
173    pub _gptoss_pad: u8,
174}
175
176/// Transcription result statistics.
177#[repr(C)]
178#[derive(Debug, Clone, Copy, Default)]
179pub struct BaseRTTranscribeStats {
180    pub n_tokens: c_int,
181    pub audio_ms: c_float,
182    pub encode_ms: c_float,
183    pub decode_ms: c_float,
184    pub total_ms: c_float,
185}
186
187/// One segment of the last transcription (verbose_json surface).
188/// `text` is borrowed from the engine — valid until the next transcription
189/// or model free.
190#[repr(C)]
191#[derive(Debug, Clone, Copy)]
192pub struct BaseRTTranscribeSegment {
193    pub start_ms: c_int,
194    pub end_ms: c_int,
195    pub text: *const c_char,
196    pub avg_logprob: c_float,
197    pub no_speech_prob: c_float,
198    pub compression_ratio: c_float,
199    pub temperature: c_float,
200}
201
202impl Default for BaseRTTranscribeSegment {
203    fn default() -> Self {
204        Self {
205            start_ms: 0,
206            end_ms: 0,
207            text: std::ptr::null(),
208            avg_logprob: 0.0,
209            no_speech_prob: 0.0,
210            compression_ratio: 0.0,
211            temperature: 0.0,
212        }
213    }
214}
215
216/// Sampling configuration for text generation.
217///
218/// Extended in baseRT 0.2 with OpenAI-compat presence / frequency penalties,
219/// a deterministic-sample `seed`, and a per-token `logit_bias` map. New
220/// fields are appended; older callers using `Default::default()` keep the
221/// classic five values plus zeroed extensions (disabled).
222#[repr(C)]
223#[derive(Debug, Clone, Copy)]
224pub struct BaseRTSamplingConfig {
225    pub temperature: c_float,
226    pub top_k: c_int,
227    pub top_p: c_float,
228    pub min_p: c_float,
229    pub repeat_penalty: c_float,
230    pub presence_penalty: c_float,
231    pub frequency_penalty: c_float,
232    pub seed: u32,
233    pub n_logit_bias: i32,
234    pub logit_bias_tokens: *const i32,
235    pub logit_bias_values: *const c_float,
236}
237
238impl Default for BaseRTSamplingConfig {
239    fn default() -> Self {
240        Self {
241            temperature: 0.0,
242            top_k: 40,
243            top_p: 0.9,
244            min_p: 0.0,
245            repeat_penalty: 1.0,
246            presence_penalty: 0.0,
247            frequency_penalty: 0.0,
248            seed: 0,
249            n_logit_bias: 0,
250            logit_bias_tokens: std::ptr::null(),
251            logit_bias_values: std::ptr::null(),
252        }
253    }
254}
255
256/// Generation result statistics.
257#[repr(C)]
258#[derive(Debug, Clone, Copy, Default)]
259pub struct BaseRTGenerationStats {
260    pub prompt_tokens: c_int,
261    pub generated_tokens: c_int,
262    pub prefill_time_ms: c_float,
263    pub decode_time_ms: c_float,
264    pub prefill_tokens_per_sec: c_float,
265    pub decode_tokens_per_sec: c_float,
266}
267
268/// Opaque per-sequence handle (independent KV state on the model's shared
269/// paged-KV pool). Mirrors `baseRT_sequence_t` in baseRT.h.
270pub type baseRT_sequence_t = *mut c_void;
271
272/// Result of a prefix-cache lookup (`baseRT_prefix_match`). `blocks` points
273/// into engine-owned storage valid until `baseRT_prefix_unlock(handle)` /
274/// `baseRT_prefix_release(handle)`. `handle == 0` means miss / cache
275/// disabled — nothing to unlock.
276#[repr(C)]
277#[derive(Debug, Clone, Copy)]
278pub struct BaseRTPrefixMatch {
279    pub matched_tokens: c_int,
280    pub n_blocks: c_int,
281    pub blocks: *const c_int,
282    pub handle: u64,
283}
284
285/// Callback for streaming token output. Return `false` to stop generation.
286pub type baseRT_token_callback = Option<
287    unsafe extern "C" fn(token_id: u32, text: *const c_char, user_data: *mut c_void) -> bool,
288>;
289
290/// Callback for streaming transcription segments. Return `false` to stop transcription.
291pub type baseRT_segment_callback = Option<
292    unsafe extern "C" fn(
293        start_ms: c_int,
294        end_ms: c_int,
295        text: *const c_char,
296        user_data: *mut c_void,
297    ) -> bool,
298>;
299
300extern "C" {
301    // === Model lifecycle ===
302
303    pub fn baseRT_load_model(
304        model_path: *const c_char,
305        kernel_library_path: *const c_char,
306        max_context: c_int,
307    ) -> baseRT_model_t;
308
309    pub fn baseRT_free_model(model: baseRT_model_t);
310    /// Tokenizer-only load: metadata + vocab, no GPU tensors. Only the
311    /// tokenizer surface is valid on the returned handle.
312    pub fn baseRT_load_tokenizer_only(model_path: *const c_char) -> baseRT_model_t;
313
314    // === Model info ===
315
316    pub fn baseRT_get_config(model: baseRT_model_t) -> BaseRTModelConfig;
317    pub fn baseRT_model_config_sizeof() -> usize;
318    /// The library's BASERT_ABI_VERSION (since 0.3.1).
319    pub fn baseRT_abi_version() -> std::os::raw::c_uint;
320    /// The configuration copied into a caller-sized buffer, zero-filled past
321    /// the library's struct; returns the library's sizeof (since 0.4.0).
322    pub fn baseRT_get_config_into(model: baseRT_model_t, out: *mut c_void, out_size: usize) -> usize;
323    pub fn baseRT_model_memory(model: baseRT_model_t) -> usize;
324    /// Device memory budget in bytes (Metal: recommended working set; CUDA:
325    /// total device memory). 0 when no supported device is present.
326    pub fn baseRT_device_memory_budget() -> usize;
327    /// A `max_context` sized for this device and this bundle. Metadata-only,
328    /// so it is safe to call before loading. 0 = could not size.
329    pub fn baseRT_suggest_max_context(
330        model_path: *const c_char,
331        max_batch: c_int,
332        kv_bits: c_int,
333        paged_kv: c_int,
334    ) -> c_int;
335    /// As above for a set of models that will be resident at the same time:
336    /// weights and KV pools add up, and the trained-window cap is the
337    /// shortest among them.
338    pub fn baseRT_suggest_max_context_multi(
339        model_paths: *const *const c_char,
340        n_models: c_int,
341        max_batch: c_int,
342        kv_bits: c_int,
343        paged_kv: c_int,
344    ) -> c_int;
345    /// As above, plus the speculators loading beside those models:
346    /// `speculator_embedded[i]` non-zero is the target bundle's embedded head
347    /// (its KV pool only); zero (or a null array) a sidecar / draft model
348    /// (weights and KV pool), even when also served. `speculator_target[i]`
349    /// >= 0 is the index of the model a sidecar's baseRT_load_drafter reopens
350    /// for its embedding / lm_head (charged where the backend copies wrapped
351    /// weights); -1 (or a null array) for none. Speculators never cap the
352    /// window.
353    pub fn baseRT_suggest_max_context_spec(
354        model_paths: *const *const c_char,
355        n_models: c_int,
356        speculator_paths: *const *const c_char,
357        speculator_embedded: *const c_int,
358        speculator_target: *const c_int,
359        n_speculators: c_int,
360        max_batch: c_int,
361        kv_bits: c_int,
362        paged_kv: c_int,
363    ) -> c_int;
364    /// 1 = the window fits the co-resident set, 0 = it does not, -1 = unknown.
365    pub fn baseRT_context_window_fits(
366        model_paths: *const *const c_char,
367        n_models: c_int,
368        max_batch: c_int,
369        kv_bits: c_int,
370        paged_kv: c_int,
371        window: c_int,
372    ) -> c_int;
373    pub fn baseRT_get_error() -> *const c_char;
374
375    // === Tokenization ===
376
377    pub fn baseRT_encode(
378        model: baseRT_model_t,
379        text: *const c_char,
380        out_tokens: *mut u32,
381        max_tokens: c_int,
382    ) -> c_int;
383    /// Special-token strings in `text` are ordinary text (client content),
384    /// never control ids. See the header.
385    pub fn baseRT_encode_plain(
386        model: baseRT_model_t,
387        text: *const c_char,
388        out_tokens: *mut u32,
389        max_tokens: c_int,
390    ) -> c_int;
391    /// `n_pieces` texts encoded as one: piece `i` parses marker strings
392    /// when `plain[i]` is zero and keeps them as text otherwise, with the
393    /// pre-tokenizer seeing the concatenation. See the header.
394    pub fn baseRT_encode_pieces(
395        model: baseRT_model_t,
396        texts: *const *const c_char,
397        plain: *const c_int,
398        n_pieces: c_int,
399        out_tokens: *mut u32,
400        max_tokens: c_int,
401    ) -> c_int;
402
403    pub fn baseRT_decode_token(model: baseRT_model_t, token_id: u32) -> *const c_char;
404    /// Length-preserving stateless decode (byte-level tokens can contain
405    /// NUL); returns the true byte length, which may exceed `max_bytes`.
406    pub fn baseRT_decode_token_raw(
407        model: baseRT_model_t,
408        token_id: u32,
409        out: *mut c_char,
410        max_bytes: c_int,
411    ) -> c_int;
412
413    // === Generation ===
414
415    pub fn baseRT_generate(
416        model: baseRT_model_t,
417        prompt_tokens: *const u32,
418        n_prompt: c_int,
419        max_tokens: c_int,
420        sampling: BaseRTSamplingConfig,
421        callback: baseRT_token_callback,
422        user_data: *mut c_void,
423    ) -> BaseRTGenerationStats;
424
425    pub fn baseRT_generate_continue(
426        model: baseRT_model_t,
427        new_tokens: *const u32,
428        n_new: c_int,
429        max_tokens: c_int,
430        sampling: BaseRTSamplingConfig,
431        callback: baseRT_token_callback,
432        user_data: *mut c_void,
433    ) -> BaseRTGenerationStats;
434
435    // === Low-level API ===
436
437    pub fn baseRT_prefill(model: baseRT_model_t, tokens: *const u32, n_tokens: c_int) -> u32;
438
439    pub fn baseRT_decode_step(model: baseRT_model_t, token_id: u32, position: c_int) -> u32;
440
441    pub fn baseRT_chain_decode(
442        model: baseRT_model_t,
443        first_token: u32,
444        start_position: c_int,
445        count: c_int,
446        out_tokens: *mut u32,
447    ) -> c_int;
448
449    pub fn baseRT_get_position(model: baseRT_model_t) -> c_int;
450
451    pub fn baseRT_set_speculation(model: baseRT_model_t, enabled: bool);
452
453    pub fn baseRT_reset(model: baseRT_model_t);
454
455    // === Whisper transcription ===
456
457    pub fn baseRT_transcribe(
458        model: baseRT_model_t,
459        wav_path: *const c_char,
460        language: *const c_char,
461        stats_out: *mut BaseRTTranscribeStats,
462    ) -> *const c_char;
463
464    pub fn baseRT_transcribe_pcm(
465        model: baseRT_model_t,
466        samples: *const c_float,
467        n_samples: c_int,
468        language: *const c_char,
469        stats_out: *mut BaseRTTranscribeStats,
470    ) -> *const c_char;
471
472    pub fn baseRT_set_timestamps(model: baseRT_model_t, enabled: bool);
473
474    pub fn baseRT_set_task(model: baseRT_model_t, task: *const c_char) -> bool;
475
476    pub fn baseRT_set_initial_prompt(model: baseRT_model_t, text: *const c_char);
477
478    pub fn baseRT_set_condition_on_previous_text(model: baseRT_model_t, enabled: bool);
479
480    pub fn baseRT_transcribe_language(model: baseRT_model_t) -> *const c_char;
481
482    pub fn baseRT_transcribe_audio_duration_ms(model: baseRT_model_t) -> c_int;
483
484    pub fn baseRT_transcribe_segment_count(model: baseRT_model_t) -> c_int;
485
486    pub fn baseRT_transcribe_segment(
487        model: baseRT_model_t,
488        index: c_int,
489        out: *mut BaseRTTranscribeSegment,
490    ) -> bool;
491
492    pub fn baseRT_is_whisper(model: baseRT_model_t) -> bool;
493
494    // === Streaming transcription ===
495
496    pub fn baseRT_transcribe_pcm_stream(
497        model: baseRT_model_t,
498        samples: *const c_float,
499        n_samples: c_int,
500        language: *const c_char,
501        stats_out: *mut BaseRTTranscribeStats,
502        callback: baseRT_segment_callback,
503        user_data: *mut c_void,
504    ) -> *const c_char;
505
506    pub fn baseRT_transcribe_stream(
507        model: baseRT_model_t,
508        wav_path: *const c_char,
509        language: *const c_char,
510        stats_out: *mut BaseRTTranscribeStats,
511        callback: baseRT_segment_callback,
512        user_data: *mut c_void,
513    ) -> *const c_char;
514
515    // === Embeddings ===
516
517    pub fn baseRT_embed(
518        model: baseRT_model_t,
519        tokens: *const u32,
520        n_tokens: c_int,
521        out_embedding: *mut c_float,
522        max_dims: c_int,
523    ) -> c_int;
524
525    pub fn baseRT_embed_text(
526        model: baseRT_model_t,
527        text: *const c_char,
528        out_embedding: *mut c_float,
529        max_dims: c_int,
530    ) -> c_int;
531
532    pub fn baseRT_embedding_dim(model: baseRT_model_t) -> c_int;
533    pub fn baseRT_is_embedding_model(model: baseRT_model_t) -> bool;
534
535    // === Chat templates ===
536
537    pub fn baseRT_format_chat(
538        model: baseRT_model_t,
539        system_prompt: *const c_char,
540        user_message: *const c_char,
541    ) -> *const c_char;
542
543    pub fn baseRT_chat_template(model: baseRT_model_t) -> *const c_char;
544
545    /// Raw Jinja `chat_template` source folded in from the HF tokenizer
546    /// (empty if the bundle carries none).
547    pub fn baseRT_chat_template_jinja(model: baseRT_model_t) -> *const c_char;
548
549    /// Create a grammar from a JSON Schema (converted to GBNF internally);
550    /// NULL on error. Used to validate that a codec's tool-call grammar
551    /// compiles in the engine (the runtime constrained-decode path).
552    pub fn baseRT_grammar_create_from_schema(
553        model: baseRT_model_t,
554        json_schema: *const c_char,
555    ) -> *mut std::os::raw::c_void;
556
557    /// Create a grammar from an xgrammar STRUCTURAL TAG document (JSON, not a
558    /// grammar string); NULL on error. The tag's content formats reach past
559    /// JSON Schema — `sequence`, `const_string`, `regex`, `or` — which is how
560    /// a dialect whose call body is not JSON (Gemma 4's `call:fn{k:v}`) still
561    /// gets an enforceable `tool_choice`.
562    pub fn baseRT_grammar_create_from_structural_tag(
563        model: baseRT_model_t,
564        tag_json: *const c_char,
565    ) -> *mut std::os::raw::c_void;
566
567    /// Create a grammar from a GBNF string; NULL on parse error.
568    pub fn baseRT_grammar_create(
569        model: baseRT_model_t,
570        gbnf: *const c_char,
571    ) -> *mut std::os::raw::c_void;
572
573    /// Step a grammar by one token; 0 = the grammar REJECTS it. Lets a test
574    /// assert what a compiled grammar actually admits, not merely that it
575    /// compiled — the difference between "the tag parses" and "the tag
576    /// permits the calls this dialect must be able to make".
577    pub fn baseRT_grammar_accept_token(
578        grammar: *mut std::os::raw::c_void,
579        token_id: u32,
580    ) -> std::os::raw::c_int;
581
582    /// Reset a grammar's acceptance state to its initial stacks, so one
583    /// handle can check several candidate strings.
584    pub fn baseRT_grammar_reset(grammar: *mut std::os::raw::c_void);
585
586    /// Free a grammar.
587    pub fn baseRT_grammar_free(grammar: *mut std::os::raw::c_void);
588
589    /// Number of added/special tokens the model declares.
590    pub fn baseRT_special_token_count(model: baseRT_model_t) -> c_int;
591
592    /// The `index`-th special token string; writes its id to `id_out`.
593    pub fn baseRT_special_token(
594        model: baseRT_model_t,
595        index: c_int,
596        id_out: *mut u32,
597    ) -> *const c_char;
598
599    // === Token counting ===
600
601    pub fn baseRT_token_count(model: baseRT_model_t, text: *const c_char) -> c_int;
602
603    // === Model inspection ===
604
605    pub fn baseRT_tensor_count(model: baseRT_model_t) -> c_int;
606    pub fn baseRT_tensor_name(model: baseRT_model_t, index: c_int) -> *const c_char;
607    pub fn baseRT_tensor_dtype(model: baseRT_model_t, index: c_int) -> u32;
608    pub fn baseRT_tensor_raw_dtype(model: baseRT_model_t, index: c_int) -> *const c_char;
609
610    // === Profiling ===
611
612    pub fn baseRT_profile_decode_step(
613        model: baseRT_model_t,
614        token_id: u32,
615        position: c_int,
616        timing_out: *mut c_float,
617        max_entries: c_int,
618    ) -> c_int;
619
620    pub fn baseRT_profile_label(model: baseRT_model_t, index: c_int) -> *const c_char;
621
622    // === GPU sampling ===
623
624    pub fn baseRT_gpu_temperature_scale(model: baseRT_model_t, temperature: c_float);
625
626    pub fn baseRT_gpu_repetition_penalty(
627        model: baseRT_model_t,
628        token_ids: *const u32,
629        n_tokens: c_int,
630        penalty: c_float,
631    );
632
633    // === Pre-load configuration (call BEFORE baseRT_load_model) ===
634
635    pub fn baseRT_set_paged_kv(enable: c_int);
636    pub fn baseRT_set_baked_decode(enable: c_int);
637    pub fn baseRT_set_max_batch_size(n: c_int);
638    pub fn baseRT_set_prefix_cache(enable: c_int);
639    pub fn baseRT_set_prefill_chunk(n: c_int);
640    pub fn baseRT_set_kv_bits(bits: c_int);
641    pub fn baseRT_set_verbose(on: c_int);
642
643    // === Error / capability introspection ===
644
645    pub fn baseRT_get_error_code() -> c_int;
646    pub fn baseRT_strerror(code: c_int) -> *const c_char;
647    pub fn baseRT_capabilities(model: baseRT_model_t) -> u32;
648    pub fn baseRT_eos_token_id(model: baseRT_model_t) -> u32;
649    pub fn baseRT_is_eos_token(model: baseRT_model_t, token: u32) -> i32;
650    pub fn baseRT_weights_identity(model: baseRT_model_t) -> u64;
651    pub fn baseRT_kv_bits_effective(model: baseRT_model_t) -> c_int;
652    pub fn baseRT_bos_id(model: baseRT_model_t) -> u32;
653
654    // === Multi-sequence generation (paged-KV only) ===
655
656    pub fn baseRT_sequence_create(model: baseRT_model_t) -> baseRT_sequence_t;
657    pub fn baseRT_sequence_free(seq: baseRT_sequence_t);
658    pub fn baseRT_sequence_rollback(seq: baseRT_sequence_t, length: c_int) -> c_int;
659
660    // === Batched prefill/decode ===
661
662    pub fn baseRT_batch_step(
663        model: baseRT_model_t,
664        seqs: *mut baseRT_sequence_t,
665        n_seqs: c_int,
666        in_tokens: *const u32,
667        in_token_counts: *const c_int,
668        out_tokens: *mut u32,
669    ) -> c_int;
670    pub fn baseRT_batch_step_fused(
671        model: baseRT_model_t,
672        seqs: *mut baseRT_sequence_t,
673        n_seqs: c_int,
674        in_tokens: *const u32,
675        in_token_counts: *const c_int,
676        out_tokens: *mut u32,
677    ) -> c_int;
678    pub fn baseRT_batch_step_fused_logits(
679        model: baseRT_model_t,
680        seqs: *mut baseRT_sequence_t,
681        n_seqs: c_int,
682        in_tokens: *const u32,
683        in_token_counts: *const c_int,
684    ) -> c_int;
685    pub fn baseRT_batch_warmup(model: baseRT_model_t, max_batch: c_int) -> c_int;
686    pub fn baseRT_max_prefill_chunk(model: baseRT_model_t) -> c_int;
687
688    // === Host-side logits-row operations ===
689
690    pub fn baseRT_read_batch_logits(
691        model: baseRT_model_t,
692        n_seqs: c_int,
693        out_logits_f16: *mut c_void,
694    ) -> c_int;
695    pub fn baseRT_batch_logits_stride(model: baseRT_model_t) -> usize;
696    pub fn baseRT_mask_logits_row(
697        model: baseRT_model_t,
698        row: *mut c_void,
699        bitmask: *const i32,
700    ) -> c_int;
701    pub fn baseRT_sample_logits_row(
702        model: baseRT_model_t,
703        row: *const c_void,
704        cfg: *const BaseRTSamplingConfig,
705        prev_tokens: *const u32,
706        n_prev: c_int,
707        repeat_window: c_int,
708        seed_offset: u32,
709    ) -> u32;
710    pub fn baseRT_argmax_logits_row(model: baseRT_model_t, row: *const c_void) -> u32;
711    pub fn baseRT_logits_row_logprobs(
712        model: baseRT_model_t,
713        row: *const c_void,
714        token: u32,
715        top_k: c_int,
716        out_token_logprob: *mut c_float,
717        out_ids: *mut u32,
718        out_logprobs: *mut c_float,
719    ) -> c_int;
720
721    // === Prefix cache — scheduler-driven primitives ===
722
723    pub fn baseRT_prefix_match(
724        model: baseRT_model_t,
725        tokens: *const u32,
726        n_tokens: c_int,
727    ) -> BaseRTPrefixMatch;
728    pub fn baseRT_sequence_seed_prefix(
729        seq: baseRT_sequence_t,
730        blocks: *const c_int,
731        n_blocks: c_int,
732        n_tokens: c_int,
733    ) -> c_int;
734    pub fn baseRT_page_size(model: baseRT_model_t) -> c_int;
735    pub fn baseRT_prefix_insert(
736        model: baseRT_model_t,
737        tokens: *const u32,
738        n_tokens: c_int,
739        seq: baseRT_sequence_t,
740    ) -> c_int;
741    pub fn baseRT_prefix_unlock(model: baseRT_model_t, handle: u64);
742    pub fn baseRT_prefix_release(model: baseRT_model_t, handle: u64);
743    pub fn baseRT_prefix_discard(model: baseRT_model_t, handle: u64);
744    pub fn baseRT_prefix_evict(model: baseRT_model_t, n_blocks: c_int) -> c_int;
745    pub fn baseRT_prefix_cache_stats(
746        model: baseRT_model_t,
747        out_hits: *mut u64,
748        out_misses: *mut u64,
749        out_reused_tokens: *mut u64,
750        out_blocks_cached: *mut c_int,
751    );
752
753    // === Recurrent (GDN) state snapshots — hybrid models only ===
754
755    pub fn baseRT_gdn_snapshot_size(model: baseRT_model_t) -> c_int;
756    pub fn baseRT_sequence_gdn_capture(seq: baseRT_sequence_t, out: *mut u8, cap: c_int) -> c_int;
757    pub fn baseRT_sequence_gdn_restore(
758        seq: baseRT_sequence_t,
759        blob: *const u8,
760        len: c_int,
761    ) -> c_int;
762}
763
764#[cfg(test)]
765mod tests {
766    use super::*;
767    use std::mem;
768
769    // -----------------------------------------------------------------------
770    // Struct size tests — must match the C struct layout in types.h
771    // -----------------------------------------------------------------------
772
773    #[test]
774    fn model_config_size() {
775        // Full struct: decoder + encoder + Gemma4 + Qwen3.5-GDN + MoE +
776        // vision + audio.
777        // Must be far larger than the old 112-byte truncation; returning a
778        // 112-byte struct by value from baseRT_get_config corrupted memory.
779        assert!(
780            mem::size_of::<BaseRTModelConfig>() > 1000,
781            "config struct unexpectedly small ({})",
782            mem::size_of::<BaseRTModelConfig>()
783        );
784        assert_eq!(mem::size_of::<BaseRTModelConfig>(), 1792);
785    }
786
787    #[test]
788    fn model_config_size_matches_library() {
789        // The authoritative drift check: compare this hand-written mirror
790        // against sizeof(BaseRTModelConfig) as compiled into libbaseRT. A
791        // mismatch means every field after the divergence point decodes as
792        // garbage through baseRT_get_config.
793        assert_eq!(
794            unsafe { baseRT_model_config_sizeof() },
795            mem::size_of::<BaseRTModelConfig>(),
796            "BaseRTModelConfig mirror drifted from include/baseRT/types.h"
797        );
798    }
799
800    #[test]
801    fn model_config_alignment() {
802        assert_eq!(mem::align_of::<BaseRTModelConfig>(), 4);
803    }
804
805    #[test]
806    fn sampling_config_size() {
807        // 5 floats/ints (20) + 2 floats (presence/freq, 28) + uint32 seed (32)
808        // + int32 n_logit_bias (36) + 4-byte pad to 8-byte align (40)
809        // + 2 pointers (8 bytes each on 64-bit) = 56 bytes
810        assert_eq!(mem::size_of::<BaseRTSamplingConfig>(), 56);
811    }
812
813    #[test]
814    fn sampling_config_alignment() {
815        // Two pointer fields force 8-byte alignment on 64-bit.
816        assert_eq!(mem::align_of::<BaseRTSamplingConfig>(), 8);
817    }
818
819    #[test]
820    fn generation_stats_size() {
821        // int + int + float + float + float + float = 24 bytes
822        assert_eq!(mem::size_of::<BaseRTGenerationStats>(), 24);
823    }
824
825    #[test]
826    fn generation_stats_alignment() {
827        assert_eq!(mem::align_of::<BaseRTGenerationStats>(), 4);
828    }
829
830    #[test]
831    fn transcribe_stats_size() {
832        // int + float + float + float + float = 20 bytes
833        assert_eq!(mem::size_of::<BaseRTTranscribeStats>(), 20);
834    }
835
836    #[test]
837    fn transcribe_stats_alignment() {
838        assert_eq!(mem::align_of::<BaseRTTranscribeStats>(), 4);
839    }
840
841    // -----------------------------------------------------------------------
842    // Field offset tests — verify C-compatible field ordering
843    // -----------------------------------------------------------------------
844
845    #[test]
846    fn model_config_field_offsets() {
847        // SAFETY: zeroed bytes are a valid bit pattern for this all-POD struct.
848        let base: BaseRTModelConfig = unsafe { mem::zeroed() };
849        let base_ptr = &base as *const _ as usize;
850
851        assert_eq!(&base.dim as *const _ as usize - base_ptr, 0);
852        assert_eq!(&base.n_layers as *const _ as usize - base_ptr, 4);
853        assert_eq!(&base.norm_eps as *const _ as usize - base_ptr, 40);
854        assert_eq!(&base.rope_theta as *const _ as usize - base_ptr, 44);
855        assert_eq!(
856            &base.sliding_window_pattern as *const _ as usize - base_ptr,
857            48
858        );
859        // The field the old binding dropped — everything after shifts by 4.
860        assert_eq!(&base.sliding_window as *const _ as usize - base_ptr, 52);
861        assert_eq!(&base.rope_local_theta as *const _ as usize - base_ptr, 56);
862        assert_eq!(&base.architecture as *const _ as usize - base_ptr, 60);
863        assert_eq!(&base.enc_n_layers as *const _ as usize - base_ptr, 92);
864        assert_eq!(&base.enc_max_seq_len as *const _ as usize - base_ptr, 112);
865        // Spot-check the tail blocks to confirm the full layout.
866        assert_eq!(&base.swa_layers as *const _ as usize - base_ptr, 144);
867        assert_eq!(&base.ffn_dims as *const _ as usize - base_ptr, 208);
868        assert_eq!(&base.attn_output_gate as *const _ as usize - base_ptr, 1232);
869        assert_eq!(
870            &base.linear_attn_layers as *const _ as usize - base_ptr,
871            1244
872        );
873        assert_eq!(&base.gdn_num_k_heads as *const _ as usize - base_ptr, 1308);
874        // Nemotron-H SSM block, inserted between gdn_* and the MoE block —
875        // 20 bytes that shifted every field below it.
876        assert_eq!(&base.ssm_state_size as *const _ as usize - base_ptr, 1328);
877        assert_eq!(&base.ssm_num_heads as *const _ as usize - base_ptr, 1344);
878        assert_eq!(&base.n_experts as *const _ as usize - base_ptr, 1348);
879        assert_eq!(
880            &base.expert_weights_scale as *const _ as usize - base_ptr,
881            1368
882        );
883        assert_eq!(&base.vision_n_layers as *const _ as usize - base_ptr, 1372);
884        assert_eq!(&base.vision_arch as *const _ as usize - base_ptr, 1432);
885        assert_eq!(&base.audio_n_layers as *const _ as usize - base_ptr, 1448);
886        assert_eq!(&base.eoa_token_id as *const _ as usize - base_ptr, 1524);
887        assert_eq!(&base.mrope_section as *const _ as usize - base_ptr, 1528);
888        assert_eq!(
889            &base.mrope_interleaved as *const _ as usize - base_ptr,
890            1540
891        );
892        assert_eq!(
893            &base.rope_scaling_factor as *const _ as usize - base_ptr,
894            1544
895        );
896        assert_eq!(
897            &base.rope_orig_max_pos as *const _ as usize - base_ptr,
898            1556
899        );
900        assert_eq!(
901            &base.rope_scaling_type as *const _ as usize - base_ptr,
902            1560
903        );
904        assert_eq!(&base.qk_scale_factor as *const _ as usize - base_ptr, 1564);
905        assert_eq!(&base.nope_layers as *const _ as usize - base_ptr, 1576);
906        assert_eq!(&base.embed_norm_eps as *const _ as usize - base_ptr, 1640);
907        assert_eq!(
908            &base.vision_window_layers as *const _ as usize - base_ptr,
909            1644
910        );
911        assert_eq!(&base.video_token_id as *const _ as usize - base_ptr, 1724);
912        assert_eq!(&base.q_lora_rank as *const _ as usize - base_ptr, 1728);
913        assert_eq!(&base.indexer_top_k as *const _ as usize - base_ptr, 1768);
914        assert_eq!(
915            &base.rope_yarn_beta_fast as *const _ as usize - base_ptr,
916            1772
917        );
918    }
919
920    #[test]
921    fn sampling_config_field_offsets() {
922        let base = BaseRTSamplingConfig::default();
923        let base_ptr = &base as *const _ as usize;
924
925        assert_eq!(&base.temperature as *const _ as usize - base_ptr, 0);
926        assert_eq!(&base.top_k as *const _ as usize - base_ptr, 4);
927        assert_eq!(&base.top_p as *const _ as usize - base_ptr, 8);
928        assert_eq!(&base.min_p as *const _ as usize - base_ptr, 12);
929        assert_eq!(&base.repeat_penalty as *const _ as usize - base_ptr, 16);
930        assert_eq!(&base.presence_penalty as *const _ as usize - base_ptr, 20);
931        assert_eq!(&base.frequency_penalty as *const _ as usize - base_ptr, 24);
932        assert_eq!(&base.seed as *const _ as usize - base_ptr, 28);
933        assert_eq!(&base.n_logit_bias as *const _ as usize - base_ptr, 32);
934        // 4-byte padding before pointer alignment to 8.
935        assert_eq!(&base.logit_bias_tokens as *const _ as usize - base_ptr, 40);
936        assert_eq!(&base.logit_bias_values as *const _ as usize - base_ptr, 48);
937    }
938
939    #[test]
940    fn generation_stats_field_offsets() {
941        let base = BaseRTGenerationStats::default();
942        let base_ptr = &base as *const _ as usize;
943
944        assert_eq!(&base.prompt_tokens as *const _ as usize - base_ptr, 0);
945        assert_eq!(&base.generated_tokens as *const _ as usize - base_ptr, 4);
946        assert_eq!(&base.prefill_time_ms as *const _ as usize - base_ptr, 8);
947        assert_eq!(&base.decode_time_ms as *const _ as usize - base_ptr, 12);
948        assert_eq!(
949            &base.prefill_tokens_per_sec as *const _ as usize - base_ptr,
950            16
951        );
952        assert_eq!(
953            &base.decode_tokens_per_sec as *const _ as usize - base_ptr,
954            20
955        );
956    }
957
958    // -----------------------------------------------------------------------
959    // Default trait tests
960    // -----------------------------------------------------------------------
961
962    #[test]
963    fn sampling_config_default_values() {
964        let cfg = BaseRTSamplingConfig::default();
965        assert_eq!(cfg.temperature, 0.0);
966        assert_eq!(cfg.top_k, 40);
967        assert!((cfg.top_p - 0.9).abs() < f32::EPSILON);
968        assert_eq!(cfg.min_p, 0.0);
969        assert_eq!(cfg.repeat_penalty, 1.0);
970        assert_eq!(cfg.presence_penalty, 0.0);
971        assert_eq!(cfg.frequency_penalty, 0.0);
972        assert_eq!(cfg.seed, 0);
973        assert_eq!(cfg.n_logit_bias, 0);
974        assert!(cfg.logit_bias_tokens.is_null());
975        assert!(cfg.logit_bias_values.is_null());
976    }
977
978    #[test]
979    fn prefix_match_layout() {
980        // int matched_tokens (4) + int n_blocks (8) + const int *blocks
981        // (16) + uint64_t handle (24). Returned BY VALUE from
982        // baseRT_prefix_match, so the mirror must match the C layout
983        // exactly.
984        assert_eq!(mem::size_of::<BaseRTPrefixMatch>(), 24);
985        assert_eq!(mem::offset_of!(BaseRTPrefixMatch, matched_tokens), 0);
986        assert_eq!(mem::offset_of!(BaseRTPrefixMatch, n_blocks), 4);
987        assert_eq!(mem::offset_of!(BaseRTPrefixMatch, blocks), 8);
988        assert_eq!(mem::offset_of!(BaseRTPrefixMatch, handle), 16);
989    }
990
991    #[test]
992    fn transcribe_stats_default_values() {
993        let stats = BaseRTTranscribeStats::default();
994        assert_eq!(stats.n_tokens, 0);
995        assert_eq!(stats.audio_ms, 0.0);
996        assert_eq!(stats.encode_ms, 0.0);
997        assert_eq!(stats.decode_ms, 0.0);
998        assert_eq!(stats.total_ms, 0.0);
999    }
1000
1001    #[test]
1002    fn generation_stats_default_values() {
1003        let stats = BaseRTGenerationStats::default();
1004        assert_eq!(stats.prompt_tokens, 0);
1005        assert_eq!(stats.generated_tokens, 0);
1006        assert_eq!(stats.prefill_time_ms, 0.0);
1007        assert_eq!(stats.decode_time_ms, 0.0);
1008        assert_eq!(stats.prefill_tokens_per_sec, 0.0);
1009        assert_eq!(stats.decode_tokens_per_sec, 0.0);
1010    }
1011}