Skip to main content

combs_formats/
gguf.rs

1//! GGUF adapter (llama.cpp ecosystem).
2//!
3//! Reads GGUF v2/v3 files: header, metadata KV pairs, tensor infos and the
4//! aligned tensor data section (mmap-backed). Implements [`ModelSource`] by
5//! mapping ggml names to HF names (`blk.0.attn_q.weight` →
6//! `model.layers.0.self_attn.q_proj.weight`) and ggml dimensions to HF
7//! layout (`[in, out]` → `[out, in]`, which for row-major data is just a
8//! shape reversal — no data movement).
9//!
10//! Supported tensor types: F32, F16, BF16, Q4_0, Q8_0. Quantized tensors
11//! are dequantized on load (CPU scalar path) — wiring `QuantizedLinear` to
12//! keep them packed in VRAM is a follow-up. Q4_K/Q5_K/Q6_K superblock
13//! formats are not yet supported (clear error).
14//!
15//! Tokenizer: a sibling `tokenizer.json` is used when present; otherwise a
16//! BPE `tokenizer.json` is synthesized from the GGUF tokenizer metadata
17//! (tokens/scores/types/merges) and cached next to the model.
18
19use std::collections::HashMap;
20use std::path::{Path, PathBuf};
21
22use memmap2::Mmap;
23
24use crate::metadata::ModelMetadata;
25use crate::source::{ModelSource, SamplerConfig, TensorDtype, TensorReader};
26use crate::tokenizer::TokenizerSpec;
27use crate::{FormatError, Result};
28
29const GGUF_MAGIC: u32 = 0x4655_4747; // "GGUF"
30
31#[derive(Debug, Clone)]
32enum MetaValue {
33    U32(u32),
34    I32(i32),
35    U64(u64),
36    F32(f32),
37    Bool(bool),
38    String(String),
39    Strings(Vec<String>),
40    F32s(Vec<f32>),
41    I32s(Vec<i32>),
42}
43
44#[derive(Debug, Clone)]
45struct TensorInfo {
46    name: String,
47    dims: Vec<usize>, // ggml order (fastest dim first)
48    ggml_type: u32,
49    offset: usize, // relative to data section
50}
51
52/// A parsed GGUF file.
53pub struct GgufSource {
54    path: PathBuf,
55    mmap: Mmap,
56    metadata: ModelMetadata,
57    kv: HashMap<String, MetaValue>,
58    tensors: HashMap<String, TensorInfo>,
59    data_start: usize,
60    tokenizer_json: PathBuf,
61    added_tokens: HashMap<u32, String>,
62    eos_ids: Vec<u32>,
63    bos_id: Option<u32>,
64}
65
66// ---------------------------------------------------------------------------
67// parsing helpers (little-endian cursor)
68
69struct Cursor<'a> {
70    buf: &'a [u8],
71    pos: usize,
72}
73
74impl<'a> Cursor<'a> {
75    fn new(buf: &'a [u8]) -> Self {
76        Cursor { buf, pos: 0 }
77    }
78    fn take(&mut self, n: usize) -> Result<&'a [u8]> {
79        if self.pos + n > self.buf.len() {
80            return Err(FormatError::Safetensors("gguf: unexpected end of file".into()));
81        }
82        let out = &self.buf[self.pos..self.pos + n];
83        self.pos += n;
84        Ok(out)
85    }
86    fn u8(&mut self) -> Result<u8> {
87        Ok(self.take(1)?[0])
88    }
89    fn u16(&mut self) -> Result<u16> {
90        Ok(u16::from_le_bytes(self.take(2)?.try_into().unwrap()))
91    }
92    fn u32(&mut self) -> Result<u32> {
93        Ok(u32::from_le_bytes(self.take(4)?.try_into().unwrap()))
94    }
95    fn i32(&mut self) -> Result<i32> {
96        Ok(i32::from_le_bytes(self.take(4)?.try_into().unwrap()))
97    }
98    fn u64(&mut self) -> Result<u64> {
99        Ok(u64::from_le_bytes(self.take(8)?.try_into().unwrap()))
100    }
101    fn i64(&mut self) -> Result<i64> {
102        Ok(i64::from_le_bytes(self.take(8)?.try_into().unwrap()))
103    }
104    fn f32(&mut self) -> Result<f32> {
105        Ok(f32::from_le_bytes(self.take(4)?.try_into().unwrap()))
106    }
107    fn string(&mut self) -> Result<String> {
108        let len = self.u64()? as usize;
109        let bytes = self.take(len)?;
110        String::from_utf8(bytes.to_vec())
111            .map_err(|e| FormatError::Safetensors(format!("gguf: bad utf8 string: {e}")))
112    }
113}
114
115const META_U8: u32 = 0;
116const META_I8: u32 = 1;
117const META_U16: u32 = 2;
118const META_I16: u32 = 3;
119const META_U32: u32 = 4;
120const META_I32: u32 = 5;
121const META_F32: u32 = 6;
122const META_BOOL: u32 = 7;
123const META_STRING: u32 = 8;
124const META_ARRAY: u32 = 9;
125const META_U64: u32 = 10;
126const META_I64: u32 = 11;
127const META_F64: u32 = 12;
128
129fn read_meta_value(c: &mut Cursor, ty: u32) -> Result<MetaValue> {
130    Ok(match ty {
131        META_U8 => MetaValue::U32(c.u8()? as u32),
132        META_I8 => MetaValue::I32(c.u8()? as i8 as i32),
133        META_U16 => MetaValue::U32(c.u16()? as u32),
134        META_I16 => MetaValue::I32(c.u16()? as i16 as i32),
135        META_U32 => MetaValue::U32(c.u32()?),
136        META_I32 => MetaValue::I32(c.i32()?),
137        META_U64 => MetaValue::U64(c.u64()?),
138        META_I64 => MetaValue::U64(c.i64()? as u64),
139        META_F32 => MetaValue::F32(c.f32()?),
140        META_F64 => MetaValue::F32(f64::from_le_bytes(c.take(8)?.try_into().unwrap()) as f32),
141        META_BOOL => MetaValue::Bool(c.u8()? != 0),
142        META_STRING => MetaValue::String(c.string()?),
143        META_ARRAY => {
144            let elem_ty = c.u32()?;
145            let len = c.u64()? as usize;
146            match elem_ty {
147                META_STRING => {
148                    let mut out = Vec::with_capacity(len);
149                    for _ in 0..len {
150                        out.push(c.string()?);
151                    }
152                    MetaValue::Strings(out)
153                }
154                META_F32 => {
155                    let mut out = Vec::with_capacity(len);
156                    for _ in 0..len {
157                        out.push(c.f32()?);
158                    }
159                    MetaValue::F32s(out)
160                }
161                META_I32 | META_I16 | META_I8 => {
162                    let mut out = Vec::with_capacity(len);
163                    for _ in 0..len {
164                        out.push(read_meta_value(c, elem_ty).map(|v| match v {
165                            MetaValue::I32(i) => i,
166                            _ => 0,
167                        })?);
168                    }
169                    MetaValue::I32s(out)
170                }
171                META_U32 | META_U16 | META_U8 => {
172                    let mut out = Vec::with_capacity(len);
173                    for _ in 0..len {
174                        out.push(read_meta_value(c, elem_ty).map(|v| match v {
175                            MetaValue::U32(u) => u as i32,
176                            _ => 0,
177                        })?);
178                    }
179                    MetaValue::I32s(out)
180                }
181                other => {
182                    return Err(FormatError::Safetensors(format!(
183                        "gguf: unsupported metadata array element type {other}"
184                    )));
185                }
186            }
187        }
188        other => {
189            return Err(FormatError::Safetensors(format!(
190                "gguf: unsupported metadata type {other}"
191            )));
192        }
193    })
194}
195
196// ggml tensor types we support.
197const GGML_F32: u32 = 0;
198const GGML_F16: u32 = 1;
199const GGML_Q4_0: u32 = 2;
200const GGML_Q8_0: u32 = 8;
201const GGML_BF16: u32 = 30;
202
203impl GgufSource {
204    /// Opens and parses a `.gguf` file.
205    pub fn load(path: impl AsRef<Path>) -> Result<Self> {
206        let path = path.as_ref().to_path_buf();
207        let file = std::fs::File::open(&path)?;
208        let mmap = unsafe { Mmap::map(&file)? };
209        let mut c = Cursor::new(&mmap);
210
211        if c.u32()? != GGUF_MAGIC {
212            return Err(FormatError::Safetensors("gguf: bad magic".into()));
213        }
214        let version = c.u32()?;
215        if !(2..=3).contains(&version) {
216            return Err(FormatError::Safetensors(format!(
217                "gguf: unsupported version {version}"
218            )));
219        }
220        let tensor_count = c.u64()? as usize;
221        let kv_count = c.u64()? as usize;
222
223        let mut kv = HashMap::with_capacity(kv_count);
224        for _ in 0..kv_count {
225            let key = c.string()?;
226            let ty = c.u32()?;
227            let value = read_meta_value(&mut c, ty)?;
228            kv.insert(key, value);
229        }
230
231        let mut tensors = HashMap::with_capacity(tensor_count);
232        for _ in 0..tensor_count {
233            let name = c.string()?;
234            let n_dims = c.u32()? as usize;
235            let mut dims = Vec::with_capacity(n_dims);
236            for _ in 0..n_dims {
237                dims.push(c.u64()? as usize);
238            }
239            let ggml_type = c.u32()?;
240            let offset = c.u64()? as usize;
241            tensors.insert(name.clone(), TensorInfo { name, dims, ggml_type, offset });
242        }
243
244        // Tensor data starts after the info section, aligned to 32 bytes.
245        let alignment = match kv.get("general.alignment") {
246            Some(MetaValue::U32(a)) => *a as usize,
247            _ => 32,
248        };
249        let data_start = c.pos.div_ceil(alignment) * alignment;
250
251        let metadata = build_model_metadata(&kv)?;
252        let (eos_ids, bos_id, added_tokens) = tokenizer_ids(&kv);
253        let tokenizer_json = ensure_tokenizer_json(&path, &kv)?;
254
255        let mut source = GgufSource {
256            path,
257            mmap,
258            metadata,
259            kv,
260            tensors,
261            data_start,
262            tokenizer_json,
263            added_tokens,
264            eos_ids,
265            bos_id,
266        };
267        // GGUF llama files usually include output.weight; if absent, lm_head
268        // is tied to the embedding matrix.
269        source.metadata.tie_word_embeddings = !source.tensors.contains_key("output.weight");
270        Ok(source)
271    }
272
273    fn kv_u64(&self, key: &str) -> Option<u64> {
274        match self.kv.get(key) {
275            Some(MetaValue::U32(v)) => Some(*v as u64),
276            Some(MetaValue::U64(v)) => Some(*v),
277            Some(MetaValue::I32(v)) => Some(*v as u64),
278            _ => None,
279        }
280    }
281}
282
283fn build_model_metadata(kv: &HashMap<String, MetaValue>) -> Result<ModelMetadata> {
284    let get_u64 = |key: &str| -> Option<u64> {
285        match kv.get(key) {
286            Some(MetaValue::U32(v)) => Some(*v as u64),
287            Some(MetaValue::U64(v)) => Some(*v),
288            Some(MetaValue::I32(v)) => Some(*v as u64),
289            _ => None,
290        }
291    };
292    let get_f32 = |key: &str| -> Option<f32> {
293        match kv.get(key) {
294            Some(MetaValue::F32(v)) => Some(*v),
295            Some(MetaValue::U32(v)) => Some(*v as f32),
296            _ => None,
297        }
298    };
299    let get_str = |key: &str| -> Option<String> {
300        match kv.get(key) {
301            Some(MetaValue::String(s)) => Some(s.clone()),
302            _ => None,
303        }
304    };
305
306    let arch = get_str("general.architecture")
307        .ok_or_else(|| FormatError::MissingField("general.architecture".into()))?;
308    let prefix = arch.clone();
309    let field = |name: &str| get_u64(&format!("{prefix}.{name}"));
310
311    let hidden = field("embedding_length")
312        .ok_or_else(|| FormatError::MissingField("embedding_length".into()))? as usize;
313    let heads = field("attention.head_count")
314        .ok_or_else(|| FormatError::MissingField("attention.head_count".into()))?
315        as usize;
316    let kv_heads = field("attention.head_count_kv").unwrap_or(heads as u64) as usize;
317    let layers = field("block_count")
318        .ok_or_else(|| FormatError::MissingField("block_count".into()))? as usize;
319    let ctx = field("context_length").unwrap_or(2048) as usize;
320    let ffn = field("feed_forward_length").unwrap_or((hidden * 4) as u64) as usize;
321    let vocab = match kv.get(&format!("{prefix}.vocab_size")) {
322        Some(MetaValue::U32(v)) => *v as usize,
323        Some(MetaValue::U64(v)) => *v as usize,
324        _ => match kv.get("tokenizer.ggml.tokens") {
325            Some(MetaValue::Strings(t)) => t.len(),
326            _ => 0,
327        },
328    };
329    let (eos_ids, bos_id, _) = tokenizer_ids(kv);
330
331    Ok(ModelMetadata {
332        architecture: arch,
333        hidden_size: hidden,
334        intermediate_size: ffn,
335        num_hidden_layers: layers,
336        num_attention_heads: heads,
337        num_key_value_heads: kv_heads,
338        vocab_size: vocab,
339        max_position_embeddings: ctx,
340        rms_norm_eps: get_f32(&format!("{prefix}.attention.layer_norm_rms_epsilon"))
341            .unwrap_or(1e-5) as f64,
342        rope_theta: get_f32(&format!("{prefix}.rope.freq_base")).unwrap_or(10000.0) as f64,
343        // GGUF files usually include output.weight; if absent, the head is tied.
344        tie_word_embeddings: false, // refined in load() via tensor presence
345        head_dim: hidden / heads,
346        attention_bias: false,
347        bos_token_id: bos_id,
348        eos_token_ids: eos_ids,
349        vision: None,
350    })
351}
352
353fn tokenizer_ids(kv: &HashMap<String, MetaValue>) -> (Vec<u32>, Option<u32>, HashMap<u32, String>) {
354    let mut eos = Vec::new();
355    let mut bos = None;
356    let mut added = HashMap::new();
357    if let Some(MetaValue::U32(v)) = kv.get("tokenizer.ggml.eos_token_id") {
358        eos.push(*v);
359    }
360    if let Some(MetaValue::U32(v)) = kv.get("tokenizer.ggml.bos_token_id") {
361        bos = Some(*v);
362    }
363    // Mark special tokens from the token_type array (3 = control).
364    if let (Some(MetaValue::Strings(tokens)), Some(MetaValue::I32s(types))) =
365        (kv.get("tokenizer.ggml.tokens"), kv.get("tokenizer.ggml.token_type"))
366    {
367        for (i, (tok, ty)) in tokens.iter().zip(types.iter()).enumerate() {
368            if *ty == 3 {
369                added.insert(i as u32, tok.clone());
370            }
371        }
372    }
373    (eos, bos, added)
374}
375
376/// Builds a minimal HF BPE tokenizer.json from GGUF tokenizer metadata when
377/// no sibling tokenizer.json exists (cached alongside the model file).
378fn ensure_tokenizer_json(path: &Path, kv: &HashMap<String, MetaValue>) -> Result<PathBuf> {
379    let sibling = path.with_file_name("tokenizer.json");
380    if sibling.exists() {
381        return Ok(sibling);
382    }
383    let cached = path.with_extension("tokenizer.json");
384    if cached.exists() {
385        return Ok(cached);
386    }
387
388    let tokens = match kv.get("tokenizer.ggml.tokens") {
389        Some(MetaValue::Strings(t)) => t,
390        _ => {
391            return Err(FormatError::MissingField(
392                "tokenizer.ggml.tokens (and no sibling tokenizer.json)".into(),
393            ));
394        }
395    };
396    let scores: Vec<f32> = match kv.get("tokenizer.ggml.scores") {
397        Some(MetaValue::F32s(s)) => s.clone(),
398        _ => vec![0.0; tokens.len()],
399    };
400    let merges: Vec<String> = match kv.get("tokenizer.ggml.merges") {
401        Some(MetaValue::Strings(m)) => m.clone(),
402        _ => vec![],
403    };
404
405    let mut vocab = serde_json::Map::new();
406    for (i, tok) in tokens.iter().enumerate() {
407        vocab.insert(tok.clone(), serde_json::Value::from(i));
408    }
409    let mut ordered: Vec<usize> = (0..tokens.len()).collect();
410    ordered.sort_by(|&a, &b| {
411        scores[a].partial_cmp(&scores[b]).unwrap_or(std::cmp::Ordering::Equal)
412    });
413    let mut ranks = serde_json::Map::new();
414    for (rank, id) in ordered.iter().enumerate() {
415        ranks.insert(id.to_string(), serde_json::Value::from(rank));
416    }
417
418    let json = serde_json::json!({
419        "version": "1.0",
420        "truncation": null,
421        "padding": null,
422        "added_tokens": [],
423        "normalizer": null,
424        "pre_tokenizer": {
425            "type": "Sequence",
426            "pretokenizers": [
427                {"type": "Split", "pattern": {"Regex": "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?\\p{L}+|\\p{N}{1,3}| ?[^\\s\\p{L}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+"}, "behavior": "Isolated", "invert": false},
428                {"type": "ByteLevel", "add_prefix_space": false, "trim_offsets": true, "use_regex": false}
429            ]
430        },
431        "post_processor": null,
432        "decoder": {"type": "ByteLevel", "add_prefix_space": true, "trim_offsets": true, "use_regex": true},
433        "model": {
434            "type": "BPE",
435            "dropout": null,
436            "unk_token": null,
437            "continuing_subword_prefix": null,
438            "end_of_word_suffix": null,
439            "fuse_unk": false,
440            "byte_fallback": false,
441            "vocab": vocab,
442            "merges": merges,
443        }
444    });
445    let serialized = serde_json::to_string(&json).map_err(|e| FormatError::Safetensors(format!("tokenizer json: {e}")))?;
446    std::fs::write(&cached, serialized)?;
447    Ok(cached)
448}
449
450/// Maps a ggml tensor name to its HF equivalent (`None` = skip tensor).
451fn map_tensor_name(ggml: &str) -> Option<String> {
452    if ggml == "token_embd.weight" {
453        return Some("model.embed_tokens.weight".into());
454    }
455    if ggml == "output.weight" {
456        return Some("lm_head.weight".into());
457    }
458    if ggml == "output_norm.weight" {
459        return Some("model.norm.weight".into());
460    }
461    let rest = ggml.strip_prefix("blk.")?;
462    let (layer, rest) = rest.split_once('.')?;
463    let hf = match rest {
464        "attn_norm.weight" => "input_layernorm.weight",
465        "ffn_norm.weight" => "post_attention_layernorm.weight",
466        "attn_q.weight" => "self_attn.q_proj.weight",
467        "attn_k.weight" => "self_attn.k_proj.weight",
468        "attn_v.weight" => "self_attn.v_proj.weight",
469        "attn_output.weight" => "self_attn.o_proj.weight",
470        "attn_q.bias" => "self_attn.q_proj.bias",
471        "attn_k.bias" => "self_attn.k_proj.bias",
472        "attn_v.bias" => "self_attn.v_proj.bias",
473        "attn_output.bias" => "self_attn.o_proj.bias",
474        "ffn_gate.weight" => "mlp.gate_proj.weight",
475        "ffn_up.weight" => "mlp.up_proj.weight",
476        "ffn_down.weight" => "mlp.down_proj.weight",
477        _ => return None,
478    };
479    Some(format!("model.layers.{layer}.{hf}"))
480}
481
482/// Number of elements in a ggml tensor.
483fn num_elements(dims: &[usize]) -> usize {
484    dims.iter().product()
485}
486
487/// Byte size of a ggml tensor's data.
488fn tensor_byte_size(info: &TensorInfo) -> Result<usize> {
489    let n = num_elements(&info.dims);
490    let block = |bs: usize| -> Result<usize> {
491        if n % 32 != 0 {
492            return Err(FormatError::Safetensors(format!(
493                "gguf tensor {}: {n} elements not divisible by block size 32",
494                info.name
495            )));
496        }
497        Ok(n / 32 * bs)
498    };
499    Ok(match info.ggml_type {
500        GGML_F32 => n * 4,
501        GGML_F16 | GGML_BF16 => n * 2,
502        GGML_Q4_0 => block(18)?, // 2-byte scale + 16 bytes per 32 values
503        GGML_Q8_0 => block(34)?, // 2-byte scale + 32 int8 per 32 values
504        other => {
505            return Err(FormatError::UnsupportedDtype {
506                tensor: info.name.clone(),
507                dtype: format!("ggml_type {other} (Q4_K/Q5_K/Q6_K not yet supported)"),
508            });
509        }
510    })
511}
512
513/// Dequantizes a Q4_0 tensor to f32. Blocks of 32 values: f16 scale + 16
514/// bytes; value i (i<16) is the low nibble of byte i, value i+16 the high.
515fn dequantize_q4_0(data: &[u8], n: usize) -> Result<Vec<f32>> {
516    let mut fixed = Vec::with_capacity(n);
517    for block in data.chunks_exact(18) {
518        let d = half::f16::from_le_bytes([block[0], block[1]]).to_f32();
519        let mut vals = [0f32; 32];
520        for j in 0..16 {
521            let byte = block[2 + j];
522            vals[j] = ((byte & 0x0F) as i32 - 8) as f32 * d;
523            vals[j + 16] = ((byte >> 4) as i32 - 8) as f32 * d;
524        }
525        fixed.extend_from_slice(&vals);
526    }
527    if fixed.len() != n {
528        return Err(FormatError::Safetensors(format!(
529            "q4_0 dequant size mismatch: {} != {n}",
530            fixed.len()
531        )));
532    }
533    Ok(fixed)
534}
535
536/// Dequantizes a Q8_0 tensor to f32.
537fn dequantize_q8_0(data: &[u8], n: usize) -> Result<Vec<f32>> {
538    let mut out = Vec::with_capacity(n);
539    for block in data.chunks_exact(34) {
540        let d = half::f16::from_le_bytes([block[0], block[1]]).to_f32();
541        for &b in &block[2..34] {
542            out.push((b as i8) as f32 * d);
543        }
544    }
545    if out.len() != n {
546        return Err(FormatError::Safetensors(format!(
547            "q8_0 dequant size mismatch: {} != {n}",
548            out.len()
549        )));
550    }
551    Ok(out)
552}
553
554impl ModelSource for GgufSource {
555    fn metadata(&self) -> &ModelMetadata {
556        &self.metadata
557    }
558
559    fn tensor_names(&self) -> Vec<String> {
560        self.tensors
561            .keys()
562            .filter_map(|k| map_tensor_name(k))
563            .collect()
564    }
565
566    fn open_tensor(&self, name: &str) -> Result<TensorReader<'_>> {
567        // Find the ggml tensor mapping to this HF name.
568        let (ggml_name, info) = self
569            .tensors
570            .iter()
571            .find(|(k, _)| map_tensor_name(k).as_deref() == Some(name))
572            .ok_or_else(|| FormatError::TensorNotFound(name.to_string()))?;
573        let _ = ggml_name;
574
575        let size = tensor_byte_size(info)?;
576        let start = self.data_start + info.offset;
577        let data = self
578            .mmap
579            .get(start..start + size)
580            .ok_or_else(|| FormatError::Safetensors(format!("gguf tensor {} out of bounds", info.name)))?;
581
582        // HF layout = ggml dims reversed (row-major data needs no movement).
583        let shape: Vec<usize> = info.dims.iter().rev().copied().collect();
584        let n = num_elements(&info.dims);
585
586        match info.ggml_type {
587            GGML_F32 | GGML_F16 | GGML_BF16 => {
588                let dtype = match info.ggml_type {
589                    GGML_F32 => TensorDtype::F32,
590                    GGML_F16 => TensorDtype::F16,
591                    _ => TensorDtype::BF16,
592                };
593                Ok(TensorReader::new(name.to_string(), shape, dtype, data))
594            }
595            GGML_Q4_0 => {
596                let values = dequantize_q4_0(data, n)?;
597                let bytes: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
598                Ok(TensorReader::owned(name.to_string(), shape, bytes))
599            }
600            GGML_Q8_0 => {
601                let values = dequantize_q8_0(data, n)?;
602                let bytes: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
603                Ok(TensorReader::owned(name.to_string(), shape, bytes))
604            }
605            other => Err(FormatError::UnsupportedDtype {
606                tensor: info.name.clone(),
607                dtype: format!("ggml_type {other}"),
608            }),
609        }
610    }
611
612    fn tokenizer(&self) -> Result<TokenizerSpec> {
613        Ok(TokenizerSpec {
614            tokenizer_json: self.tokenizer_json.clone(),
615            added_tokens: self.added_tokens.clone(),
616            chat_template: None,
617        })
618    }
619
620    fn sampler_defaults(&self) -> Option<SamplerConfig> {
621        None
622    }
623}
624
625impl GgufSource {
626    /// End-of-sequence ids from tokenizer metadata.
627    pub fn eos_token_ids(&self) -> &[u32] {
628        &self.eos_ids
629    }
630
631    /// Model file path.
632    pub fn path(&self) -> &Path {
633        &self.path
634    }
635}