Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions bindings/go/model_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,9 @@ type ModelPaths struct {
ModelName string
RuntimeID string
ModelType ModelType
// QairtVersion is the QAIRT version the model's assets were pulled for
// (from AI Hub metadata); empty for non-AI-Hub / llama.cpp models.
QairtVersion string
}

// ModelGetPaths resolves "org/repo[:precision]" (or alias) to absolute on-disk paths.
Expand All @@ -362,6 +365,7 @@ func ModelGetPaths(name string) (*ModelPaths, error) {
ModelName: C.GoString(out.model_name),
RuntimeID: C.GoString(out.plugin_id),
ModelType: ModelType(out.model_type),
QairtVersion: C.GoString(out.qairt_version),
}, nil
}

Expand Down
1 change: 1 addition & 0 deletions bindings/python/geniex/_ffi/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ class geniex_ModelPaths(Structure):
('model_name', c_char_p),
('plugin_id', c_char_p),
('model_type', c_int32),
('qairt_version', c_char_p),
]


Expand Down
4 changes: 4 additions & 0 deletions bindings/python/geniex/model_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ class ModelPaths:
model_type: str # "llm" or "vlm"
mmproj_path: str | None = None
tokenizer_path: str | None = None
# QAIRT version the model's assets were pulled for (AI Hub metadata);
# empty for non-AI-Hub / llama.cpp models.
qairt_version: str = ''


@dataclass(frozen=True)
Expand Down Expand Up @@ -406,6 +409,7 @@ def get_paths(model_name: str) -> ModelPaths:
model_type=_type_str(out.model_type),
mmproj_path=out.mmproj_path.decode() if out.mmproj_path else None,
tokenizer_path=out.tokenizer_path.decode() if out.tokenizer_path else None,
qairt_version=out.qairt_version.decode() if out.qairt_version else '',
)
finally:
lib.geniex_model_paths_free(byref(out))
Expand Down
18 changes: 4 additions & 14 deletions cli/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,6 @@ import (
"github.com/spf13/viper"
)

// DefaultAIHubBaseURL is the public root for Qualcomm AI Hub release assets.
const DefaultAIHubBaseURL = "https://qaihub-public-assets.s3.us-west-2.amazonaws.com/qai-hub-models"

// DefaultAIHubVersion is the pinned aihm release the CLI consumes. The public
// bucket has no `latest` alias; manifests are only at
// <base>/releases/<version>/manifest.json. Override via GENIEX_AIHUBVERSION.
const DefaultAIHubVersion = "v0.57.0"

type Config struct {
// Global settings
DataDir string
Expand All @@ -39,17 +31,15 @@ type Config struct {
KeyFile string // TLS private key file path

// Env only params
HFToken string
Log string
AIHubVersion string // Override the pinned aihm release version
HFToken string
Log string
}

// init sets up viper defaults and env binding. Runs once at package load.
func init() {
// ENV only param need to set default here
viper.SetDefault("hftoken", "") // Default empty token
viper.SetDefault("log", "none") // Default log level
viper.SetDefault("aihubversion", DefaultAIHubVersion) // Pinned aihm release version
viper.SetDefault("hftoken", "") // Default empty token
viper.SetDefault("log", "none") // Default log level

viper.SetEnvPrefix("geniex")
viper.AutomaticEnv()
Expand Down
61 changes: 60 additions & 1 deletion sdk/model-manager/crates/core/src/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ where
/// Historical `DeviceId` and `MinSDKVersion` keys are accepted (serde
/// silently drops unknown JSON fields on deserialize) but no longer
/// serialised — qairt / llama_cpp plugins don't read them and AI Hub
/// hub already tracks the chipset out-of-band.
/// hub already tracks the chipset out-of-band. `QairtVersion` (below) is
/// the current, purposeful version stamp, sourced from AI Hub asset
/// metadata rather than a placeholder.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelManifest {
#[serde(rename = "Name")]
Expand All @@ -62,6 +64,16 @@ pub struct ModelManifest {
skip_serializing_if = "String::is_empty"
)]
pub precision: String,
/// QAIRT version the model's assets were exported against, captured at
/// pull time from AI Hub's `release-assets.json` (`tool_versions.qairt`).
/// Lets a later run detect artifacts left stale by a GenieX update whose
/// bundled QAIRT no longer matches. Empty for non-AI-Hub / llama.cpp models.
#[serde(
rename = "QairtVersion",
default,
skip_serializing_if = "String::is_empty"
)]
pub qairt_version: String,
#[serde(rename = "ModelFile", default, deserialize_with = "null_as_default")]
pub model_file: HashMap<String, ModelFileInfo>,
#[serde(rename = "MMProjFile", default, deserialize_with = "null_as_default")]
Expand Down Expand Up @@ -104,3 +116,50 @@ pub struct DownloadInfo {
pub total_downloaded: i64,
pub total_size: i64,
}

