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:
parent
da31675f64
commit
f4661912b0
@ -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
39
src-tauri/Cargo.lock
generated
@ -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",
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,2 +1,3 @@
|
|||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod cleanup;
|
pub mod cleanup;
|
||||||
|
pub mod gguf;
|
||||||
|
|||||||
@ -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()),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user