feat: Add GGUF metadata reading functionality (#6120)

* feat: Add GGUF metadata reading functionality

This commit introduces a new Tauri command and a corresponding function to read metadata from GGUF model files.

The new read_gguf_metadata command in the Rust backend uses the byteorder crate to parse the GGUF file format and extract key metadata. This information, including the file's version, tensor count, and a key-value map of other metadata, is then made available to the TypeScript frontend.

This functionality is a foundational step toward providing users with more detailed information about their loaded models directly within the application.

This will be refactored later.

fixes: #6001

* loadMetadata() should return

* Properly throw eror to FE

* Use BufReader to improve performance
This commit is contained in:
Akarshan Biswas 2025-08-13 22:54:20 +05:30 committed by GitHub
parent da31675f64
commit f4661912b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 235 additions and 38 deletions

View File

@ -100,6 +100,13 @@ interface DeviceList {
mem: number mem: number
free: number free: number
} }
interface GgufMetadata {
version: number
tensor_count: number
metadata: Record<string, string>
}
/** /**
* Override the default app.log function to use Jan's logging system. * Override the default app.log function to use Jan's logging system.
* @param args * @param args
@ -1591,4 +1598,15 @@ export default class llamacpp_extension extends AIEngine {
override getChatClient(sessionId: string): any { override getChatClient(sessionId: string): any {
throw new Error('method not implemented yet') throw new Error('method not implemented yet')
} }
private async loadMetadata(path: string): Promise<GgufMetadata> {
try {
const data = await invoke<GgufMetadata>('read_gguf_metadata', {
path: path,
})
return data
} catch (err) {
throw err
}
}
} }

39
src-tauri/Cargo.lock generated
View File

@ -8,6 +8,7 @@ version = "0.6.599"
dependencies = [ dependencies = [
"ash", "ash",
"base64 0.22.1", "base64 0.22.1",
"byteorder",
"dirs", "dirs",
"env", "env",
"fix-path-env", "fix-path-env",
@ -86,21 +87,6 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "alloc-no-stdlib"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
[[package]]
name = "alloc-stdlib"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
dependencies = [
"alloc-no-stdlib",
]
[[package]] [[package]]
name = "android-tzdata" name = "android-tzdata"
version = "0.1.1" version = "0.1.1"
@ -467,27 +453,6 @@ dependencies = [
"syn 2.0.104", "syn 2.0.104",
] ]
[[package]]
name = "brotli"
version = "8.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
"brotli-decompressor",
]
[[package]]
name = "brotli-decompressor"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
]
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.19.0" version = "3.19.0"
@ -5006,7 +4971,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b54a99a6cd8e01abcfa61508177e6096a4fe2681efecee9214e962f2f073ae4a" checksum = "b54a99a6cd8e01abcfa61508177e6096a4fe2681efecee9214e962f2f073ae4a"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"brotli",
"ico", "ico",
"json-patch", "json-patch",
"plist", "plist",
@ -5344,7 +5308,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330c15cabfe1d9f213478c9e8ec2b0c76dab26bb6f314b8ad1c8a568c1d186e" checksum = "9330c15cabfe1d9f213478c9e8ec2b0c76dab26bb6f314b8ad1c8a568c1d186e"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"brotli",
"cargo_metadata", "cargo_metadata",
"ctor", "ctor",
"dunce", "dunce",

View File

@ -74,6 +74,7 @@ sha2 = "0.10.9"
base64 = "0.22.1" base64 = "0.22.1"
libloading = "0.8.7" libloading = "0.8.7"
thiserror = "2.0.12" thiserror = "2.0.12"
byteorder = "1.5.0"
[dependencies.tauri] [dependencies.tauri]
version = "2.5.0" version = "2.5.0"

View File

@ -0,0 +1,213 @@
use byteorder::{LittleEndian, ReadBytesExt};
use serde::Serialize;
use std::{
collections::HashMap,
convert::TryFrom,
fs::File,
io::{self, Read, Seek, BufReader},
path::Path,
};
#[derive(Debug, Clone, Copy)]
#[repr(u32)]
enum GgufValueType {
Uint8 = 0,
Int8 = 1,
Uint16 = 2,
Int16 = 3,
Uint32 = 4,
Int32 = 5,
Float32 = 6,
Bool = 7,
String = 8,
Array = 9,
Uint64 = 10,
Int64 = 11,
Float64 = 12,
}
impl TryFrom<u32> for GgufValueType {
type Error = io::Error;
fn try_from(value: u32) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::Uint8),
1 => Ok(Self::Int8),
2 => Ok(Self::Uint16),
3 => Ok(Self::Int16),
4 => Ok(Self::Uint32),
5 => Ok(Self::Int32),
6 => Ok(Self::Float32),
7 => Ok(Self::Bool),
8 => Ok(Self::String),
9 => Ok(Self::Array),
10 => Ok(Self::Uint64),
11 => Ok(Self::Int64),
12 => Ok(Self::Float64),
_ => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Unknown GGUF value type: {}", value),
)),
}
}
}
#[derive(Serialize)]
pub struct GgufMetadata {
version: u32,
tensor_count: u64,
metadata: HashMap<String, String>,
}
fn read_gguf_metadata_internal<P: AsRef<Path>>(path: P) -> io::Result<GgufMetadata> {
let mut file = BufReader::new(File::open(path)?);
let mut magic = [0u8; 4];
file.read_exact(&mut magic)?;
if &magic != b"GGUF" {
return Err(io::Error::new(io::ErrorKind::InvalidData, "Not a GGUF file"));
}
let version = file.read_u32::<LittleEndian>()?;
let tensor_count = file.read_u64::<LittleEndian>()?;
let metadata_count = file.read_u64::<LittleEndian>()?;
let mut metadata_map = HashMap::new();
for i in 0..metadata_count {
match read_metadata_entry(&mut file, i) {
Ok((key, value)) => {
metadata_map.insert(key, value);
}
Err(e) => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Error reading metadata entry {}: {}", i, e),
));
}
}
}
Ok(GgufMetadata {
version,
tensor_count,
metadata: metadata_map,
})
}
fn read_metadata_entry<R: Read + Seek>(reader: &mut R, index: u64) -> io::Result<(String, String)> {
let key = read_gguf_string(reader).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Failed to read key for metadata entry {}: {}", index, e),
)
})?;
let value_type_u32 = reader.read_u32::<LittleEndian>()?;
let value_type = GgufValueType::try_from(value_type_u32)?;
let value = read_gguf_value(reader, value_type)?;
Ok((key, value))
}
fn read_gguf_string<R: Read>(reader: &mut R) -> io::Result<String> {
let len = reader.read_u64::<LittleEndian>()?;
if len > (1024 * 1024) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("String length {} is unreasonably large", len),
));
}
let mut buf = vec![0u8; len as usize];
reader.read_exact(&mut buf)?;
Ok(String::from_utf8(buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?)
}
fn read_gguf_value<R: Read + Seek>(
reader: &mut R,
value_type: GgufValueType,
) -> io::Result<String> {
match value_type {
GgufValueType::Uint8 => Ok(reader.read_u8()?.to_string()),
GgufValueType::Int8 => Ok(reader.read_i8()?.to_string()),
GgufValueType::Uint16 => Ok(reader.read_u16::<LittleEndian>()?.to_string()),
GgufValueType::Int16 => Ok(reader.read_i16::<LittleEndian>()?.to_string()),
GgufValueType::Uint32 => Ok(reader.read_u32::<LittleEndian>()?.to_string()),
GgufValueType::Int32 => Ok(reader.read_i32::<LittleEndian>()?.to_string()),
GgufValueType::Float32 => Ok(reader.read_f32::<LittleEndian>()?.to_string()),
GgufValueType::Bool => Ok((reader.read_u8()? != 0).to_string()),
GgufValueType::String => read_gguf_string(reader),
GgufValueType::Uint64 => Ok(reader.read_u64::<LittleEndian>()?.to_string()),
GgufValueType::Int64 => Ok(reader.read_i64::<LittleEndian>()?.to_string()),
GgufValueType::Float64 => Ok(reader.read_f64::<LittleEndian>()?.to_string()),
GgufValueType::Array => {
let elem_type_u32 = reader.read_u32::<LittleEndian>()?;
let elem_type = GgufValueType::try_from(elem_type_u32)?;
let len = reader.read_u64::<LittleEndian>()?;
if len > 1_000_000 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Array length {} is unreasonably large", len),
));
}
if len > 24 {
skip_array_data(reader, elem_type, len)?;
return Ok(format!(
"<Array of type {:?} with {} elements, data skipped>",
elem_type, len
));
}
let mut elems = Vec::with_capacity(len as usize);
for _ in 0..len {
elems.push(read_gguf_value(reader, elem_type)?);
}
Ok(format!("[{}]", elems.join(", ")))
}
}
}
fn skip_array_data<R: Read + Seek>(
reader: &mut R,
elem_type: GgufValueType,
len: u64,
) -> io::Result<()> {
match elem_type {
GgufValueType::Uint8 | GgufValueType::Int8 | GgufValueType::Bool => {
reader.seek(io::SeekFrom::Current(len as i64))?;
}
GgufValueType::Uint16 | GgufValueType::Int16 => {
reader.seek(io::SeekFrom::Current((len * 2) as i64))?;
}
GgufValueType::Uint32 | GgufValueType::Int32 | GgufValueType::Float32 => {
reader.seek(io::SeekFrom::Current((len * 4) as i64))?;
}
GgufValueType::Uint64 | GgufValueType::Int64 | GgufValueType::Float64 => {
reader.seek(io::SeekFrom::Current((len * 8) as i64))?;
}
GgufValueType::String => {
for _ in 0..len {
let str_len = reader.read_u64::<LittleEndian>()?;
reader.seek(io::SeekFrom::Current(str_len as i64))?;
}
}
GgufValueType::Array => {
for _ in 0..len {
read_gguf_value(reader, elem_type)?;
}
}
}
Ok(())
}
#[tauri::command]
pub async fn read_gguf_metadata(path: String) -> Result<GgufMetadata, String> {
// run the blocking code in a separate thread pool
tauri::async_runtime::spawn_blocking(move || {
read_gguf_metadata_internal(path)
.map_err(|e| e.to_string())
})
.await
.unwrap()
}

View File

@ -1,2 +1,3 @@
pub mod server; pub mod server;
pub mod cleanup; pub mod cleanup;
pub mod gguf;

View File

@ -102,6 +102,7 @@ pub fn run() {
core::utils::extensions::inference_llamacpp_extension::server::get_loaded_models, core::utils::extensions::inference_llamacpp_extension::server::get_loaded_models,
core::utils::extensions::inference_llamacpp_extension::server::generate_api_key, core::utils::extensions::inference_llamacpp_extension::server::generate_api_key,
core::utils::extensions::inference_llamacpp_extension::server::is_process_running, core::utils::extensions::inference_llamacpp_extension::server::is_process_running,
core::utils::extensions::inference_llamacpp_extension::gguf::read_gguf_metadata,
]) ])
.manage(AppState { .manage(AppState {
app_token: Some(generate_app_token()), app_token: Some(generate_app_token()),