#[cfg(test)]
mod tests {
use super::*;

fn sample() -> ModelManifest {
ModelManifest {
name: "org/repo".into(),
model_name: "repo".into(),
model_type: ModelType::Llm,
plugin_id: "qairt".into(),
precision: "W4A16".into(),
qairt_version: "2.45.0.260326154327".into(),
model_file: HashMap::new(),
mmproj_file: ModelFileInfo::default(),
tokenizer_file: ModelFileInfo::default(),
extra_files: Vec::new(),
}
}

#[test]
fn serializes_qairt_version_key() {
let json = serde_json::to_string(&sample()).unwrap();
assert!(
json.contains(r#""QairtVersion":"2.45.0.260326154327""#),
"missing QairtVersion key: {json}"
);
}

#[test]
fn empty_qairt_version_is_omitted() {
// Non-AI-Hub models leave the stamp empty; the key must not appear.
let mut m = sample();
m.qairt_version = String::new();
let json = serde_json::to_string(&m).unwrap();
assert!(!json.contains("QairtVersion"), "unexpected key: {json}");
}

#[test]
fn legacy_manifest_without_qairt_version_deserializes() {
// geniex.json written before this field must still load.
let legacy =
r#"{"Name":"org/repo","ModelName":"repo","ModelType":"llm","PluginId":"qairt"}"#;
let m: ModelManifest = serde_json::from_str(legacy).unwrap();
assert!(m.qairt_version.is_empty());
}
}
1 change: 1 addition & 0 deletions sdk/model-manager/crates/core/src/manifest_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ pub fn infer_manifest_from_names(
model_type,
plugin_id,
precision: String::new(),
qairt_version: String::new(),
model_file,
mmproj_file,
tokenizer_file,
Expand Down
6 changes: 6 additions & 0 deletions sdk/model-manager/crates/core/src/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ pub struct ModelPaths {
pub model_name: String,
pub plugin_id: String,
pub model_type: ModelType,
/// QAIRT version the model's assets were exported against (from the
/// manifest). Empty for non-AI-Hub / llama.cpp models. Lets callers
/// detect artifacts left stale by a GenieX update.
pub qairt_version: String,
}

/// Resolve file paths from a manifest + local base directory + optional quant hint.
Expand Down Expand Up @@ -84,6 +88,7 @@ pub fn resolve_model_paths(
model_name: manifest.model_name.clone(),
plugin_id: manifest.plugin_id.clone(),
model_type: manifest.model_type.clone(),
qairt_version: manifest.qairt_version.clone(),
},
))
}
Expand Down Expand Up @@ -145,6 +150,7 @@ mod tests {
model_type: ModelType::Llm,
plugin_id: "llama_cpp".to_string(),
precision: String::new(),
qairt_version: String::new(),
model_file,
mmproj_file: ModelFileInfo::default(),
tokenizer_file: ModelFileInfo::default(),
Expand Down
1 change: 1 addition & 0 deletions sdk/model-manager/crates/core/src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ mod tests {
model_type: ModelType::Llm,
plugin_id: "llama_cpp".to_string(),
precision: String::new(),
qairt_version: String::new(),
model_file,
mmproj_file: ModelFileInfo::default(),
tokenizer_file: ModelFileInfo::default(),
Expand Down
68 changes: 68 additions & 0 deletions sdk/model-manager/crates/core/src/source/ai_hub/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,18 @@ pub struct AssetDetails {
pub download_url: String,
#[serde(default)]
pub uncompressed_size: Option<u64>,
/// Tool versions the asset was built against, e.g. `{"qairt": "2.45.0.…"}`.
/// Genie/QAIRT assets carry `qairt`; llama.cpp assets ship an empty map.
#[serde(default)]
pub tool_versions: ToolVersions,
}

/// Compiler/runtime versions an AI Hub asset was exported with.
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct ToolVersions {
#[serde(default)]
pub qairt: String,
}

/// `platform.json`: chipset catalogue with aliases used to canonicalize
Expand All @@ -117,3 +129,59 @@ pub struct ChipsetInfo {
#[serde(default)]
pub aliases: Vec<String>,
}

#[cfg(test)]
mod tests {
use super::*;

// Trimmed from a live release-assets.json (qwen3_4b, v0.57.0). Genie
// assets carry tool_versions.qairt; llama.cpp assets ship an empty map.
const RELEASE_ASSETS: &str = r#"{
"model_id": "qwen3_4b",
"aihm_version": "0.57.0",
"assets": [
{
"precision": "PRECISION_W4A16",
"runtime": "RUNTIME_GENIE",
"chipset": "qualcomm-snapdragon-x-elite",
"download_url": "https://example.invalid/qwen3_4b.zip",
"tool_versions": {"qairt": "2.45.0.260326154327"}
},
{
"precision": "PRECISION_W4A16",
"runtime": "RUNTIME_GENIEX_LLAMACPP",
"chipset": null,
"download_url": "https://example.invalid/qwen3_4b-llamacpp.zip",
"tool_versions": {}
}
]
}"#;

#[test]
fn parses_qairt_tool_version_from_genie_asset() {
let ra: ModelReleaseAssets = serde_json::from_str(RELEASE_ASSETS).unwrap();
let genie = ra
.assets
.iter()
.find(|a| a.runtime == "RUNTIME_GENIE")
.unwrap();
assert_eq!(genie.tool_versions.qairt, "2.45.0.260326154327");
}

#[test]
fn tool_versions_defaults_empty_when_absent_or_bare() {
let ra: ModelReleaseAssets = serde_json::from_str(RELEASE_ASSETS).unwrap();
let llama = ra
.assets
.iter()
.find(|a| a.runtime == "RUNTIME_GENIEX_LLAMACPP")
.unwrap();
assert!(llama.tool_versions.qairt.is_empty());

// Assets predating the tool_versions field must still parse.
let legacy =
r#"{"runtime":"RUNTIME_GENIE","precision":"","download_url":"","chipset":null}"#;
let asset: AssetDetails = serde_json::from_str(legacy).unwrap();
assert!(asset.tool_versions.qairt.is_empty());
}
}
1 change: 1 addition & 0 deletions sdk/model-manager/crates/core/src/source/ai_hub/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ impl ModelSource for AiHubSource {
model_type,
plugin_id: "qairt".to_string(),
precision: precision_label,
qairt_version: asset.tool_versions.qairt.clone(),
model_file,
mmproj_file: ModelFileInfo::default(),
tokenizer_file: ModelFileInfo::default(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ impl std::fmt::Display for UnavailableChipset {
#[cfg(test)]
mod tests {
use super::*;
use crate::source::ai_hub::manifest::ChipsetInfo;
use crate::source::ai_hub::manifest::{ChipsetInfo, ToolVersions};

fn platform(entries: &[(&str, &[&str])]) -> PlatformInfo {
PlatformInfo {
Expand Down Expand Up @@ -199,6 +199,7 @@ mod tests {
precision: precision.to_string(),
download_url: format!("https://example.invalid/{chipset}-{precision}.zip"),
uncompressed_size: Some(1),
tool_versions: ToolVersions::default(),
}
}

Expand Down
1 change: 1 addition & 0 deletions sdk/model-manager/crates/core/src/source/dockerhub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@ fn build_plan(
model_type,
plugin_id: "llama_cpp".to_string(),
precision: config.quantization.clone(),
qairt_version: String::new(),
model_file,
mmproj_file,
tokenizer_file: ModelFileInfo::default(),
Expand Down
2 changes: 2 additions & 0 deletions sdk/model-manager/crates/core/src/source/localfs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ impl LocalFsSource {
model_type,
plugin_id: QAIRT_PLUGIN_ID.to_string(),
precision: String::new(),
qairt_version: String::new(),
model_file,
mmproj_file: ModelFileInfo::default(),
tokenizer_file: ModelFileInfo::default(),
Expand Down Expand Up @@ -315,6 +316,7 @@ impl LocalFsSource {
model_type,
plugin_id: QAIRT_PLUGIN_ID.to_string(),
precision: String::new(),
qairt_version: String::new(),
model_file,
mmproj_file: ModelFileInfo::default(),
tokenizer_file: ModelFileInfo::default(),
Expand Down
1 change: 1 addition & 0 deletions sdk/model-manager/crates/core/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ mod tests {
model_type: ModelType::Llm,
plugin_id: "llama_cpp".to_string(),
precision: String::new(),
qairt_version: String::new(),
model_file,
mmproj_file: ModelFileInfo::default(),
tokenizer_file: ModelFileInfo::default(),
Expand Down
4 changes: 3 additions & 1 deletion sdk/model-manager/crates/core/tests/ai_hub_pull.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,8 @@ async fn ai_hub_pull_writes_manifest_and_extracts_flat() {
"runtime": "RUNTIME_GENIE",
"precision": "PRECISION_W4A16",
"download_url": "{asset_url}",
"uncompressed_size": {}
"uncompressed_size": {},
"tool_versions": {{"qairt": "2.45.0.260326154327"}}
}}
]
}}"#,
Expand Down Expand Up @@ -212,6 +213,7 @@ async fn ai_hub_pull_writes_manifest_and_extracts_flat() {
let mf = store.get_manifest("tests/TestNet").unwrap();
assert_eq!(mf.plugin_id, "qairt");
assert_eq!(mf.precision, "W4A16");
assert_eq!(mf.qairt_version, "2.45.0.260326154327");
let entry = mf.model_file.get("N/A").expect("N/A quant entry");
assert_eq!(entry.name, "model-00.bin");
assert!(entry.downloaded);
Expand Down
4 changes: 4 additions & 0 deletions sdk/model-manager/crates/ffi/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub struct GenieXModelPaths {
pub model_name: *mut c_char,
pub plugin_id: *mut c_char,
pub model_type: GenieXModelType,
pub qairt_version: *mut c_char,
}

impl GenieXModelPaths {
Expand All @@ -45,6 +46,7 @@ impl GenieXModelPaths {
model_name: std::ptr::null_mut(),
plugin_id: std::ptr::null_mut(),
model_type: GenieXModelType::Llm,
qairt_version: std::ptr::null_mut(),
}
}
}
Expand Down Expand Up @@ -73,6 +75,7 @@ pub extern "C" fn geniex_model_get_paths(
(*out_paths).model_dir = str_to_cptr(&paths.model_dir.to_string_lossy());
(*out_paths).model_name = str_to_cptr(&paths.model_name);
(*out_paths).plugin_id = str_to_cptr(&paths.plugin_id);
(*out_paths).qairt_version = str_to_cptr(&paths.qairt_version);
(*out_paths).mmproj_path = paths
.mmproj_path
.as_ref()
Expand Down Expand Up @@ -104,6 +107,7 @@ pub unsafe extern "C" fn geniex_model_paths_free(paths: *mut GenieXModelPaths) {
free_cptr(p.model_dir);
free_cptr(p.model_name);
free_cptr(p.plugin_id);
free_cptr(p.qairt_version);
*paths = GenieXModelPaths::null();
}

Expand Down
1 change: 1 addition & 0 deletions sdk/model-manager/include/geniex_model.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ typedef struct {
char* model_name; /**< Architecture name, e.g. "qwen3-4b". */
char* plugin_id; /**< Plugin ID, e.g. "llama_cpp". */
geniex_ModelType model_type; /**< LLM or VLM. */
char* qairt_version; /**< QAIRT version the assets were built for; "" if none. */
} geniex_ModelPaths;

/**
Expand Down
Loading