use std::fmt;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed};
use std::sync::Arc;
use super::api::Api;
use super::info::build_model_info;
use super::native::NativeModel;
use super::task::spawn_blocking;
use crate::error::{FoundryLocalError, Result};
use crate::types::ModelInfo;
#[derive(Clone)]
pub(crate) struct VariantData {
native: NativeModel,
info: ModelInfo,
}
pub struct Model {
inner: ModelKind,
}
type DownloadProgressCallback = Box<dyn FnMut(f64) + Send + 'static>;
pub struct DownloadBuilder<'a> {
model: &'a Model,
progress: Option<DownloadProgressCallback>,
cancel_flag: Option<Arc<AtomicBool>>,
}
impl<'a> DownloadBuilder<'a> {
fn new(model: &'a Model) -> Self {
Self {
model,
progress: None,
cancel_flag: None,
}
}
pub fn progress<F>(mut self, callback: F) -> Self
where
F: FnMut(f64) + Send + 'static,
{
self.progress = Some(Box::new(callback));
self
}
pub fn cancel(mut self, cancel_flag: Arc<AtomicBool>) -> Self {
self.cancel_flag = Some(cancel_flag);
self
}
pub async fn run(self) -> Result<()> {
let native = self.model.selected_variant().native.clone();
let progress = self.progress;
let cancel_flag = self.cancel_flag;
spawn_blocking(move || native.download(progress, cancel_flag)).await
}
}
enum ModelKind {
Variant(Arc<VariantData>),
Group {
alias: String,
variants: Vec<Arc<VariantData>>,
selected: AtomicUsize,
},
}
impl Clone for Model {
fn clone(&self) -> Self {
Self {
inner: match &self.inner {
ModelKind::Variant(v) => ModelKind::Variant(v.clone()),
ModelKind::Group {
alias,
variants,
selected,
} => ModelKind::Group {
alias: alias.clone(),
variants: variants.clone(),
selected: AtomicUsize::new(selected.load(Relaxed)),
},
},
}
}
}
impl fmt::Debug for Model {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.inner {
ModelKind::Variant(v) => f
.debug_struct("Model::ModelVariant")
.field("id", &v.info.id)
.field("alias", &v.info.alias)
.finish(),
ModelKind::Group {
alias,
variants,
selected,
} => f
.debug_struct("Model::Model")
.field("alias", alias)
.field("id", &variants[selected.load(Relaxed)].info.id)
.field("variants_count", &variants.len())
.field("selected_index", &selected.load(Relaxed))
.finish(),
}
}
}
impl Model {
pub(crate) fn from_variant(api: &Arc<Api>, native: NativeModel) -> Result<Self> {
let info = build_model_info(api, &native)?;
Ok(Self {
inner: ModelKind::Variant(Arc::new(VariantData { native, info })),
})
}
pub(crate) fn from_group(api: &Arc<Api>, native: NativeModel) -> Result<Self> {
let group_info = build_model_info(api, &native)?;
let alias = group_info.alias.clone();
let mut variants = Vec::new();
for variant_native in native.get_variants()? {
let info = build_model_info(api, &variant_native)?;
variants.push(Arc::new(VariantData {
native: variant_native,
info,
}));
}
if variants.is_empty() {
return Ok(Self {
inner: ModelKind::Variant(Arc::new(VariantData {
native,
info: group_info,
})),
});
}
let selected = variants.iter().position(|v| v.info.cached).unwrap_or(0);
Ok(Self {
inner: ModelKind::Group {
alias,
variants,
selected: AtomicUsize::new(selected),
},
})
}
}
impl Model {
fn selected_variant(&self) -> &VariantData {
match &self.inner {
ModelKind::Variant(v) => v.as_ref(),
ModelKind::Group {
variants, selected, ..
} => variants[selected.load(Relaxed)].as_ref(),
}
}
pub(crate) fn selected_native(&self) -> &NativeModel {
&self.selected_variant().native
}
}
impl Model {
pub fn id(&self) -> &str {
&self.selected_variant().info.id
}
pub fn alias(&self) -> &str {
match &self.inner {
ModelKind::Variant(v) => &v.info.alias,
ModelKind::Group { alias, .. } => alias,
}
}
pub fn info(&self) -> Result<ModelInfo> {
let variant = self.selected_variant();
build_model_info(&variant.native.api, &variant.native)
}
pub fn context_length(&self) -> Option<u64> {
self.selected_variant().info.context_length
}
pub fn input_modalities(&self) -> Option<&str> {
self.selected_variant().info.input_modalities.as_deref()
}
pub fn output_modalities(&self) -> Option<&str> {
self.selected_variant().info.output_modalities.as_deref()
}
pub fn capabilities(&self) -> Option<&str> {
self.selected_variant().info.capabilities.as_deref()
}
pub fn supports_tool_calling(&self) -> Option<bool> {
self.selected_variant().info.supports_tool_calling
}
pub async fn is_cached(&self) -> Result<bool> {
let native = self.selected_native().clone();
spawn_blocking(move || native.is_cached()).await
}
pub async fn is_loaded(&self) -> Result<bool> {
let native = self.selected_native().clone();
spawn_blocking(move || native.is_loaded()).await
}
pub async fn download<F>(&self, progress: Option<F>) -> Result<()>
where
F: FnMut(f64) + Send + 'static,
{
let native = self.selected_native().clone();
let progress: Option<DownloadProgressCallback> =
progress.map(|f| Box::new(f) as DownloadProgressCallback);
spawn_blocking(move || native.download(progress, None)).await
}
pub fn download_builder(&self) -> DownloadBuilder<'_> {
DownloadBuilder::new(self)
}
pub async fn path(&self) -> Result<PathBuf> {
let native = self.selected_native().clone();
let id = self.id().to_owned();
let path = spawn_blocking(move || native.path()).await?;
match path {
Some(p) => Ok(PathBuf::from(p)),
None => Err(FoundryLocalError::ModelOperation {
reason: format!("Error getting path for model {id}. Has it been downloaded?"),
}),
}
}
pub async fn load(&self) -> Result<()> {
let native = self.selected_native().clone();
spawn_blocking(move || native.load()).await
}
pub async fn unload(&self) -> Result<()> {
let native = self.selected_native().clone();
spawn_blocking(move || native.unload()).await
}
pub async fn remove_from_cache(&self) -> Result<()> {
let native = self.selected_native().clone();
spawn_blocking(move || native.remove_from_cache()).await
}
#[deprecated(
since = "2.0.0",
note = "The OpenAI direct clients are deprecated; use `ChatSession::new(&model)` instead."
)]
#[allow(deprecated)]
pub fn create_chat_client(&self) -> crate::openai::ChatClient {
let v = self.selected_variant();
crate::openai::ChatClient::new(&v.info.id, v.native.clone())
}
#[deprecated(
since = "2.0.0",
note = "The OpenAI direct clients are deprecated; use `AudioSession::new(&model)` instead."
)]
#[allow(deprecated)]
pub fn create_audio_client(&self) -> crate::openai::AudioClient {
let v = self.selected_variant();
crate::openai::AudioClient::new(&v.info.id, v.native.clone())
}
#[deprecated(
since = "2.0.0",
note = "The OpenAI direct clients are deprecated; use `EmbeddingsSession::new(&model)` \
instead."
)]
#[allow(deprecated)]
pub fn create_embedding_client(&self) -> crate::openai::EmbeddingClient {
let v = self.selected_variant();
crate::openai::EmbeddingClient::new(&v.info.id, v.native.clone())
}
pub fn variants(&self) -> Vec<Arc<Model>> {
match &self.inner {
ModelKind::Variant(v) => {
vec![Arc::new(Model {
inner: ModelKind::Variant(v.clone()),
})]
}
ModelKind::Group { variants, .. } => variants
.iter()
.map(|v| {
Arc::new(Model {
inner: ModelKind::Variant(v.clone()),
})
})
.collect(),
}
}
pub fn select_variant(&self, variant: &Model) -> Result<()> {
self.select_variant_by_id(variant.id())
}
pub fn select_variant_by_id(&self, id: &str) -> Result<()> {
match &self.inner {
ModelKind::Variant(v) => Err(FoundryLocalError::ModelOperation {
reason: format!(
"Selecting a variant is not supported on a single-variant model. \
Call Catalog::get_model(\"{}\") to get a model with all variants available.",
v.info.alias
),
}),
ModelKind::Group {
variants,
selected,
alias,
} => match variants.iter().position(|v| v.info.id == id) {
Some(pos) => {
selected.store(pos, Relaxed);
Ok(())
}
None => {
let available: Vec<&str> =
variants.iter().map(|v| v.info.id.as_str()).collect();
Err(FoundryLocalError::ModelOperation {
reason: format!(
"Variant '{id}' not found for model '{alias}'. Available: {available:?}",
),
})
}
},
}
}
}