diff --git a/crates/goose-sdk-types/src/custom_requests.rs b/crates/goose-sdk-types/src/custom_requests.rs index 8bd0ad4683df..7243fd1f8baf 100644 --- a/crates/goose-sdk-types/src/custom_requests.rs +++ b/crates/goose-sdk-types/src/custom_requests.rs @@ -1771,6 +1771,352 @@ pub struct ProviderInventoryEntryDto { pub model_selection_hint: Option, } +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum LocalInferenceToolCallingMode { + #[default] + Auto, + ForceNative, + ForceEmulated, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum LocalInferenceChatTemplate { + #[default] + Embedded, + Builtin { + name: String, + }, + CustomInline { + template: String, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(tag = "type", rename_all_fields = "camelCase")] +pub enum LocalInferenceSamplingConfig { + Greedy, + Temperature { + temperature: f32, + top_k: i32, + top_p: f32, + min_p: f32, + #[serde(default, skip_serializing_if = "Option::is_none")] + seed: Option, + }, + MirostatV2 { + tau: f32, + eta: f32, + #[serde(default, skip_serializing_if = "Option::is_none")] + seed: Option, + }, +} + +impl Default for LocalInferenceSamplingConfig { + fn default() -> Self { + Self::Temperature { + temperature: 0.8, + top_k: 40, + top_p: 0.95, + min_p: 0.05, + seed: None, + } + } +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct LocalInferenceModelSettingsDto { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub backend_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub context_size: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_output_tokens: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub draft_model: Option, + #[serde(default)] + pub sampling: LocalInferenceSamplingConfig, + pub repeat_penalty: f32, + pub repeat_last_n: i32, + pub frequency_penalty: f32, + pub presence_penalty: f32, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub n_batch: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub n_gpu_layers: Option, + pub use_mlock: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub flash_attention: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub n_threads: Option, + #[serde(default)] + pub tool_calling: LocalInferenceToolCallingMode, + #[serde(default)] + pub chat_template: LocalInferenceChatTemplate, + pub enable_thinking: bool, + pub vision_capable: bool, + pub image_token_estimate: usize, + pub mmproj_size_bytes: u64, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub enum LocalInferenceDownloadState { + #[default] + NotDownloaded, + Downloading, + Downloaded, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct LocalInferenceModelDownloadStatusDto { + pub state: LocalInferenceDownloadState, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub progress_percent: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub bytes_downloaded: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub total_bytes: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub speed_bps: Option, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct LocalInferenceDownloadProgressDto { + pub model_id: String, + pub status: String, + pub bytes_downloaded: u64, + pub total_bytes: u64, + pub progress_percent: f32, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub speed_bps: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub eta_seconds: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub error: Option, + pub task_exited: bool, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct LocalInferenceModelDto { + pub id: String, + pub repo_id: String, + pub filename: String, + pub quantization: String, + pub size_bytes: u64, + pub status: LocalInferenceModelDownloadStatusDto, + pub recommended: bool, + pub settings: LocalInferenceModelSettingsDto, + pub vision_capable: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub mmproj_status: Option, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct LocalInferenceHfModelVariantDto { + pub variant_id: String, + pub label: String, + pub backend_id: String, + pub format: String, + pub model_id: String, + pub download_id: String, + pub size_bytes: u64, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub filename: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub download_url: Option, + pub description: String, + pub quality_rank: u8, + pub sharded: bool, + pub supported: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub unsupported_reason: Option, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct LocalInferenceHfGgufFileDto { + pub filename: String, + pub size_bytes: u64, + pub quantization: String, + pub download_url: String, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct LocalInferenceHfModelInfoDto { + pub repo_id: String, + pub author: String, + pub model_name: String, + pub downloads: u64, + #[serde(default)] + pub gguf_files: Vec, + #[serde(default)] + pub variants: Vec, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request( + method = "_goose/unstable/local-inference/models/list", + response = LocalInferenceModelsListResponse +)] +#[serde(rename_all = "camelCase")] +pub struct LocalInferenceModelsListRequest {} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +#[serde(rename_all = "camelCase")] +pub struct LocalInferenceModelsListResponse { + pub models: Vec, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request( + method = "_goose/unstable/local-inference/models/download", + response = LocalInferenceModelDownloadResponse +)] +#[serde(rename_all = "camelCase")] +pub struct LocalInferenceModelDownloadRequest { + pub spec: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub backend_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub variant_id: Option, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +#[serde(rename_all = "camelCase")] +pub struct LocalInferenceModelDownloadResponse { + pub model_id: String, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request( + method = "_goose/unstable/local-inference/models/download/progress", + response = LocalInferenceModelDownloadProgressResponse +)] +#[serde(rename_all = "camelCase")] +pub struct LocalInferenceModelDownloadProgressRequest { + pub model_id: String, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +#[serde(rename_all = "camelCase")] +pub struct LocalInferenceModelDownloadProgressResponse { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub progress: Option, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request( + method = "_goose/unstable/local-inference/models/download/cancel", + response = EmptyResponse +)] +#[serde(rename_all = "camelCase")] +pub struct LocalInferenceModelDownloadCancelRequest { + pub model_id: String, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request( + method = "_goose/unstable/local-inference/models/delete", + response = EmptyResponse +)] +#[serde(rename_all = "camelCase")] +pub struct LocalInferenceModelDeleteRequest { + pub model_id: String, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request( + method = "_goose/unstable/local-inference/models/settings/read", + response = LocalInferenceModelSettingsReadResponse +)] +#[serde(rename_all = "camelCase")] +pub struct LocalInferenceModelSettingsReadRequest { + pub model_id: String, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +#[serde(rename_all = "camelCase")] +pub struct LocalInferenceModelSettingsReadResponse { + pub settings: LocalInferenceModelSettingsDto, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request( + method = "_goose/unstable/local-inference/models/settings/update", + response = LocalInferenceModelSettingsUpdateResponse +)] +#[serde(rename_all = "camelCase")] +pub struct LocalInferenceModelSettingsUpdateRequest { + pub model_id: String, + pub settings: LocalInferenceModelSettingsDto, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +#[serde(rename_all = "camelCase")] +pub struct LocalInferenceModelSettingsUpdateResponse { + pub settings: LocalInferenceModelSettingsDto, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request( + method = "_goose/unstable/local-inference/huggingface/search", + response = LocalInferenceHuggingFaceSearchResponse +)] +#[serde(rename_all = "camelCase")] +pub struct LocalInferenceHuggingFaceSearchRequest { + pub query: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub limit: Option, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +#[serde(rename_all = "camelCase")] +pub struct LocalInferenceHuggingFaceSearchResponse { + pub models: Vec, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request( + method = "_goose/unstable/local-inference/huggingface/repo/variants", + response = LocalInferenceHuggingFaceRepoVariantsResponse +)] +#[serde(rename_all = "camelCase")] +pub struct LocalInferenceHuggingFaceRepoVariantsRequest { + pub repo_id: String, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +#[serde(rename_all = "camelCase")] +pub struct LocalInferenceHuggingFaceRepoVariantsResponse { + pub variants: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub recommended_index: Option, + pub available_memory_bytes: u64, + pub downloaded_quants: Vec, + pub downloaded_variants: Vec, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request( + method = "_goose/unstable/local-inference/chat-templates/builtin/list", + response = LocalInferenceBuiltinChatTemplatesListResponse +)] +#[serde(rename_all = "camelCase")] +pub struct LocalInferenceBuiltinChatTemplatesListRequest {} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +#[serde(rename_all = "camelCase")] +pub struct LocalInferenceBuiltinChatTemplatesListResponse { + pub templates: Vec, +} + /// Empty success response for operations that return no data. #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] pub struct EmptyResponse {} diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index 5f0d34c76f82..ec679a7dc9ff 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -656,33 +656,8 @@ pub struct ApiDoc; super::routes::dictation::get_download_progress, super::routes::dictation::cancel_download, super::routes::dictation::delete_model, - super::routes::local_inference::list_local_models, - super::routes::local_inference::sync_featured_models, - super::routes::local_inference::search_hf_models, - super::routes::local_inference::list_builtin_chat_templates, - super::routes::local_inference::get_repo_files, - super::routes::local_inference::download_hf_model, - super::routes::local_inference::get_local_model_download_progress, - super::routes::local_inference::cancel_local_model_download, - super::routes::local_inference::delete_local_model, - super::routes::local_inference::get_model_settings, - super::routes::local_inference::update_model_settings, ), - components(schemas( - super::routes::dictation::WhisperModelResponse, - super::routes::local_inference::LocalModelResponse, - super::routes::local_inference::ModelDownloadStatus, - super::routes::local_inference::DownloadModelRequest, - goose::providers::local_inference::hf_models::HfModelInfo, - goose::providers::local_inference::hf_models::HfModelVariant, - goose::providers::local_inference::hf_models::HfGgufFile, - goose::providers::local_inference::hf_models::HfQuantVariant, - super::routes::local_inference::RepoVariantsResponse, - goose::providers::local_inference::local_model_registry::ModelSettings, - goose::providers::local_inference::local_model_registry::ChatTemplate, - goose::providers::local_inference::local_model_registry::SamplingConfig, - goose::providers::local_inference::local_model_registry::ToolCallingMode, - )) + components(schemas(super::routes::dictation::WhisperModelResponse,)) )] pub struct LocalInferenceApiDoc; diff --git a/crates/goose-server/src/routes/local_inference.rs b/crates/goose-server/src/routes/local_inference.rs deleted file mode 100644 index 39e84bb46378..000000000000 --- a/crates/goose-server/src/routes/local_inference.rs +++ /dev/null @@ -1,885 +0,0 @@ -use std::path::PathBuf; - -use crate::routes::errors::ErrorResponse; -use crate::state::AppState; -use axum::{ - extract::{Path, Query}, - http::StatusCode, - routing::{delete, get, post}, - Json, Router, -}; -use futures::future::join_all; -use goose::config::paths::Paths; -use goose::download_manager::{get_download_manager, DownloadProgress, DownloadStatus}; -use goose::providers::huggingface_auth; -use goose::providers::local_inference::hf_models::{self, HfModelInfo, HfModelVariant}; -use goose::providers::local_inference::{ - available_inference_memory_bytes, builtin_chat_template_names, - hf_models::{ - register_resolved_model, resolve_local_model_selection, resolve_local_model_spec, - resolve_model_spec, HfGgufFile, - }, - local_model_registry::{ - default_settings_for_model, featured_mmproj_spec, get_registry, model_id_from_repo, - LocalModelEntry, LocalModelStorage, ModelDownloadStatus as RegistryDownloadStatus, - ModelSettings, FEATURED_MODELS, - }, - recommend_local_model, -}; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; -use tracing::debug; -use utoipa::ToSchema; - -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -#[serde(tag = "state")] -pub enum ModelDownloadStatus { - NotDownloaded, - Downloading { - progress_percent: f32, - bytes_downloaded: u64, - total_bytes: u64, - speed_bps: Option, - }, - Downloaded, -} - -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -pub struct LocalModelResponse { - pub id: String, - pub repo_id: String, - pub filename: String, - pub quantization: String, - pub size_bytes: u64, - pub status: ModelDownloadStatus, - pub recommended: bool, - pub settings: ModelSettings, - pub vision_capable: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub mmproj_status: Option, -} - -async fn ensure_featured_models_in_registry() -> Result<(), ErrorResponse> { - let mut mmproj_downloads_needed: Vec<(String, String, PathBuf)> = Vec::new(); - - struct PendingResolve { - spec: &'static str, - repo_id: String, - quantization: String, - model_id: String, - } - let mut to_resolve = Vec::new(); - - for featured in FEATURED_MODELS { - let (repo_id, quantization) = match hf_models::parse_model_spec(featured.spec) { - Ok(parts) => parts, - Err(_) => continue, - }; - - let model_id = model_id_from_repo(&repo_id, &quantization); - - { - let registry = get_registry() - .lock() - .map_err(|_| ErrorResponse::internal("Failed to acquire registry lock"))?; - if let Some(existing) = registry.get_model(&model_id) { - let needs_backfill = existing.mmproj_path.is_none() && featured.mmproj.is_some(); - let needs_download = existing.is_downloaded() - && featured.mmproj.is_some() - && !existing.mmproj_path.as_ref().is_some_and(|p| p.exists()); - - if needs_download { - if let Some(mmproj) = featured.mmproj.as_ref() { - let path = mmproj.local_path(); - let url = format!( - "https://huggingface.co/{}/resolve/main/{}", - mmproj.repo, mmproj.filename - ); - mmproj_downloads_needed.push((model_id.clone(), url, path)); - } - } - - if !needs_backfill { - continue; - } - // Fall through to resolve for backfill - } - } - - to_resolve.push(PendingResolve { - spec: featured.spec, - repo_id, - quantization, - model_id, - }); - } - - let resolved: Vec<(PendingResolve, HfGgufFile)> = - join_all(to_resolve.into_iter().map(|pending| async move { - let hf_file = match resolve_model_spec(pending.spec).await { - Ok((_repo, file)) => file, - Err(_) => { - let filename = format!( - "{}-{}.gguf", - pending.repo_id.split('/').next_back().unwrap_or("model"), - pending.quantization - ); - HfGgufFile { - filename: filename.clone(), - size_bytes: 0, - quantization: pending.quantization.to_string(), - download_url: format!( - "https://huggingface.co/{}/resolve/main/{}", - pending.repo_id, filename - ), - } - } - }; - (pending, hf_file) - })) - .await; - - let entries_to_add: Vec = resolved - .into_iter() - .map(|(pending, hf_file)| { - let local_path = Paths::in_data_dir("models").join(&hf_file.filename); - let settings = default_settings_for_model(&pending.model_id); - LocalModelEntry { - id: pending.model_id, - repo_id: pending.repo_id, - filename: hf_file.filename, - quantization: pending.quantization, - local_path, - source_url: hf_file.download_url, - backend_id: settings.backend_id.clone(), - storage: LocalModelStorage::GooseManaged, - settings, - size_bytes: hf_file.size_bytes, - mmproj_path: None, - mmproj_source_url: None, - mmproj_size_bytes: 0, - mmproj_checked: false, - shard_files: vec![], - } - }) - .collect(); - - { - let mut registry = get_registry() - .lock() - .map_err(|_| ErrorResponse::internal("Failed to acquire registry lock"))?; - - if !entries_to_add.is_empty() { - registry.sync_with_featured(entries_to_add); - } - - // Backfill mmproj data for all registry models and collect any - // needed mmproj downloads for models already on disk. - for model in registry.list_models_mut() { - model.enrich_with_featured_mmproj(); - if model.is_downloaded() { - if let Some(mmproj) = featured_mmproj_spec(&model.id) { - let path = mmproj.local_path(); - if !path.exists() { - let url = format!( - "https://huggingface.co/{}/resolve/main/{}", - mmproj.repo, mmproj.filename - ); - mmproj_downloads_needed.push((model.id.clone(), url, path)); - } - } - } - } - let _ = registry.save(); - } - - // Auto-download mmproj files for models that are already downloaded. - // Deduplicate by path since multiple quants share one mmproj file. - let dm = get_download_manager(); - let hf_token = huggingface_auth::resolve_token_async().await.ok().flatten(); - let mut started_paths = std::collections::HashSet::new(); - for (model_id, url, path) in mmproj_downloads_needed { - if !path.exists() && started_paths.insert(path.clone()) { - let download_id = format!("{}-mmproj", model_id); - let dominated_by_active = dm - .get_progress(&download_id) - .is_some_and(|p| p.status == goose::download_manager::DownloadStatus::Downloading); - if !dominated_by_active { - tracing::info!(model_id = %model_id, "Auto-downloading vision encoder for existing model"); - if let Err(e) = dm - .download_model_with_bearer_token( - download_id, - url, - path, - hf_token.clone(), - None, - ) - .await - { - tracing::warn!(model_id = %model_id, error = %e, "Failed to start mmproj download"); - } - } - } - } - - Ok(()) -} - -#[utoipa::path( - post, - path = "/local-inference/sync-featured", - responses( - (status = 200, description = "Featured models synced to registry") - ) -)] -pub async fn sync_featured_models() -> Result { - ensure_featured_models_in_registry().await?; - Ok(StatusCode::OK) -} - -#[utoipa::path( - get, - path = "/local-inference/models", - responses( - (status = 200, description = "List of available local LLM models", body = Vec) - ) -)] -pub async fn list_local_models( - axum::extract::State(state): axum::extract::State>, -) -> Result>, ErrorResponse> { - let runtime = state.get_inference_runtime()?; - let recommended_id = recommend_local_model(&runtime); - - let registry = get_registry() - .lock() - .map_err(|_| ErrorResponse::internal("Failed to acquire registry lock"))?; - - let mut models: Vec = Vec::new(); - - for entry in registry.list_models() { - let goose_status = entry.download_status(); - - let status = match goose_status { - RegistryDownloadStatus::NotDownloaded => ModelDownloadStatus::NotDownloaded, - RegistryDownloadStatus::Downloading { - progress_percent, - bytes_downloaded, - total_bytes, - speed_bps, - } => ModelDownloadStatus::Downloading { - progress_percent, - bytes_downloaded, - total_bytes, - speed_bps: Some(speed_bps), - }, - RegistryDownloadStatus::Downloaded => ModelDownloadStatus::Downloaded, - }; - - let size_bytes = entry.file_size(); - - let vision_capable = entry.settings.vision_capable; - let mmproj_status = if vision_capable { - let ms = entry.mmproj_download_status(); - Some(match ms { - RegistryDownloadStatus::NotDownloaded => ModelDownloadStatus::NotDownloaded, - RegistryDownloadStatus::Downloading { - progress_percent, - bytes_downloaded, - total_bytes, - speed_bps, - } => ModelDownloadStatus::Downloading { - progress_percent, - bytes_downloaded, - total_bytes, - speed_bps: Some(speed_bps), - }, - RegistryDownloadStatus::Downloaded => ModelDownloadStatus::Downloaded, - }) - } else { - None - }; - - models.push(LocalModelResponse { - id: entry.id.clone(), - repo_id: entry.repo_id.clone(), - filename: entry.filename.clone(), - quantization: entry.quantization.clone(), - size_bytes, - status, - recommended: recommended_id == entry.id, - settings: entry.settings.clone(), - vision_capable, - mmproj_status, - }); - } - - models.sort_by(|a, b| { - let a_downloaded = matches!(a.status, ModelDownloadStatus::Downloaded); - let b_downloaded = matches!(b.status, ModelDownloadStatus::Downloaded); - match (b_downloaded, a_downloaded) { - (true, false) => std::cmp::Ordering::Greater, - (false, true) => std::cmp::Ordering::Less, - _ => a.id.cmp(&b.id), - } - }); - - Ok(Json(models)) -} - -#[derive(Debug, Deserialize)] -pub struct SearchQuery { - pub q: String, - pub limit: Option, -} - -#[derive(Debug, Serialize, ToSchema)] -pub struct RepoVariantsResponse { - pub variants: Vec, - pub recommended_index: Option, - pub available_memory_bytes: u64, - pub downloaded_quants: Vec, - pub downloaded_variants: Vec, -} - -#[utoipa::path( - get, - path = "/local-inference/search", - params( - ("q" = String, Query, description = "Search query"), - ("limit" = Option, Query, description = "Max results") - ), - responses( - (status = 200, description = "Search results", body = Vec), - (status = 500, description = "Search failed") - ) -)] -pub async fn search_hf_models( - Query(params): Query, -) -> Result>, ErrorResponse> { - let limit = params.limit.unwrap_or(20).min(50); - let results = hf_models::search_local_models(¶ms.q, limit) - .await - .map_err(|e| ErrorResponse::internal(format!("Search failed: {}", e)))?; - Ok(Json(results)) -} - -#[utoipa::path( - get, - path = "/local-inference/repo/{author}/{repo}/files", - responses( - (status = 200, description = "GGUF files in the repo", body = RepoVariantsResponse) - ) -)] -pub async fn get_repo_files( - axum::extract::State(state): axum::extract::State>, - Path((author, repo)): Path<(String, String)>, -) -> Result, ErrorResponse> { - let repo_id = format!("{}/{}", author, repo); - let variants = hf_models::get_repo_local_variants(&repo_id) - .await - .map_err(|e| ErrorResponse::internal(format!("Failed to fetch repo files: {}", e)))?; - - let runtime = state.get_inference_runtime()?; - let available_memory = available_inference_memory_bytes(&runtime); - let gguf_variants: Vec<_> = variants - .iter() - .filter(|variant| variant.backend_id == "llamacpp") - .map( - |variant| goose::providers::local_inference::hf_models::HfQuantVariant { - quantization: variant.variant_id.clone(), - size_bytes: variant.size_bytes, - filename: variant.filename.clone().unwrap_or_default(), - download_url: variant.download_url.clone().unwrap_or_default(), - description: "", - quality_rank: variant.quality_rank, - sharded: variant.sharded, - }, - ) - .collect(); - let recommended_index = hf_models::recommend_variant(&gguf_variants, available_memory); - - let (downloaded_quants, downloaded_variants) = { - let registry = get_registry() - .lock() - .map_err(|_| ErrorResponse::internal("Failed to acquire registry lock"))?; - let models: Vec<_> = registry - .list_models() - .iter() - .filter(|m| m.repo_id == repo_id && m.is_downloaded()) - .collect(); - ( - models.iter().map(|m| m.quantization.clone()).collect(), - models.iter().map(|m| m.id.clone()).collect(), - ) - }; - - Ok(Json(RepoVariantsResponse { - variants, - recommended_index, - available_memory_bytes: available_memory, - downloaded_quants, - downloaded_variants, - })) -} - -#[derive(Debug, Deserialize, ToSchema)] -pub struct DownloadModelRequest { - /// Model spec/download id like "bartowski/Llama-3.2-3B-Instruct-GGUF:Q4_K_M" or "google/gemma-4-31B-it" - pub spec: String, - /// Optional backend id for callers selecting a concrete variant row. - pub backend_id: Option, - /// Optional backend-specific variant id, such as a GGUF quantization or MLX dtype. - pub variant_id: Option, -} - -#[derive(Clone)] -struct LocalModelSelection { - repo_id: String, - backend_id: String, - variant_id: Option, -} - -fn explicit_model_selection( - req: &DownloadModelRequest, -) -> anyhow::Result> { - if let Some(backend_id) = req.backend_id.as_deref() { - let (repo_id, parsed_variant_id) = hf_models::parse_model_spec(&req.spec) - .map(|(repo_id, quantization)| (repo_id, Some(quantization))) - .unwrap_or_else(|_| (req.spec.clone(), None)); - let variant_id = req.variant_id.clone().or(parsed_variant_id); - match backend_id { - "mlx" | "llamacpp" => Ok(Some(LocalModelSelection { - repo_id, - backend_id: backend_id.to_string(), - variant_id, - })), - _ => anyhow::bail!("Unknown local inference backend '{}'", backend_id), - } - } else { - Ok(None) - } -} - -async fn local_model_id_from_request( - req: &DownloadModelRequest, - selection: Option<&LocalModelSelection>, -) -> anyhow::Result { - if let Some(selection) = selection { - return match selection.backend_id.as_str() { - "mlx" => Ok(selection.repo_id.clone()), - "llamacpp" => { - let quantization = selection.variant_id.as_deref().ok_or_else(|| { - anyhow::anyhow!( - "llama.cpp model '{}' is missing a quantization", - selection.repo_id - ) - })?; - Ok(model_id_from_repo(&selection.repo_id, quantization)) - } - _ => anyhow::bail!("Unknown local inference backend '{}'", selection.backend_id), - }; - } - - if let Ok((repo_id, quantization)) = hf_models::parse_model_spec(&req.spec) { - return Ok(model_id_from_repo(&repo_id, &quantization)); - } - - let variants = hf_models::get_repo_local_variants(&req.spec).await?; - let has_llamacpp = variants - .iter() - .any(|variant| variant.backend_id == "llamacpp"); - let mlx_variants: Vec<_> = variants - .iter() - .filter(|variant| variant.backend_id == "mlx") - .collect(); - if mlx_variants.len() == 1 && !has_llamacpp { - Ok(req.spec.clone()) - } else { - anyhow::bail!( - "Model spec '{}' is ambiguous; choose one of: {}", - req.spec, - variants - .iter() - .map(|variant| variant.download_id.as_str()) - .collect::>() - .join(", ") - ) - } -} - -fn mark_download_failed(model_id: &str, error: impl std::fmt::Display) { - let manager = get_download_manager(); - let download_id = format!("{}-model", model_id); - if manager.get_progress(&download_id).is_none() { - manager.set_progress(DownloadProgress { - model_id: download_id.clone(), - status: DownloadStatus::Failed, - bytes_downloaded: 0, - total_bytes: 0, - progress_percent: 0.0, - speed_bps: None, - eta_seconds: None, - error: Some(error.to_string()), - task_exited: true, - }); - return; - } - - manager.update_progress(&download_id, |progress| { - if progress.status != DownloadStatus::Cancelled { - progress.status = DownloadStatus::Failed; - progress.error = Some(error.to_string()); - } - progress.task_exited = true; - }); -} - -fn model_download_completed(model_id: &str) -> bool { - get_download_manager() - .get_progress(&format!("{}-model", model_id)) - .is_some_and(|progress| progress.status == DownloadStatus::Completed) -} - -fn register_pending_download_model( - model_id: &str, - req: &DownloadModelRequest, - selection: Option<&LocalModelSelection>, -) -> anyhow::Result<()> { - let (repo_id, backend_id, variant_id) = if let Some(selection) = selection { - ( - selection.repo_id.clone(), - selection.backend_id.clone(), - selection - .variant_id - .clone() - .unwrap_or_else(|| "default".to_string()), - ) - } else if let Ok((repo_id, quantization)) = hf_models::parse_model_spec(&req.spec) { - (repo_id, "llamacpp".to_string(), quantization) - } else { - (req.spec.clone(), "mlx".to_string(), "default".to_string()) - }; - - let mut registry = get_registry() - .lock() - .map_err(|_| anyhow::anyhow!("Failed to acquire registry lock"))?; - if registry.has_model(model_id) { - return Ok(()); - } - - let mut settings = default_settings_for_model(model_id); - if backend_id != "llamacpp" { - settings.backend_id = Some(backend_id.clone()); - } - - let filename = variant_id.clone(); - registry.add_model(LocalModelEntry { - id: model_id.to_string(), - repo_id, - filename: filename.clone(), - quantization: variant_id, - local_path: Paths::in_data_dir("models").join(filename), - source_url: req.spec.clone(), - backend_id: settings.backend_id.clone(), - storage: LocalModelStorage::HuggingFaceCache, - settings, - size_bytes: 0, - mmproj_path: None, - mmproj_source_url: None, - mmproj_size_bytes: 0, - mmproj_checked: false, - shard_files: vec![], - }) -} - -#[utoipa::path( - post, - path = "/local-inference/download", - request_body = DownloadModelRequest, - responses( - (status = 202, description = "Download started", body = String), - (status = 400, description = "Invalid request") - ) -)] -pub async fn download_hf_model( - Json(req): Json, -) -> Result<(StatusCode, Json), ErrorResponse> { - let selection = explicit_model_selection(&req) - .map_err(|e| ErrorResponse::bad_request(format!("Invalid spec: {}", e)))?; - let model_id = local_model_id_from_request(&req, selection.as_ref()) - .await - .map_err(|e| ErrorResponse::bad_request(format!("Invalid spec: {}", e)))?; - let download_id = format!("{}-model", model_id); - let download_reserved = get_download_manager() - .reserve_download(DownloadProgress { - model_id: download_id, - status: DownloadStatus::Downloading, - bytes_downloaded: 0, - total_bytes: 0, - progress_percent: 0.0, - speed_bps: None, - eta_seconds: None, - error: None, - task_exited: false, - }) - .map_err(|e| ErrorResponse::internal(format!("Download failed: {}", e)))?; - if !download_reserved { - return Ok((StatusCode::ACCEPTED, Json(model_id))); - } - - if let Err(error) = register_pending_download_model(&model_id, &req, selection.as_ref()) { - mark_download_failed(&model_id, &error); - return Err(ErrorResponse::internal(format!( - "Failed to register download: {}", - error - ))); - } - - let spec = req.spec.clone(); - let selection_for_task = selection.clone(); - let model_id_for_task = model_id.clone(); - tokio::spawn(async move { - let resolved = if let Some(selection) = selection_for_task { - resolve_local_model_selection( - &selection.repo_id, - &selection.backend_id, - selection.variant_id.as_deref(), - ) - .await - } else { - resolve_local_model_spec(&spec).await - }; - match resolved { - Ok(resolved) => { - if !model_download_completed(&model_id_for_task) { - return; - } - if let Err(error) = register_resolved_model(resolved, &spec) { - mark_download_failed(&model_id_for_task, error); - } - } - Err(error) => mark_download_failed(&model_id_for_task, error), - } - }); - - Ok((StatusCode::ACCEPTED, Json(model_id))) -} - -#[utoipa::path( - get, - path = "/local-inference/models/{model_id}/download", - responses( - (status = 200, description = "Download progress", body = DownloadProgress), - (status = 404, description = "No active download") - ) -)] -pub async fn get_local_model_download_progress( - Path(model_id): Path, -) -> Result, ErrorResponse> { - let download_id = format!("{}-model", model_id); - debug!(model_id = %model_id, download_id = %download_id, "Getting download progress"); - - let manager = get_download_manager(); - - let model_progress = manager - .get_progress(&download_id) - .ok_or_else(|| ErrorResponse::not_found("No active download"))?; - - Ok(Json(model_progress)) -} - -#[utoipa::path( - delete, - path = "/local-inference/models/{model_id}/download", - responses( - (status = 200, description = "Download cancelled"), - (status = 404, description = "No active download") - ) -)] -pub async fn cancel_local_model_download( - Path(model_id): Path, -) -> Result { - let manager = get_download_manager(); - manager - .cancel_download(&format!("{}-model", model_id)) - .map_err(|e| ErrorResponse::internal(format!("{}", e)))?; - let _ = manager.cancel_download(&format!("{}-mmproj", model_id)); - - Ok(StatusCode::OK) -} - -#[utoipa::path( - delete, - path = "/local-inference/models/{model_id}", - responses( - (status = 200, description = "Model deleted"), - (status = 404, description = "Model not found") - ) -)] -pub async fn delete_local_model(Path(model_id): Path) -> Result { - let mut registry = get_registry() - .lock() - .map_err(|_| ErrorResponse::internal("Failed to acquire registry lock"))?; - if registry.get_model(&model_id).is_none() { - return Err(ErrorResponse::not_found("Model not found")); - } - registry - .delete_model(&model_id) - .map_err(|e| ErrorResponse::internal(format!("{}", e)))?; - - Ok(StatusCode::OK) -} - -#[utoipa::path( - get, - path = "/local-inference/models/{model_id}/settings", - responses( - (status = 200, description = "Model settings", body = ModelSettings), - (status = 404, description = "Model not found") - ) -)] -pub async fn get_model_settings( - Path(model_id): Path, -) -> Result, ErrorResponse> { - let registry = get_registry() - .lock() - .map_err(|_| ErrorResponse::internal("Failed to acquire registry lock"))?; - - if let Some(settings) = registry.get_model_settings(&model_id) { - return Ok(Json(settings.clone())); - } - - Err(ErrorResponse::not_found("Model not found")) -} - -#[utoipa::path( - put, - path = "/local-inference/models/{model_id}/settings", - request_body = ModelSettings, - responses( - (status = 200, description = "Settings updated", body = ModelSettings), - (status = 404, description = "Model not found"), - (status = 500, description = "Failed to save settings") - ) -)] -pub async fn update_model_settings( - Path(model_id): Path, - Json(settings): Json, -) -> Result, ErrorResponse> { - let mut registry = get_registry() - .lock() - .map_err(|_| ErrorResponse::internal("Failed to acquire registry lock"))?; - - registry - .update_model_settings(&model_id, settings.clone()) - .map_err(|e| ErrorResponse::not_found(format!("{}", e)))?; - - Ok(Json(settings)) -} - -#[utoipa::path( - get, - path = "/local-inference/chat-templates/builtin", - responses( - (status = 200, description = "llama.cpp built-in chat template names", body = Vec) - ) -)] -pub async fn list_builtin_chat_templates() -> Json> { - Json(builtin_chat_template_names()) -} - -pub fn routes(state: Arc) -> Router { - let registered_paths: std::collections::HashSet = get_registry() - .lock() - .map(|reg| { - reg.list_models() - .iter() - .flat_map(|m| { - m.all_local_paths() - .map(|p| p.to_path_buf()) - .chain(m.mmproj_path.as_deref().map(|p| p.to_path_buf())) - }) - .collect() - }) - .unwrap_or_default(); - goose::download_manager::cleanup_partial_downloads( - &Paths::in_data_dir("models"), - ®istered_paths, - ); - - Router::new() - .route("/local-inference/models", get(list_local_models)) - .route("/local-inference/sync-featured", post(sync_featured_models)) - .route("/local-inference/search", get(search_hf_models)) - .route( - "/local-inference/chat-templates/builtin", - get(list_builtin_chat_templates), - ) - .route( - "/local-inference/repo/{author}/{repo}/files", - get(get_repo_files), - ) - .route("/local-inference/download", post(download_hf_model)) - .route( - "/local-inference/models/{model_id}/download", - get(get_local_model_download_progress), - ) - .route( - "/local-inference/models/{model_id}/download", - delete(cancel_local_model_download), - ) - .route( - "/local-inference/models/{model_id}", - delete(delete_local_model), - ) - .route( - "/local-inference/models/{model_id}/settings", - get(get_model_settings), - ) - .route( - "/local-inference/models/{model_id}/settings", - axum::routing::put(update_model_settings), - ) - .with_state(state) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn progress_for(model_id: &str, status: DownloadStatus) -> DownloadProgress { - DownloadProgress { - model_id: format!("{}-model", model_id), - status, - bytes_downloaded: 0, - total_bytes: 0, - progress_percent: 0.0, - speed_bps: None, - eta_seconds: None, - error: None, - task_exited: true, - } - } - - #[test] - fn model_download_completed_requires_completed_progress() { - let model_id = "test-completed-registration-gate"; - let manager = get_download_manager(); - manager.set_progress(progress_for(model_id, DownloadStatus::Completed)); - - assert!(model_download_completed(model_id)); - - manager.clear_completed(&format!("{}-model", model_id)); - } - - #[test] - fn model_download_completed_rejects_cancelled_progress() { - let model_id = "test-cancelled-registration-gate"; - let manager = get_download_manager(); - manager.set_progress(progress_for(model_id, DownloadStatus::Cancelled)); - - assert!(!model_download_completed(model_id)); - - manager.clear_completed(&format!("{}-model", model_id)); - } -} diff --git a/crates/goose-server/src/routes/mod.rs b/crates/goose-server/src/routes/mod.rs index 45b0da38aac0..83cb42d90d8f 100644 --- a/crates/goose-server/src/routes/mod.rs +++ b/crates/goose-server/src/routes/mod.rs @@ -3,8 +3,6 @@ pub mod agent; pub mod config_management; pub mod dictation; pub mod errors; -#[cfg(feature = "local-inference")] -pub mod local_inference; pub mod mcp_app_proxy; pub mod prompts; pub mod recipe; @@ -24,7 +22,7 @@ use axum::Router; // Function to configure all routes pub fn configure(state: Arc, secret_key: String) -> Router { - let router = Router::new() + Router::new() .merge(status::routes(state.clone())) .merge(reply::routes(state.clone())) .merge(action_required::routes(state.clone())) @@ -38,10 +36,5 @@ pub fn configure(state: Arc, secret_key: String) -> Rout .merge(mcp_app_proxy::routes(secret_key)) .merge(session_events::routes(state.clone())) .merge(sampling::routes(state.clone())) - .merge(dictation::routes(state.clone())); - - #[cfg(feature = "local-inference")] - let router = router.merge(local_inference::routes(state)); - - router + .merge(dictation::routes(state.clone())) } diff --git a/crates/goose-server/src/state.rs b/crates/goose-server/src/state.rs index afd2c4c0f279..c402935c98c4 100644 --- a/crates/goose-server/src/state.rs +++ b/crates/goose-server/src/state.rs @@ -6,15 +6,11 @@ use goose::session::SessionManager; use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use std::sync::Arc; -#[cfg(feature = "local-inference")] -use std::sync::OnceLock; use tokio::sync::Mutex; use tokio::task::JoinHandle; use crate::session_event_bus::SessionEventBus; use goose::agents::ExtensionLoadResult; -#[cfg(feature = "local-inference")] -use goose::providers::local_inference::InferenceRuntime; type ExtensionLoadingTasks = Arc>>>>>>>; @@ -25,8 +21,6 @@ pub struct AppState { pub recipe_file_hash_map: Arc>>, recipe_session_tracker: Arc>>, pub extension_loading_tasks: ExtensionLoadingTasks, - #[cfg(feature = "local-inference")] - inference_runtime: Arc>>, session_buses: Arc>>>, } @@ -40,32 +34,10 @@ impl AppState { recipe_file_hash_map: Arc::new(Mutex::new(HashMap::new())), recipe_session_tracker: Arc::new(Mutex::new(HashSet::new())), extension_loading_tasks: Arc::new(Mutex::new(HashMap::new())), - #[cfg(feature = "local-inference")] - inference_runtime: Arc::new(OnceLock::new()), session_buses: Arc::new(Mutex::new(HashMap::new())), })) } - #[cfg(feature = "local-inference")] - pub fn get_inference_runtime(&self) -> anyhow::Result> { - if let Some(runtime) = self.inference_runtime.get() { - return Ok(runtime.clone()); - } - - let runtime = InferenceRuntime::get_or_init()?; - - // Another thread may win the race to cache the runtime in AppState. - // In that case, return the already-initialized cached runtime. - match self.inference_runtime.set(runtime.clone()) { - Ok(()) => Ok(runtime), - Err(_) => Ok(self - .inference_runtime - .get() - .expect("inference runtime initialized by another thread") - .clone()), - } - } - pub async fn set_extension_loading_task( &self, session_id: String, diff --git a/crates/goose/acp-meta.json b/crates/goose/acp-meta.json index 619d7e53ecc1..11260c7dcdfe 100644 --- a/crates/goose/acp-meta.json +++ b/crates/goose/acp-meta.json @@ -504,6 +504,56 @@ "method": "_goose/unstable/dictation/models/select", "requestType": "DictationModelSelectRequest_unstable", "responseType": "EmptyResponse" + }, + { + "method": "_goose/unstable/local-inference/models/list", + "requestType": "LocalInferenceModelsListRequest_unstable", + "responseType": "LocalInferenceModelsListResponse_unstable" + }, + { + "method": "_goose/unstable/local-inference/models/download", + "requestType": "LocalInferenceModelDownloadRequest_unstable", + "responseType": "LocalInferenceModelDownloadResponse_unstable" + }, + { + "method": "_goose/unstable/local-inference/models/download/progress", + "requestType": "LocalInferenceModelDownloadProgressRequest_unstable", + "responseType": "LocalInferenceModelDownloadProgressResponse_unstable" + }, + { + "method": "_goose/unstable/local-inference/models/download/cancel", + "requestType": "LocalInferenceModelDownloadCancelRequest_unstable", + "responseType": "EmptyResponse" + }, + { + "method": "_goose/unstable/local-inference/models/delete", + "requestType": "LocalInferenceModelDeleteRequest_unstable", + "responseType": "EmptyResponse" + }, + { + "method": "_goose/unstable/local-inference/models/settings/read", + "requestType": "LocalInferenceModelSettingsReadRequest_unstable", + "responseType": "LocalInferenceModelSettingsReadResponse_unstable" + }, + { + "method": "_goose/unstable/local-inference/models/settings/update", + "requestType": "LocalInferenceModelSettingsUpdateRequest_unstable", + "responseType": "LocalInferenceModelSettingsUpdateResponse_unstable" + }, + { + "method": "_goose/unstable/local-inference/huggingface/search", + "requestType": "LocalInferenceHuggingFaceSearchRequest_unstable", + "responseType": "LocalInferenceHuggingFaceSearchResponse_unstable" + }, + { + "method": "_goose/unstable/local-inference/huggingface/repo/variants", + "requestType": "LocalInferenceHuggingFaceRepoVariantsRequest_unstable", + "responseType": "LocalInferenceHuggingFaceRepoVariantsResponse_unstable" + }, + { + "method": "_goose/unstable/local-inference/chat-templates/builtin/list", + "requestType": "LocalInferenceBuiltinChatTemplatesListRequest_unstable", + "responseType": "LocalInferenceBuiltinChatTemplatesListResponse_unstable" } ], "notifications": [ diff --git a/crates/goose/acp-schema.json b/crates/goose/acp-schema.json index 90ce11dfe221..673c6dd18705 100644 --- a/crates/goose/acp-schema.json +++ b/crates/goose/acp-schema.json @@ -5684,6 +5684,837 @@ "x-side": "agent", "x-method": "_goose/unstable/dictation/models/select" }, + "LocalInferenceModelsListRequest_unstable": { + "type": "object", + "x-side": "agent", + "x-method": "_goose/unstable/local-inference/models/list" + }, + "LocalInferenceModelsListResponse_unstable": { + "type": "object", + "properties": { + "models": { + "type": "array", + "items": { + "$ref": "#/$defs/LocalInferenceModelDto" + } + } + }, + "required": [ + "models" + ], + "x-side": "agent", + "x-method": "_goose/unstable/local-inference/models/list" + }, + "LocalInferenceModelDto": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "repoId": { + "type": "string" + }, + "filename": { + "type": "string" + }, + "quantization": { + "type": "string" + }, + "sizeBytes": { + "type": "integer", + "minimum": 0 + }, + "status": { + "$ref": "#/$defs/LocalInferenceModelDownloadStatusDto" + }, + "recommended": { + "type": "boolean" + }, + "settings": { + "$ref": "#/$defs/LocalInferenceModelSettingsDto" + }, + "visionCapable": { + "type": "boolean" + }, + "mmprojStatus": { + "anyOf": [ + { + "$ref": "#/$defs/LocalInferenceModelDownloadStatusDto" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "repoId", + "filename", + "quantization", + "sizeBytes", + "status", + "recommended", + "settings", + "visionCapable" + ] + }, + "LocalInferenceModelDownloadStatusDto": { + "type": "object", + "properties": { + "state": { + "$ref": "#/$defs/LocalInferenceDownloadState" + }, + "progressPercent": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "bytesDownloaded": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "totalBytes": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "speedBps": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + } + }, + "required": [ + "state" + ] + }, + "LocalInferenceDownloadState": { + "type": "string", + "enum": [ + "NotDownloaded", + "Downloading", + "Downloaded" + ] + }, + "LocalInferenceModelSettingsDto": { + "type": "object", + "properties": { + "backendId": { + "type": [ + "string", + "null" + ] + }, + "contextSize": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "maxOutputTokens": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "draftModel": { + "type": [ + "string", + "null" + ] + }, + "sampling": { + "$ref": "#/$defs/LocalInferenceSamplingConfig", + "default": { + "type": "Temperature", + "temperature": 0.800000011920929, + "topK": 40, + "topP": 0.949999988079071, + "minP": 0.05000000074505806 + } + }, + "repeatPenalty": { + "type": "number", + "format": "float" + }, + "repeatLastN": { + "type": "integer" + }, + "frequencyPenalty": { + "type": "number", + "format": "float" + }, + "presencePenalty": { + "type": "number", + "format": "float" + }, + "nBatch": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "nGpuLayers": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "useMlock": { + "type": "boolean" + }, + "flashAttention": { + "type": [ + "boolean", + "null" + ] + }, + "nThreads": { + "type": [ + "integer", + "null" + ] + }, + "toolCalling": { + "$ref": "#/$defs/LocalInferenceToolCallingMode", + "default": "auto" + }, + "chatTemplate": { + "$ref": "#/$defs/LocalInferenceChatTemplate", + "default": { + "type": "embedded" + } + }, + "enableThinking": { + "type": "boolean" + }, + "visionCapable": { + "type": "boolean" + }, + "imageTokenEstimate": { + "type": "integer", + "minimum": 0 + }, + "mmprojSizeBytes": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "repeatPenalty", + "repeatLastN", + "frequencyPenalty", + "presencePenalty", + "useMlock", + "enableThinking", + "visionCapable", + "imageTokenEstimate", + "mmprojSizeBytes" + ] + }, + "LocalInferenceSamplingConfig": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "Greedy" + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "temperature": { + "type": "number", + "format": "float" + }, + "topK": { + "type": "integer" + }, + "topP": { + "type": "number", + "format": "float" + }, + "minP": { + "type": "number", + "format": "float" + }, + "seed": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "type": { + "type": "string", + "const": "Temperature" + } + }, + "required": [ + "type", + "temperature", + "topK", + "topP", + "minP" + ] + }, + { + "type": "object", + "properties": { + "tau": { + "type": "number", + "format": "float" + }, + "eta": { + "type": "number", + "format": "float" + }, + "seed": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "type": { + "type": "string", + "const": "MirostatV2" + } + }, + "required": [ + "type", + "tau", + "eta" + ] + } + ] + }, + "LocalInferenceToolCallingMode": { + "type": "string", + "enum": [ + "auto", + "force_native", + "force_emulated" + ] + }, + "LocalInferenceChatTemplate": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "embedded" + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string", + "const": "builtin" + } + }, + "required": [ + "type", + "name" + ] + }, + { + "type": "object", + "properties": { + "template": { + "type": "string" + }, + "type": { + "type": "string", + "const": "custom_inline" + } + }, + "required": [ + "type", + "template" + ] + } + ] + }, + "LocalInferenceModelDownloadRequest_unstable": { + "type": "object", + "properties": { + "spec": { + "type": "string" + }, + "backendId": { + "type": [ + "string", + "null" + ] + }, + "variantId": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "spec" + ], + "x-side": "agent", + "x-method": "_goose/unstable/local-inference/models/download" + }, + "LocalInferenceModelDownloadResponse_unstable": { + "type": "object", + "properties": { + "modelId": { + "type": "string" + } + }, + "required": [ + "modelId" + ], + "x-side": "agent", + "x-method": "_goose/unstable/local-inference/models/download" + }, + "LocalInferenceModelDownloadProgressRequest_unstable": { + "type": "object", + "properties": { + "modelId": { + "type": "string" + } + }, + "required": [ + "modelId" + ], + "x-side": "agent", + "x-method": "_goose/unstable/local-inference/models/download/progress" + }, + "LocalInferenceModelDownloadProgressResponse_unstable": { + "type": "object", + "properties": { + "progress": { + "anyOf": [ + { + "$ref": "#/$defs/LocalInferenceDownloadProgressDto" + }, + { + "type": "null" + } + ] + } + }, + "x-side": "agent", + "x-method": "_goose/unstable/local-inference/models/download/progress" + }, + "LocalInferenceDownloadProgressDto": { + "type": "object", + "properties": { + "modelId": { + "type": "string" + }, + "status": { + "type": "string" + }, + "bytesDownloaded": { + "type": "integer", + "minimum": 0 + }, + "totalBytes": { + "type": "integer", + "minimum": 0 + }, + "progressPercent": { + "type": "number", + "format": "float" + }, + "speedBps": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "etaSeconds": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "error": { + "type": [ + "string", + "null" + ] + }, + "taskExited": { + "type": "boolean" + } + }, + "required": [ + "modelId", + "status", + "bytesDownloaded", + "totalBytes", + "progressPercent", + "taskExited" + ] + }, + "LocalInferenceModelDownloadCancelRequest_unstable": { + "type": "object", + "properties": { + "modelId": { + "type": "string" + } + }, + "required": [ + "modelId" + ], + "x-side": "agent", + "x-method": "_goose/unstable/local-inference/models/download/cancel" + }, + "LocalInferenceModelDeleteRequest_unstable": { + "type": "object", + "properties": { + "modelId": { + "type": "string" + } + }, + "required": [ + "modelId" + ], + "x-side": "agent", + "x-method": "_goose/unstable/local-inference/models/delete" + }, + "LocalInferenceModelSettingsReadRequest_unstable": { + "type": "object", + "properties": { + "modelId": { + "type": "string" + } + }, + "required": [ + "modelId" + ], + "x-side": "agent", + "x-method": "_goose/unstable/local-inference/models/settings/read" + }, + "LocalInferenceModelSettingsReadResponse_unstable": { + "type": "object", + "properties": { + "settings": { + "$ref": "#/$defs/LocalInferenceModelSettingsDto" + } + }, + "required": [ + "settings" + ], + "x-side": "agent", + "x-method": "_goose/unstable/local-inference/models/settings/read" + }, + "LocalInferenceModelSettingsUpdateRequest_unstable": { + "type": "object", + "properties": { + "modelId": { + "type": "string" + }, + "settings": { + "$ref": "#/$defs/LocalInferenceModelSettingsDto" + } + }, + "required": [ + "modelId", + "settings" + ], + "x-side": "agent", + "x-method": "_goose/unstable/local-inference/models/settings/update" + }, + "LocalInferenceModelSettingsUpdateResponse_unstable": { + "type": "object", + "properties": { + "settings": { + "$ref": "#/$defs/LocalInferenceModelSettingsDto" + } + }, + "required": [ + "settings" + ], + "x-side": "agent", + "x-method": "_goose/unstable/local-inference/models/settings/update" + }, + "LocalInferenceHuggingFaceSearchRequest_unstable": { + "type": "object", + "properties": { + "query": { + "type": "string" + }, + "limit": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + } + }, + "required": [ + "query" + ], + "x-side": "agent", + "x-method": "_goose/unstable/local-inference/huggingface/search" + }, + "LocalInferenceHuggingFaceSearchResponse_unstable": { + "type": "object", + "properties": { + "models": { + "type": "array", + "items": { + "$ref": "#/$defs/LocalInferenceHfModelInfoDto" + } + } + }, + "required": [ + "models" + ], + "x-side": "agent", + "x-method": "_goose/unstable/local-inference/huggingface/search" + }, + "LocalInferenceHfModelInfoDto": { + "type": "object", + "properties": { + "repoId": { + "type": "string" + }, + "author": { + "type": "string" + }, + "modelName": { + "type": "string" + }, + "downloads": { + "type": "integer", + "minimum": 0 + }, + "ggufFiles": { + "type": "array", + "items": { + "$ref": "#/$defs/LocalInferenceHfGgufFileDto" + }, + "default": [] + }, + "variants": { + "type": "array", + "items": { + "$ref": "#/$defs/LocalInferenceHfModelVariantDto" + }, + "default": [] + } + }, + "required": [ + "repoId", + "author", + "modelName", + "downloads" + ] + }, + "LocalInferenceHfGgufFileDto": { + "type": "object", + "properties": { + "filename": { + "type": "string" + }, + "sizeBytes": { + "type": "integer", + "minimum": 0 + }, + "quantization": { + "type": "string" + }, + "downloadUrl": { + "type": "string" + } + }, + "required": [ + "filename", + "sizeBytes", + "quantization", + "downloadUrl" + ] + }, + "LocalInferenceHfModelVariantDto": { + "type": "object", + "properties": { + "variantId": { + "type": "string" + }, + "label": { + "type": "string" + }, + "backendId": { + "type": "string" + }, + "format": { + "type": "string" + }, + "modelId": { + "type": "string" + }, + "downloadId": { + "type": "string" + }, + "sizeBytes": { + "type": "integer", + "minimum": 0 + }, + "filename": { + "type": [ + "string", + "null" + ] + }, + "downloadUrl": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": "string" + }, + "qualityRank": { + "type": "integer", + "maximum": 255, + "minimum": 0 + }, + "sharded": { + "type": "boolean" + }, + "supported": { + "type": "boolean" + }, + "unsupportedReason": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "variantId", + "label", + "backendId", + "format", + "modelId", + "downloadId", + "sizeBytes", + "description", + "qualityRank", + "sharded", + "supported" + ] + }, + "LocalInferenceHuggingFaceRepoVariantsRequest_unstable": { + "type": "object", + "properties": { + "repoId": { + "type": "string" + } + }, + "required": [ + "repoId" + ], + "x-side": "agent", + "x-method": "_goose/unstable/local-inference/huggingface/repo/variants" + }, + "LocalInferenceHuggingFaceRepoVariantsResponse_unstable": { + "type": "object", + "properties": { + "variants": { + "type": "array", + "items": { + "$ref": "#/$defs/LocalInferenceHfModelVariantDto" + } + }, + "recommendedIndex": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "availableMemoryBytes": { + "type": "integer", + "minimum": 0 + }, + "downloadedQuants": { + "type": "array", + "items": { + "type": "string" + } + }, + "downloadedVariants": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "variants", + "availableMemoryBytes", + "downloadedQuants", + "downloadedVariants" + ], + "x-side": "agent", + "x-method": "_goose/unstable/local-inference/huggingface/repo/variants" + }, + "LocalInferenceBuiltinChatTemplatesListRequest_unstable": { + "type": "object", + "x-side": "agent", + "x-method": "_goose/unstable/local-inference/chat-templates/builtin/list" + }, + "LocalInferenceBuiltinChatTemplatesListResponse_unstable": { + "type": "object", + "properties": { + "templates": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "templates" + ], + "x-side": "agent", + "x-method": "_goose/unstable/local-inference/chat-templates/builtin/list" + }, "GooseSessionNotification_unstable": { "type": "object", "properties": { @@ -6788,6 +7619,96 @@ ], "description": "Params for _goose/unstable/dictation/models/select", "title": "DictationModelSelectRequest_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/LocalInferenceModelsListRequest_unstable" + } + ], + "description": "Params for _goose/unstable/local-inference/models/list", + "title": "LocalInferenceModelsListRequest_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/LocalInferenceModelDownloadRequest_unstable" + } + ], + "description": "Params for _goose/unstable/local-inference/models/download", + "title": "LocalInferenceModelDownloadRequest_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/LocalInferenceModelDownloadProgressRequest_unstable" + } + ], + "description": "Params for _goose/unstable/local-inference/models/download/progress", + "title": "LocalInferenceModelDownloadProgressRequest_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/LocalInferenceModelDownloadCancelRequest_unstable" + } + ], + "description": "Params for _goose/unstable/local-inference/models/download/cancel", + "title": "LocalInferenceModelDownloadCancelRequest_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/LocalInferenceModelDeleteRequest_unstable" + } + ], + "description": "Params for _goose/unstable/local-inference/models/delete", + "title": "LocalInferenceModelDeleteRequest_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/LocalInferenceModelSettingsReadRequest_unstable" + } + ], + "description": "Params for _goose/unstable/local-inference/models/settings/read", + "title": "LocalInferenceModelSettingsReadRequest_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/LocalInferenceModelSettingsUpdateRequest_unstable" + } + ], + "description": "Params for _goose/unstable/local-inference/models/settings/update", + "title": "LocalInferenceModelSettingsUpdateRequest_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/LocalInferenceHuggingFaceSearchRequest_unstable" + } + ], + "description": "Params for _goose/unstable/local-inference/huggingface/search", + "title": "LocalInferenceHuggingFaceSearchRequest_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/LocalInferenceHuggingFaceRepoVariantsRequest_unstable" + } + ], + "description": "Params for _goose/unstable/local-inference/huggingface/repo/variants", + "title": "LocalInferenceHuggingFaceRepoVariantsRequest_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/LocalInferenceBuiltinChatTemplatesListRequest_unstable" + } + ], + "description": "Params for _goose/unstable/local-inference/chat-templates/builtin/list", + "title": "LocalInferenceBuiltinChatTemplatesListRequest_unstable" } ] }, @@ -7346,6 +8267,70 @@ } ], "title": "DictationModelDownloadProgressResponse_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/LocalInferenceModelsListResponse_unstable" + } + ], + "title": "LocalInferenceModelsListResponse_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/LocalInferenceModelDownloadResponse_unstable" + } + ], + "title": "LocalInferenceModelDownloadResponse_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/LocalInferenceModelDownloadProgressResponse_unstable" + } + ], + "title": "LocalInferenceModelDownloadProgressResponse_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/LocalInferenceModelSettingsReadResponse_unstable" + } + ], + "title": "LocalInferenceModelSettingsReadResponse_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/LocalInferenceModelSettingsUpdateResponse_unstable" + } + ], + "title": "LocalInferenceModelSettingsUpdateResponse_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/LocalInferenceHuggingFaceSearchResponse_unstable" + } + ], + "title": "LocalInferenceHuggingFaceSearchResponse_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/LocalInferenceHuggingFaceRepoVariantsResponse_unstable" + } + ], + "title": "LocalInferenceHuggingFaceRepoVariantsResponse_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/LocalInferenceBuiltinChatTemplatesListResponse_unstable" + } + ], + "title": "LocalInferenceBuiltinChatTemplatesListResponse_unstable" } ] }, diff --git a/crates/goose/src/acp/server.rs b/crates/goose/src/acp/server.rs index a9e862fee847..14c5b446bb3d 100644 --- a/crates/goose/src/acp/server.rs +++ b/crates/goose/src/acp/server.rs @@ -93,6 +93,7 @@ mod extensions; mod fork_session; mod list_sessions; mod load_session; +mod local_inference; mod manage_sessions; mod new_session; mod onboarding; diff --git a/crates/goose/src/acp/server/custom_dispatch.rs b/crates/goose/src/acp/server/custom_dispatch.rs index b15693746d1a..4cf02d1b7d58 100644 --- a/crates/goose/src/acp/server/custom_dispatch.rs +++ b/crates/goose/src/acp/server/custom_dispatch.rs @@ -823,4 +823,85 @@ impl GooseAcpAgent { ) -> Result { self.on_dictation_model_select(req).await } + + #[custom_method(LocalInferenceModelsListRequest)] + async fn dispatch_local_inference_models_list( + &self, + req: LocalInferenceModelsListRequest, + ) -> Result { + self.on_local_inference_models_list(req).await + } + + #[custom_method(LocalInferenceModelDownloadRequest)] + async fn dispatch_local_inference_model_download( + &self, + req: LocalInferenceModelDownloadRequest, + ) -> Result { + self.on_local_inference_model_download(req).await + } + + #[custom_method(LocalInferenceModelDownloadProgressRequest)] + async fn dispatch_local_inference_model_download_progress( + &self, + req: LocalInferenceModelDownloadProgressRequest, + ) -> Result { + self.on_local_inference_model_download_progress(req).await + } + + #[custom_method(LocalInferenceModelDownloadCancelRequest)] + async fn dispatch_local_inference_model_download_cancel( + &self, + req: LocalInferenceModelDownloadCancelRequest, + ) -> Result { + self.on_local_inference_model_download_cancel(req).await + } + + #[custom_method(LocalInferenceModelDeleteRequest)] + async fn dispatch_local_inference_model_delete( + &self, + req: LocalInferenceModelDeleteRequest, + ) -> Result { + self.on_local_inference_model_delete(req).await + } + + #[custom_method(LocalInferenceModelSettingsReadRequest)] + async fn dispatch_local_inference_model_settings_read( + &self, + req: LocalInferenceModelSettingsReadRequest, + ) -> Result { + self.on_local_inference_model_settings_read(req).await + } + + #[custom_method(LocalInferenceModelSettingsUpdateRequest)] + async fn dispatch_local_inference_model_settings_update( + &self, + req: LocalInferenceModelSettingsUpdateRequest, + ) -> Result { + self.on_local_inference_model_settings_update(req).await + } + + #[custom_method(LocalInferenceHuggingFaceSearchRequest)] + async fn dispatch_local_inference_huggingface_search( + &self, + req: LocalInferenceHuggingFaceSearchRequest, + ) -> Result { + self.on_local_inference_huggingface_search(req).await + } + + #[custom_method(LocalInferenceHuggingFaceRepoVariantsRequest)] + async fn dispatch_local_inference_huggingface_repo_variants( + &self, + req: LocalInferenceHuggingFaceRepoVariantsRequest, + ) -> Result { + self.on_local_inference_huggingface_repo_variants(req).await + } + + #[custom_method(LocalInferenceBuiltinChatTemplatesListRequest)] + async fn dispatch_local_inference_builtin_chat_templates_list( + &self, + req: LocalInferenceBuiltinChatTemplatesListRequest, + ) -> Result { + self.on_local_inference_builtin_chat_templates_list(req) + .await + } } diff --git a/crates/goose/src/acp/server/local_inference.rs b/crates/goose/src/acp/server/local_inference.rs new file mode 100644 index 000000000000..06cb8b5d716c --- /dev/null +++ b/crates/goose/src/acp/server/local_inference.rs @@ -0,0 +1,183 @@ +use super::*; + +#[cfg(not(feature = "local-inference"))] +fn local_inference_unavailable() -> agent_client_protocol::Error { + agent_client_protocol::Error::invalid_params().data("Local inference not enabled") +} + +impl GooseAcpAgent { + pub(super) async fn on_local_inference_models_list( + &self, + _req: LocalInferenceModelsListRequest, + ) -> Result { + #[cfg(feature = "local-inference")] + { + crate::providers::local_inference::management::list_models() + .await + .internal_err() + } + + #[cfg(not(feature = "local-inference"))] + Err(local_inference_unavailable()) + } + + pub(super) async fn on_local_inference_model_download( + &self, + req: LocalInferenceModelDownloadRequest, + ) -> Result { + #[cfg(feature = "local-inference")] + { + crate::providers::local_inference::management::download_model(req) + .await + .invalid_params_err() + } + + #[cfg(not(feature = "local-inference"))] + { + let _ = req; + Err(local_inference_unavailable()) + } + } + + pub(super) async fn on_local_inference_model_download_progress( + &self, + req: LocalInferenceModelDownloadProgressRequest, + ) -> Result { + #[cfg(feature = "local-inference")] + { + crate::providers::local_inference::management::download_progress(&req.model_id) + .map(|progress| LocalInferenceModelDownloadProgressResponse { progress }) + .internal_err() + } + + #[cfg(not(feature = "local-inference"))] + { + let _ = req; + Err(local_inference_unavailable()) + } + } + + pub(super) async fn on_local_inference_model_download_cancel( + &self, + req: LocalInferenceModelDownloadCancelRequest, + ) -> Result { + #[cfg(feature = "local-inference")] + { + crate::providers::local_inference::management::cancel_download(&req.model_id) + .internal_err()?; + Ok(EmptyResponse {}) + } + + #[cfg(not(feature = "local-inference"))] + { + let _ = req; + Err(local_inference_unavailable()) + } + } + + pub(super) async fn on_local_inference_model_delete( + &self, + req: LocalInferenceModelDeleteRequest, + ) -> Result { + #[cfg(feature = "local-inference")] + { + crate::providers::local_inference::management::delete_model(&req.model_id) + .invalid_params_err()?; + Ok(EmptyResponse {}) + } + + #[cfg(not(feature = "local-inference"))] + { + let _ = req; + Err(local_inference_unavailable()) + } + } + + pub(super) async fn on_local_inference_model_settings_read( + &self, + req: LocalInferenceModelSettingsReadRequest, + ) -> Result { + #[cfg(feature = "local-inference")] + { + crate::providers::local_inference::management::get_model_settings(&req.model_id) + .invalid_params_err() + } + + #[cfg(not(feature = "local-inference"))] + { + let _ = req; + Err(local_inference_unavailable()) + } + } + + pub(super) async fn on_local_inference_model_settings_update( + &self, + req: LocalInferenceModelSettingsUpdateRequest, + ) -> Result { + #[cfg(feature = "local-inference")] + { + crate::providers::local_inference::management::update_model_settings( + &req.model_id, + req.settings, + ) + .invalid_params_err() + } + + #[cfg(not(feature = "local-inference"))] + { + let _ = req; + Err(local_inference_unavailable()) + } + } + + pub(super) async fn on_local_inference_huggingface_search( + &self, + req: LocalInferenceHuggingFaceSearchRequest, + ) -> Result { + #[cfg(feature = "local-inference")] + { + crate::providers::local_inference::management::search_huggingface_models( + req.query, req.limit, + ) + .await + .internal_err() + } + + #[cfg(not(feature = "local-inference"))] + { + let _ = req; + Err(local_inference_unavailable()) + } + } + + pub(super) async fn on_local_inference_huggingface_repo_variants( + &self, + req: LocalInferenceHuggingFaceRepoVariantsRequest, + ) -> Result { + #[cfg(feature = "local-inference")] + { + crate::providers::local_inference::management::huggingface_repo_variants(req.repo_id) + .await + .internal_err() + } + + #[cfg(not(feature = "local-inference"))] + { + let _ = req; + Err(local_inference_unavailable()) + } + } + + pub(super) async fn on_local_inference_builtin_chat_templates_list( + &self, + _req: LocalInferenceBuiltinChatTemplatesListRequest, + ) -> Result { + #[cfg(feature = "local-inference")] + { + Ok(crate::providers::local_inference::management::list_builtin_chat_templates()) + } + + #[cfg(not(feature = "local-inference"))] + Err(local_inference_unavailable()) + } +} diff --git a/crates/goose/src/providers/local_inference.rs b/crates/goose/src/providers/local_inference.rs index 504a17a5aed1..8f84811ee418 100644 --- a/crates/goose/src/providers/local_inference.rs +++ b/crates/goose/src/providers/local_inference.rs @@ -2,6 +2,7 @@ mod backend; pub mod hf_models; mod llamacpp; pub mod local_model_registry; +pub mod management; mod mlx; pub(crate) mod multimodal; #[cfg(feature = "mlx")] @@ -67,10 +68,11 @@ pub fn builtin_chat_template_names() -> Vec { } /// Global weak reference used to share a single `InferenceRuntime` across -/// all providers and server routes. Only a `Weak` is stored — strong `Arc`s -/// live in providers and `AppState`. When all strong refs drop (normal -/// shutdown), the runtime is deallocated and the backend freed. The `Weak` -/// left behind is inert during `__cxa_finalize`, so no ggml statics race. +/// all providers and management APIs. Only a `Weak` is stored here — strong +/// `Arc`s live in providers and the local-inference management layer. When all +/// strong refs drop (normal shutdown), the runtime is deallocated and the +/// backend freed. The `Weak` left behind is inert during `__cxa_finalize`, so no +/// ggml statics race. static RUNTIME: StdMutex> = StdMutex::new(Weak::new()); impl InferenceRuntime { diff --git a/crates/goose/src/providers/local_inference/management.rs b/crates/goose/src/providers/local_inference/management.rs new file mode 100644 index 000000000000..3b602921d1ce --- /dev/null +++ b/crates/goose/src/providers/local_inference/management.rs @@ -0,0 +1,837 @@ +use super::hf_models::{ + self, register_resolved_model, resolve_local_model_selection, resolve_local_model_spec, + resolve_model_spec, HfGgufFile, HfModelInfo, HfModelVariant, +}; +use super::local_model_registry::{ + default_settings_for_model, featured_mmproj_spec, get_registry, model_id_from_repo, + ChatTemplate, LocalModelEntry, LocalModelStorage, ModelDownloadStatus, ModelSettings, + SamplingConfig, ToolCallingMode, FEATURED_MODELS, +}; +use super::{ + available_inference_memory_bytes, builtin_chat_template_names, recommend_local_model, + InferenceRuntime, +}; +use crate::config::paths::Paths; +use crate::download_manager::{get_download_manager, DownloadProgress, DownloadStatus}; +use crate::providers::huggingface_auth; +use anyhow::{anyhow, Result}; +use futures::future::join_all; +use goose_sdk_types::custom_requests::{ + LocalInferenceBuiltinChatTemplatesListResponse, LocalInferenceChatTemplate, + LocalInferenceDownloadProgressDto, LocalInferenceDownloadState, LocalInferenceHfGgufFileDto, + LocalInferenceHfModelInfoDto, LocalInferenceHfModelVariantDto, + LocalInferenceHuggingFaceRepoVariantsResponse, LocalInferenceHuggingFaceSearchResponse, + LocalInferenceModelDownloadRequest, LocalInferenceModelDownloadResponse, + LocalInferenceModelDownloadStatusDto, LocalInferenceModelDto, LocalInferenceModelSettingsDto, + LocalInferenceModelSettingsReadResponse, LocalInferenceModelSettingsUpdateResponse, + LocalInferenceModelsListResponse, LocalInferenceSamplingConfig, LocalInferenceToolCallingMode, +}; +use std::path::PathBuf; +use std::sync::{Arc, OnceLock}; + +static MANAGEMENT_RUNTIME: OnceLock> = OnceLock::new(); + +#[derive(Clone)] +struct LocalModelSelection { + repo_id: String, + backend_id: String, + variant_id: Option, +} + +pub async fn list_models() -> Result { + ensure_featured_models_current().await?; + + let runtime = management_runtime()?; + let recommended_id = recommend_local_model(&runtime); + + let registry = get_registry() + .lock() + .map_err(|_| anyhow!("Failed to acquire registry lock"))?; + let mut models: Vec = registry + .list_models() + .iter() + .map(|entry| local_model_to_dto(entry, &recommended_id)) + .collect(); + + models.sort_by(|a, b| { + let a_downloaded = a.status.state == LocalInferenceDownloadState::Downloaded; + let b_downloaded = b.status.state == LocalInferenceDownloadState::Downloaded; + match (b_downloaded, a_downloaded) { + (true, false) => std::cmp::Ordering::Greater, + (false, true) => std::cmp::Ordering::Less, + _ => a.id.cmp(&b.id), + } + }); + + Ok(LocalInferenceModelsListResponse { models }) +} + +pub async fn search_huggingface_models( + query: String, + limit: Option, +) -> Result { + let limit = limit.unwrap_or(20).min(50); + let models = hf_models::search_local_models(&query, limit) + .await? + .into_iter() + .map(hf_model_info_to_dto) + .collect(); + Ok(LocalInferenceHuggingFaceSearchResponse { models }) +} + +pub async fn huggingface_repo_variants( + repo_id: String, +) -> Result { + let variants = hf_models::get_repo_local_variants(&repo_id).await?; + + let runtime = management_runtime()?; + let available_memory = available_inference_memory_bytes(&runtime); + let gguf_variants: Vec<_> = variants + .iter() + .filter(|variant| variant.backend_id == "llamacpp") + .map(|variant| hf_models::HfQuantVariant { + quantization: variant.variant_id.clone(), + size_bytes: variant.size_bytes, + filename: variant.filename.clone().unwrap_or_default(), + download_url: variant.download_url.clone().unwrap_or_default(), + description: "", + quality_rank: variant.quality_rank, + sharded: variant.sharded, + }) + .collect(); + let recommended_index = hf_models::recommend_variant(&gguf_variants, available_memory); + + let (downloaded_quants, downloaded_variants) = { + let registry = get_registry() + .lock() + .map_err(|_| anyhow!("Failed to acquire registry lock"))?; + let models: Vec<_> = registry + .list_models() + .iter() + .filter(|m| m.repo_id == repo_id && m.is_downloaded()) + .collect(); + ( + models.iter().map(|m| m.quantization.clone()).collect(), + models.iter().map(|m| m.id.clone()).collect(), + ) + }; + + Ok(LocalInferenceHuggingFaceRepoVariantsResponse { + variants: variants.into_iter().map(hf_model_variant_to_dto).collect(), + recommended_index, + available_memory_bytes: available_memory, + downloaded_quants, + downloaded_variants, + }) +} + +pub async fn download_model( + req: LocalInferenceModelDownloadRequest, +) -> Result { + let selection = explicit_model_selection(&req)?; + let model_id = local_model_id_from_request(&req, selection.as_ref()).await?; + let download_id = format!("{}-model", model_id); + let download_reserved = get_download_manager().reserve_download(DownloadProgress { + model_id: download_id, + status: DownloadStatus::Downloading, + bytes_downloaded: 0, + total_bytes: 0, + progress_percent: 0.0, + speed_bps: None, + eta_seconds: None, + error: None, + task_exited: false, + })?; + if !download_reserved { + return Ok(LocalInferenceModelDownloadResponse { model_id }); + } + + if let Err(error) = register_pending_download_model(&model_id, &req, selection.as_ref()) { + mark_download_failed(&model_id, &error); + return Err(error.context("Failed to register download")); + } + + let spec = req.spec.clone(); + let selection_for_task = selection.clone(); + let model_id_for_task = model_id.clone(); + tokio::spawn(async move { + let resolved = if let Some(selection) = selection_for_task { + resolve_local_model_selection( + &selection.repo_id, + &selection.backend_id, + selection.variant_id.as_deref(), + ) + .await + } else { + resolve_local_model_spec(&spec).await + }; + match resolved { + Ok(resolved) => { + if !model_download_completed(&model_id_for_task) { + return; + } + if let Err(error) = register_resolved_model(resolved, &spec) { + mark_download_failed(&model_id_for_task, error); + } + } + Err(error) => mark_download_failed(&model_id_for_task, error), + } + }); + + Ok(LocalInferenceModelDownloadResponse { model_id }) +} + +pub fn download_progress(model_id: &str) -> Result> { + Ok(get_download_manager() + .get_progress(&format!("{}-model", model_id)) + .map(download_progress_to_dto)) +} + +pub fn cancel_download(model_id: &str) -> Result<()> { + let manager = get_download_manager(); + manager.cancel_download(&format!("{}-model", model_id))?; + let _ = manager.cancel_download(&format!("{}-mmproj", model_id)); + Ok(()) +} + +pub fn delete_model(model_id: &str) -> Result<()> { + let mut registry = get_registry() + .lock() + .map_err(|_| anyhow!("Failed to acquire registry lock"))?; + if registry.get_model(model_id).is_none() { + anyhow::bail!("Model not found"); + } + registry.delete_model(model_id) +} + +pub fn get_model_settings(model_id: &str) -> Result { + let registry = get_registry() + .lock() + .map_err(|_| anyhow!("Failed to acquire registry lock"))?; + let settings = registry + .get_model_settings(model_id) + .ok_or_else(|| anyhow!("Model not found"))?; + Ok(LocalInferenceModelSettingsReadResponse { + settings: model_settings_to_dto(settings), + }) +} + +pub fn update_model_settings( + model_id: &str, + settings: LocalInferenceModelSettingsDto, +) -> Result { + let settings = model_settings_from_dto(settings); + let mut registry = get_registry() + .lock() + .map_err(|_| anyhow!("Failed to acquire registry lock"))?; + registry.update_model_settings(model_id, settings.clone())?; + Ok(LocalInferenceModelSettingsUpdateResponse { + settings: model_settings_to_dto(&settings), + }) +} + +pub fn list_builtin_chat_templates() -> LocalInferenceBuiltinChatTemplatesListResponse { + LocalInferenceBuiltinChatTemplatesListResponse { + templates: builtin_chat_template_names(), + } +} + +fn management_runtime() -> Result> { + if let Some(runtime) = MANAGEMENT_RUNTIME.get() { + return Ok(runtime.clone()); + } + + let runtime = InferenceRuntime::get_or_init()?; + match MANAGEMENT_RUNTIME.set(runtime.clone()) { + Ok(()) => Ok(runtime), + Err(_) => Ok(MANAGEMENT_RUNTIME + .get() + .expect("local inference management runtime initialized by another thread") + .clone()), + } +} + +pub async fn ensure_featured_models_current() -> Result<()> { + let mut mmproj_downloads_needed: Vec<(String, String, PathBuf)> = Vec::new(); + + struct PendingResolve { + spec: &'static str, + repo_id: String, + quantization: String, + model_id: String, + } + let mut to_resolve = Vec::new(); + + for featured in FEATURED_MODELS { + let (repo_id, quantization) = match hf_models::parse_model_spec(featured.spec) { + Ok(parts) => parts, + Err(_) => continue, + }; + + let model_id = model_id_from_repo(&repo_id, &quantization); + + { + let registry = get_registry() + .lock() + .map_err(|_| anyhow!("Failed to acquire registry lock"))?; + if let Some(existing) = registry.get_model(&model_id) { + let needs_backfill = existing.mmproj_path.is_none() && featured.mmproj.is_some(); + let needs_download = existing.is_downloaded() + && featured.mmproj.is_some() + && !existing.mmproj_path.as_ref().is_some_and(|p| p.exists()); + + if needs_download { + if let Some(mmproj) = featured.mmproj.as_ref() { + let path = mmproj.local_path(); + let url = format!( + "https://huggingface.co/{}/resolve/main/{}", + mmproj.repo, mmproj.filename + ); + mmproj_downloads_needed.push((model_id.clone(), url, path)); + } + } + + if !needs_backfill { + continue; + } + } + } + + to_resolve.push(PendingResolve { + spec: featured.spec, + repo_id, + quantization, + model_id, + }); + } + + let resolved: Vec<(PendingResolve, HfGgufFile)> = + join_all(to_resolve.into_iter().map(|pending| async move { + let hf_file = match resolve_model_spec(pending.spec).await { + Ok((_repo, file)) => file, + Err(_) => { + let filename = format!( + "{}-{}.gguf", + pending.repo_id.split('/').next_back().unwrap_or("model"), + pending.quantization + ); + HfGgufFile { + filename: filename.clone(), + size_bytes: 0, + quantization: pending.quantization.to_string(), + download_url: format!( + "https://huggingface.co/{}/resolve/main/{}", + pending.repo_id, filename + ), + } + } + }; + (pending, hf_file) + })) + .await; + + let entries_to_add: Vec = resolved + .into_iter() + .map(|(pending, hf_file)| { + let local_path = Paths::in_data_dir("models").join(&hf_file.filename); + let settings = default_settings_for_model(&pending.model_id); + LocalModelEntry { + id: pending.model_id, + repo_id: pending.repo_id, + filename: hf_file.filename, + quantization: pending.quantization, + local_path, + source_url: hf_file.download_url, + backend_id: settings.backend_id.clone(), + storage: LocalModelStorage::GooseManaged, + settings, + size_bytes: hf_file.size_bytes, + mmproj_path: None, + mmproj_source_url: None, + mmproj_size_bytes: 0, + mmproj_checked: false, + shard_files: vec![], + } + }) + .collect(); + + { + let mut registry = get_registry() + .lock() + .map_err(|_| anyhow!("Failed to acquire registry lock"))?; + + if !entries_to_add.is_empty() { + registry.sync_with_featured(entries_to_add); + } + + for model in registry.list_models_mut() { + model.enrich_with_featured_mmproj(); + if model.is_downloaded() { + if let Some(mmproj) = featured_mmproj_spec(&model.id) { + let path = mmproj.local_path(); + if !path.exists() { + let url = format!( + "https://huggingface.co/{}/resolve/main/{}", + mmproj.repo, mmproj.filename + ); + mmproj_downloads_needed.push((model.id.clone(), url, path)); + } + } + } + } + let _ = registry.save(); + } + + let dm = get_download_manager(); + let hf_token = huggingface_auth::resolve_token_async().await.ok().flatten(); + let mut started_paths = std::collections::HashSet::new(); + for (model_id, url, path) in mmproj_downloads_needed { + if !path.exists() && started_paths.insert(path.clone()) { + let download_id = format!("{}-mmproj", model_id); + let dominated_by_active = dm + .get_progress(&download_id) + .is_some_and(|p| p.status == DownloadStatus::Downloading); + if !dominated_by_active { + tracing::info!(model_id = %model_id, "Auto-downloading vision encoder for existing model"); + if let Err(e) = dm + .download_model_with_bearer_token( + download_id, + url, + path, + hf_token.clone(), + None, + ) + .await + { + tracing::warn!(model_id = %model_id, error = %e, "Failed to start mmproj download"); + } + } + } + } + + Ok(()) +} + +fn local_model_to_dto(entry: &LocalModelEntry, recommended_id: &str) -> LocalInferenceModelDto { + let vision_capable = entry.settings.vision_capable; + LocalInferenceModelDto { + id: entry.id.clone(), + repo_id: entry.repo_id.clone(), + filename: entry.filename.clone(), + quantization: entry.quantization.clone(), + size_bytes: entry.file_size(), + status: model_download_status_to_dto(entry.download_status()), + recommended: recommended_id == entry.id, + settings: model_settings_to_dto(&entry.settings), + vision_capable, + mmproj_status: vision_capable + .then(|| model_download_status_to_dto(entry.mmproj_download_status())), + } +} + +fn model_download_status_to_dto( + status: ModelDownloadStatus, +) -> LocalInferenceModelDownloadStatusDto { + match status { + ModelDownloadStatus::NotDownloaded => LocalInferenceModelDownloadStatusDto { + state: LocalInferenceDownloadState::NotDownloaded, + ..Default::default() + }, + ModelDownloadStatus::Downloading { + progress_percent, + bytes_downloaded, + total_bytes, + speed_bps, + } => LocalInferenceModelDownloadStatusDto { + state: LocalInferenceDownloadState::Downloading, + progress_percent: Some(progress_percent), + bytes_downloaded: Some(bytes_downloaded), + total_bytes: Some(total_bytes), + speed_bps: Some(speed_bps), + }, + ModelDownloadStatus::Downloaded => LocalInferenceModelDownloadStatusDto { + state: LocalInferenceDownloadState::Downloaded, + ..Default::default() + }, + } +} + +fn download_progress_to_dto(progress: DownloadProgress) -> LocalInferenceDownloadProgressDto { + LocalInferenceDownloadProgressDto { + model_id: progress.model_id, + status: serde_json::to_value(progress.status) + .ok() + .and_then(|value| value.as_str().map(ToOwned::to_owned)) + .unwrap_or_else(|| "unknown".to_string()), + bytes_downloaded: progress.bytes_downloaded, + total_bytes: progress.total_bytes, + progress_percent: progress.progress_percent, + speed_bps: progress.speed_bps, + eta_seconds: progress.eta_seconds, + error: progress.error, + task_exited: progress.task_exited, + } +} + +fn hf_model_info_to_dto(model: HfModelInfo) -> LocalInferenceHfModelInfoDto { + LocalInferenceHfModelInfoDto { + repo_id: model.repo_id, + author: model.author, + model_name: model.model_name, + downloads: model.downloads, + gguf_files: model + .gguf_files + .into_iter() + .map(|file| LocalInferenceHfGgufFileDto { + filename: file.filename, + size_bytes: file.size_bytes, + quantization: file.quantization, + download_url: file.download_url, + }) + .collect(), + variants: model + .variants + .into_iter() + .map(hf_model_variant_to_dto) + .collect(), + } +} + +fn hf_model_variant_to_dto(variant: HfModelVariant) -> LocalInferenceHfModelVariantDto { + LocalInferenceHfModelVariantDto { + variant_id: variant.variant_id, + label: variant.label, + backend_id: variant.backend_id, + format: variant.format, + model_id: variant.model_id, + download_id: variant.download_id, + size_bytes: variant.size_bytes, + filename: variant.filename, + download_url: variant.download_url, + description: variant.description, + quality_rank: variant.quality_rank, + sharded: variant.sharded, + supported: variant.supported, + unsupported_reason: variant.unsupported_reason, + } +} + +pub fn model_settings_to_dto(settings: &ModelSettings) -> LocalInferenceModelSettingsDto { + LocalInferenceModelSettingsDto { + backend_id: settings.backend_id.clone(), + context_size: settings.context_size, + max_output_tokens: settings.max_output_tokens, + draft_model: settings.draft_model.clone(), + sampling: sampling_to_dto(&settings.sampling), + repeat_penalty: settings.repeat_penalty, + repeat_last_n: settings.repeat_last_n, + frequency_penalty: settings.frequency_penalty, + presence_penalty: settings.presence_penalty, + n_batch: settings.n_batch, + n_gpu_layers: settings.n_gpu_layers, + use_mlock: settings.use_mlock, + flash_attention: settings.flash_attention, + n_threads: settings.n_threads, + tool_calling: tool_calling_to_dto(settings.tool_calling), + chat_template: chat_template_to_dto(&settings.chat_template), + enable_thinking: settings.enable_thinking, + vision_capable: settings.vision_capable, + image_token_estimate: settings.image_token_estimate, + mmproj_size_bytes: settings.mmproj_size_bytes, + } +} + +pub fn model_settings_from_dto(settings: LocalInferenceModelSettingsDto) -> ModelSettings { + ModelSettings { + backend_id: settings.backend_id, + context_size: settings.context_size, + max_output_tokens: settings.max_output_tokens, + draft_model: settings.draft_model, + sampling: sampling_from_dto(settings.sampling), + repeat_penalty: settings.repeat_penalty, + repeat_last_n: settings.repeat_last_n, + frequency_penalty: settings.frequency_penalty, + presence_penalty: settings.presence_penalty, + n_batch: settings.n_batch, + n_gpu_layers: settings.n_gpu_layers, + use_mlock: settings.use_mlock, + flash_attention: settings.flash_attention, + n_threads: settings.n_threads, + tool_calling: tool_calling_from_dto(settings.tool_calling), + chat_template: chat_template_from_dto(settings.chat_template), + enable_thinking: settings.enable_thinking, + vision_capable: settings.vision_capable, + image_token_estimate: settings.image_token_estimate, + mmproj_size_bytes: settings.mmproj_size_bytes, + } +} + +fn sampling_to_dto(sampling: &SamplingConfig) -> LocalInferenceSamplingConfig { + match sampling { + SamplingConfig::Greedy => LocalInferenceSamplingConfig::Greedy, + SamplingConfig::Temperature { + temperature, + top_k, + top_p, + min_p, + seed, + } => LocalInferenceSamplingConfig::Temperature { + temperature: *temperature, + top_k: *top_k, + top_p: *top_p, + min_p: *min_p, + seed: *seed, + }, + SamplingConfig::MirostatV2 { tau, eta, seed } => LocalInferenceSamplingConfig::MirostatV2 { + tau: *tau, + eta: *eta, + seed: *seed, + }, + } +} + +fn sampling_from_dto(sampling: LocalInferenceSamplingConfig) -> SamplingConfig { + match sampling { + LocalInferenceSamplingConfig::Greedy => SamplingConfig::Greedy, + LocalInferenceSamplingConfig::Temperature { + temperature, + top_k, + top_p, + min_p, + seed, + } => SamplingConfig::Temperature { + temperature, + top_k, + top_p, + min_p, + seed, + }, + LocalInferenceSamplingConfig::MirostatV2 { tau, eta, seed } => { + SamplingConfig::MirostatV2 { tau, eta, seed } + } + } +} + +fn tool_calling_to_dto(mode: ToolCallingMode) -> LocalInferenceToolCallingMode { + match mode { + ToolCallingMode::Auto => LocalInferenceToolCallingMode::Auto, + ToolCallingMode::ForceNative => LocalInferenceToolCallingMode::ForceNative, + ToolCallingMode::ForceEmulated => LocalInferenceToolCallingMode::ForceEmulated, + } +} + +fn tool_calling_from_dto(mode: LocalInferenceToolCallingMode) -> ToolCallingMode { + match mode { + LocalInferenceToolCallingMode::Auto => ToolCallingMode::Auto, + LocalInferenceToolCallingMode::ForceNative => ToolCallingMode::ForceNative, + LocalInferenceToolCallingMode::ForceEmulated => ToolCallingMode::ForceEmulated, + } +} + +fn chat_template_to_dto(template: &ChatTemplate) -> LocalInferenceChatTemplate { + match template { + ChatTemplate::Embedded => LocalInferenceChatTemplate::Embedded, + ChatTemplate::Builtin { name } => { + LocalInferenceChatTemplate::Builtin { name: name.clone() } + } + ChatTemplate::CustomInline { template } => LocalInferenceChatTemplate::CustomInline { + template: template.clone(), + }, + } +} + +fn chat_template_from_dto(template: LocalInferenceChatTemplate) -> ChatTemplate { + match template { + LocalInferenceChatTemplate::Embedded => ChatTemplate::Embedded, + LocalInferenceChatTemplate::Builtin { name } => ChatTemplate::Builtin { name }, + LocalInferenceChatTemplate::CustomInline { template } => { + ChatTemplate::CustomInline { template } + } + } +} + +fn explicit_model_selection( + req: &LocalInferenceModelDownloadRequest, +) -> Result> { + if let Some(backend_id) = req.backend_id.as_deref() { + let (repo_id, parsed_variant_id) = hf_models::parse_model_spec(&req.spec) + .map(|(repo_id, quantization)| (repo_id, Some(quantization))) + .unwrap_or_else(|_| (req.spec.clone(), None)); + let variant_id = req.variant_id.clone().or(parsed_variant_id); + match backend_id { + "mlx" | "llamacpp" => Ok(Some(LocalModelSelection { + repo_id, + backend_id: backend_id.to_string(), + variant_id, + })), + _ => anyhow::bail!("Unknown local inference backend '{}'", backend_id), + } + } else { + Ok(None) + } +} + +async fn local_model_id_from_request( + req: &LocalInferenceModelDownloadRequest, + selection: Option<&LocalModelSelection>, +) -> Result { + if let Some(selection) = selection { + return match selection.backend_id.as_str() { + "mlx" => Ok(selection.repo_id.clone()), + "llamacpp" => { + let quantization = selection.variant_id.as_deref().ok_or_else(|| { + anyhow!( + "llama.cpp model '{}' is missing a quantization", + selection.repo_id + ) + })?; + Ok(model_id_from_repo(&selection.repo_id, quantization)) + } + _ => anyhow::bail!("Unknown local inference backend '{}'", selection.backend_id), + }; + } + + if let Ok((repo_id, quantization)) = hf_models::parse_model_spec(&req.spec) { + return Ok(model_id_from_repo(&repo_id, &quantization)); + } + + let variants = hf_models::get_repo_local_variants(&req.spec).await?; + let has_llamacpp = variants + .iter() + .any(|variant| variant.backend_id == "llamacpp"); + let mlx_variants: Vec<_> = variants + .iter() + .filter(|variant| variant.backend_id == "mlx") + .collect(); + if mlx_variants.len() == 1 && !has_llamacpp { + Ok(req.spec.clone()) + } else { + anyhow::bail!( + "Model spec '{}' is ambiguous; choose one of: {}", + req.spec, + variants + .iter() + .map(|variant| variant.download_id.as_str()) + .collect::>() + .join(", ") + ) + } +} + +fn mark_download_failed(model_id: &str, error: impl std::fmt::Display) { + let manager = get_download_manager(); + let download_id = format!("{}-model", model_id); + if manager.get_progress(&download_id).is_none() { + manager.set_progress(DownloadProgress { + model_id: download_id.clone(), + status: DownloadStatus::Failed, + bytes_downloaded: 0, + total_bytes: 0, + progress_percent: 0.0, + speed_bps: None, + eta_seconds: None, + error: Some(error.to_string()), + task_exited: true, + }); + return; + } + + manager.update_progress(&download_id, |progress| { + if progress.status != DownloadStatus::Cancelled { + progress.status = DownloadStatus::Failed; + progress.error = Some(error.to_string()); + } + progress.task_exited = true; + }); +} + +fn model_download_completed(model_id: &str) -> bool { + get_download_manager() + .get_progress(&format!("{}-model", model_id)) + .is_some_and(|progress| progress.status == DownloadStatus::Completed) +} + +fn register_pending_download_model( + model_id: &str, + req: &LocalInferenceModelDownloadRequest, + selection: Option<&LocalModelSelection>, +) -> Result<()> { + let (repo_id, backend_id, variant_id) = if let Some(selection) = selection { + ( + selection.repo_id.clone(), + selection.backend_id.clone(), + selection + .variant_id + .clone() + .unwrap_or_else(|| "default".to_string()), + ) + } else if let Ok((repo_id, quantization)) = hf_models::parse_model_spec(&req.spec) { + (repo_id, "llamacpp".to_string(), quantization) + } else { + (req.spec.clone(), "mlx".to_string(), "default".to_string()) + }; + + let mut registry = get_registry() + .lock() + .map_err(|_| anyhow!("Failed to acquire registry lock"))?; + if registry.has_model(model_id) { + return Ok(()); + } + + let mut settings = default_settings_for_model(model_id); + if backend_id != "llamacpp" { + settings.backend_id = Some(backend_id.clone()); + } + + let filename = variant_id.clone(); + registry.add_model(LocalModelEntry { + id: model_id.to_string(), + repo_id, + filename: filename.clone(), + quantization: variant_id, + local_path: Paths::in_data_dir("models").join(filename), + source_url: req.spec.clone(), + backend_id: settings.backend_id.clone(), + storage: LocalModelStorage::HuggingFaceCache, + settings, + size_bytes: 0, + mmproj_path: None, + mmproj_source_url: None, + mmproj_size_bytes: 0, + mmproj_checked: false, + shard_files: vec![], + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn settings_round_trip_preserves_defaults() { + let settings = ModelSettings::default(); + let dto = model_settings_to_dto(&settings); + let round_trip = model_settings_from_dto(dto); + assert_eq!(round_trip.repeat_penalty, settings.repeat_penalty); + assert_eq!(round_trip.repeat_last_n, settings.repeat_last_n); + assert_eq!(round_trip.enable_thinking, settings.enable_thinking); + assert_eq!( + round_trip.image_token_estimate, + settings.image_token_estimate + ); + } + + #[tokio::test] + async fn explicit_llamacpp_selection_derives_quantized_model_id() { + let req = LocalInferenceModelDownloadRequest { + spec: "test/repo".to_string(), + backend_id: Some("llamacpp".to_string()), + variant_id: Some("Q4_K_M".to_string()), + }; + let selection = explicit_model_selection(&req).unwrap(); + let model_id = local_model_id_from_request(&req, selection.as_ref()) + .await + .unwrap(); + assert_eq!(model_id, "test/repo:Q4_K_M"); + } +} diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index bb6e4bcebe57..e8ce9725a3fc 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -1643,344 +1643,6 @@ } } }, - "/local-inference/chat-templates/builtin": { - "get": { - "tags": [ - "super::routes::local_inference" - ], - "operationId": "list_builtin_chat_templates", - "responses": { - "200": { - "description": "llama.cpp built-in chat template names", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - } - } - }, - "/local-inference/download": { - "post": { - "tags": [ - "super::routes::local_inference" - ], - "operationId": "download_hf_model", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DownloadModelRequest" - } - } - }, - "required": true - }, - "responses": { - "202": { - "description": "Download started", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } - }, - "400": { - "description": "Invalid request" - } - } - } - }, - "/local-inference/models": { - "get": { - "tags": [ - "super::routes::local_inference" - ], - "operationId": "list_local_models", - "responses": { - "200": { - "description": "List of available local LLM models", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LocalModelResponse" - } - } - } - } - } - } - } - }, - "/local-inference/models/{model_id}": { - "delete": { - "tags": [ - "super::routes::local_inference" - ], - "operationId": "delete_local_model", - "parameters": [ - { - "name": "model_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Model deleted" - }, - "404": { - "description": "Model not found" - } - } - } - }, - "/local-inference/models/{model_id}/download": { - "get": { - "tags": [ - "super::routes::local_inference" - ], - "operationId": "get_local_model_download_progress", - "parameters": [ - { - "name": "model_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Download progress", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DownloadProgress" - } - } - } - }, - "404": { - "description": "No active download" - } - } - }, - "delete": { - "tags": [ - "super::routes::local_inference" - ], - "operationId": "cancel_local_model_download", - "parameters": [ - { - "name": "model_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Download cancelled" - }, - "404": { - "description": "No active download" - } - } - } - }, - "/local-inference/models/{model_id}/settings": { - "get": { - "tags": [ - "super::routes::local_inference" - ], - "operationId": "get_model_settings", - "parameters": [ - { - "name": "model_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Model settings", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ModelSettings" - } - } - } - }, - "404": { - "description": "Model not found" - } - } - }, - "put": { - "tags": [ - "super::routes::local_inference" - ], - "operationId": "update_model_settings", - "parameters": [ - { - "name": "model_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ModelSettings" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Settings updated", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ModelSettings" - } - } - } - }, - "404": { - "description": "Model not found" - }, - "500": { - "description": "Failed to save settings" - } - } - } - }, - "/local-inference/repo/{author}/{repo}/files": { - "get": { - "tags": [ - "super::routes::local_inference" - ], - "operationId": "get_repo_files", - "parameters": [ - { - "name": "author", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "repo", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "GGUF files in the repo", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RepoVariantsResponse" - } - } - } - } - } - } - }, - "/local-inference/search": { - "get": { - "tags": [ - "super::routes::local_inference" - ], - "operationId": "search_hf_models", - "parameters": [ - { - "name": "q", - "in": "query", - "description": "Search query", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "description": "Max results", - "required": false, - "schema": { - "type": "integer", - "nullable": true, - "minimum": 0 - } - } - ], - "responses": { - "200": { - "description": "Search results", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/HfModelInfo" - } - } - } - } - }, - "500": { - "description": "Search failed" - } - } - } - }, - "/local-inference/sync-featured": { - "post": { - "tags": [ - "super::routes::local_inference" - ], - "operationId": "sync_featured_models", - "responses": { - "200": { - "description": "Featured models synced to registry" - } - } - } - }, "/recipes/decode": { "post": { "tags": [ @@ -3365,63 +3027,6 @@ } } }, - "ChatTemplate": { - "oneOf": [ - { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "embedded" - ] - } - } - }, - { - "type": "object", - "required": [ - "name", - "type" - ], - "properties": { - "name": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "builtin" - ] - } - } - }, - { - "type": "object", - "required": [ - "template", - "type" - ], - "properties": { - "template": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "custom_inline" - ] - } - } - } - ], - "discriminator": { - "propertyName": "type" - } - }, "CheckProviderRequest": { "type": "object", "required": [ @@ -4194,28 +3799,6 @@ } } }, - "DownloadModelRequest": { - "type": "object", - "required": [ - "spec" - ], - "properties": { - "backend_id": { - "type": "string", - "description": "Optional backend id for callers selecting a concrete variant row.", - "nullable": true - }, - "spec": { - "type": "string", - "description": "Model spec/download id like \"bartowski/Llama-3.2-3B-Instruct-GGUF:Q4_K_M\" or \"google/gemma-4-31B-it\"" - }, - "variant_id": { - "type": "string", - "description": "Optional backend-specific variant id, such as a GGUF quantization or MLX dtype.", - "nullable": true - } - } - }, "DownloadProgress": { "type": "object", "required": [ @@ -4885,186 +4468,18 @@ "type": "string", "nullable": true } - } - } - ] - }, - "GooseMode": { - "type": "string", - "enum": [ - "auto", - "approve", - "smart_approve", - "chat" - ] - }, - "HfGgufFile": { - "type": "object", - "description": "A single downloadable GGUF file (used internally and for downloads).", - "required": [ - "filename", - "size_bytes", - "quantization", - "download_url" - ], - "properties": { - "download_url": { - "type": "string" - }, - "filename": { - "type": "string" - }, - "quantization": { - "type": "string" - }, - "size_bytes": { - "type": "integer", - "format": "int64", - "minimum": 0 - } - } - }, - "HfModelInfo": { - "type": "object", - "required": [ - "repo_id", - "author", - "model_name", - "downloads", - "gguf_files" - ], - "properties": { - "author": { - "type": "string" - }, - "downloads": { - "type": "integer", - "format": "int64", - "minimum": 0 - }, - "gguf_files": { - "type": "array", - "items": { - "$ref": "#/components/schemas/HfGgufFile" - } - }, - "model_name": { - "type": "string" - }, - "repo_id": { - "type": "string" - }, - "variants": { - "type": "array", - "items": { - "$ref": "#/components/schemas/HfModelVariant" - } - } - } - }, - "HfModelVariant": { - "type": "object", - "required": [ - "variant_id", - "label", - "backend_id", - "format", - "model_id", - "download_id", - "size_bytes", - "description", - "quality_rank" - ], - "properties": { - "backend_id": { - "type": "string" - }, - "description": { - "type": "string" - }, - "download_id": { - "type": "string" - }, - "download_url": { - "type": "string", - "nullable": true - }, - "filename": { - "type": "string", - "nullable": true - }, - "format": { - "type": "string" - }, - "label": { - "type": "string" - }, - "model_id": { - "type": "string" - }, - "quality_rank": { - "type": "integer", - "format": "int32", - "minimum": 0 - }, - "sharded": { - "type": "boolean" - }, - "size_bytes": { - "type": "integer", - "format": "int64", - "minimum": 0 - }, - "supported": { - "type": "boolean" - }, - "unsupported_reason": { - "type": "string", - "nullable": true - }, - "variant_id": { - "type": "string" - } - } - }, - "HfQuantVariant": { - "type": "object", - "description": "A quantization variant — groups sharded files into one logical entry.", - "required": [ - "quantization", - "size_bytes", - "filename", - "download_url", - "description", - "quality_rank" - ], - "properties": { - "description": { - "type": "string" - }, - "download_url": { - "type": "string" - }, - "filename": { - "type": "string" - }, - "quality_rank": { - "type": "integer", - "format": "int32", - "minimum": 0 - }, - "quantization": { - "type": "string" - }, - "sharded": { - "type": "boolean" - }, - "size_bytes": { - "type": "integer", - "format": "int64", - "minimum": 0 + } } - } + ] + }, + "GooseMode": { + "type": "string", + "enum": [ + "auto", + "approve", + "smart_approve", + "chat" + ] }, "Icon": { "type": "object", @@ -5236,59 +4651,6 @@ } } }, - "LocalModelResponse": { - "type": "object", - "required": [ - "id", - "repo_id", - "filename", - "quantization", - "size_bytes", - "status", - "recommended", - "settings", - "vision_capable" - ], - "properties": { - "filename": { - "type": "string" - }, - "id": { - "type": "string" - }, - "mmproj_status": { - "allOf": [ - { - "$ref": "#/components/schemas/ModelDownloadStatus" - } - ], - "nullable": true - }, - "quantization": { - "type": "string" - }, - "recommended": { - "type": "boolean" - }, - "repo_id": { - "type": "string" - }, - "settings": { - "$ref": "#/components/schemas/ModelSettings" - }, - "size_bytes": { - "type": "integer", - "format": "int64", - "minimum": 0 - }, - "status": { - "$ref": "#/components/schemas/ModelDownloadStatus" - }, - "vision_capable": { - "type": "boolean" - } - } - }, "McpAppResource": { "type": "object", "description": "MCP App Resource\nRepresents a UI resource that can be rendered in an MCP App", @@ -5827,78 +5189,6 @@ } } }, - "ModelDownloadStatus": { - "oneOf": [ - { - "type": "object", - "required": [ - "state" - ], - "properties": { - "state": { - "type": "string", - "enum": [ - "NotDownloaded" - ] - } - } - }, - { - "type": "object", - "required": [ - "progress_percent", - "bytes_downloaded", - "total_bytes", - "state" - ], - "properties": { - "bytes_downloaded": { - "type": "integer", - "format": "int64", - "minimum": 0 - }, - "progress_percent": { - "type": "number", - "format": "float" - }, - "speed_bps": { - "type": "integer", - "format": "int64", - "nullable": true, - "minimum": 0 - }, - "state": { - "type": "string", - "enum": [ - "Downloading" - ] - }, - "total_bytes": { - "type": "integer", - "format": "int64", - "minimum": 0 - } - } - }, - { - "type": "object", - "required": [ - "state" - ], - "properties": { - "state": { - "type": "string", - "enum": [ - "Downloaded" - ] - } - } - } - ], - "discriminator": { - "propertyName": "state" - } - }, "ModelInfo": { "type": "object", "description": "Information about a model's capabilities", @@ -6036,98 +5326,6 @@ } } }, - "ModelSettings": { - "type": "object", - "properties": { - "backend_id": { - "type": "string", - "description": "Backend implementation to use for this model. Defaults to llama.cpp.", - "nullable": true - }, - "chat_template": { - "$ref": "#/components/schemas/ChatTemplate" - }, - "context_size": { - "type": "integer", - "format": "int32", - "nullable": true, - "minimum": 0 - }, - "draft_model": { - "type": "string", - "nullable": true - }, - "enable_thinking": { - "type": "boolean" - }, - "flash_attention": { - "type": "boolean", - "nullable": true - }, - "frequency_penalty": { - "type": "number", - "format": "float" - }, - "image_token_estimate": { - "type": "integer", - "description": "Estimated tokens per image for budget planning before mtmd tokenization.\nThe actual count is determined after tokenization via `chunks.total_tokens()`.", - "minimum": 0 - }, - "max_output_tokens": { - "type": "integer", - "nullable": true, - "minimum": 0 - }, - "mmproj_size_bytes": { - "type": "integer", - "format": "int64", - "description": "Size of the mmproj file in bytes, used for memory accounting.", - "minimum": 0 - }, - "n_batch": { - "type": "integer", - "format": "int32", - "nullable": true, - "minimum": 0 - }, - "n_gpu_layers": { - "type": "integer", - "format": "int32", - "nullable": true, - "minimum": 0 - }, - "n_threads": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "presence_penalty": { - "type": "number", - "format": "float" - }, - "repeat_last_n": { - "type": "integer", - "format": "int32" - }, - "repeat_penalty": { - "type": "number", - "format": "float" - }, - "sampling": { - "$ref": "#/components/schemas/SamplingConfig" - }, - "tool_calling": { - "$ref": "#/components/schemas/ToolCallingMode" - }, - "use_mlock": { - "type": "boolean" - }, - "vision_capable": { - "type": "boolean", - "description": "Whether this model architecture supports vision input.\nDerived from associated mmproj metadata, not user-configurable." - } - } - }, "ModelTemplate": { "type": "object", "required": [ @@ -6881,45 +6079,6 @@ } } }, - "RepoVariantsResponse": { - "type": "object", - "required": [ - "variants", - "available_memory_bytes", - "downloaded_quants", - "downloaded_variants" - ], - "properties": { - "available_memory_bytes": { - "type": "integer", - "format": "int64", - "minimum": 0 - }, - "downloaded_quants": { - "type": "array", - "items": { - "type": "string" - } - }, - "downloaded_variants": { - "type": "array", - "items": { - "type": "string" - } - }, - "recommended_index": { - "type": "integer", - "nullable": true, - "minimum": 0 - }, - "variants": { - "type": "array", - "items": { - "$ref": "#/components/schemas/HfModelVariant" - } - } - } - }, "ResourceContents": { "anyOf": [ { @@ -7117,97 +6276,6 @@ } } }, - "SamplingConfig": { - "oneOf": [ - { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "Greedy" - ] - } - } - }, - { - "type": "object", - "required": [ - "temperature", - "top_k", - "top_p", - "min_p", - "type" - ], - "properties": { - "min_p": { - "type": "number", - "format": "float" - }, - "seed": { - "type": "integer", - "format": "int32", - "nullable": true, - "minimum": 0 - }, - "temperature": { - "type": "number", - "format": "float" - }, - "top_k": { - "type": "integer", - "format": "int32" - }, - "top_p": { - "type": "number", - "format": "float" - }, - "type": { - "type": "string", - "enum": [ - "Temperature" - ] - } - } - }, - { - "type": "object", - "required": [ - "tau", - "eta", - "type" - ], - "properties": { - "eta": { - "type": "number", - "format": "float" - }, - "seed": { - "type": "integer", - "format": "int32", - "nullable": true, - "minimum": 0 - }, - "tau": { - "type": "number", - "format": "float" - }, - "type": { - "type": "string", - "enum": [ - "MirostatV2" - ] - } - } - } - ], - "discriminator": { - "propertyName": "type" - } - }, "SavePromptRequest": { "type": "object", "required": [ @@ -8100,14 +7168,6 @@ } } }, - "ToolCallingMode": { - "type": "string", - "enum": [ - "auto", - "force_native", - "force_emulated" - ] - }, "ToolConfirmationRequest": { "type": "object", "required": [ diff --git a/ui/desktop/src/acp/local-inference.ts b/ui/desktop/src/acp/local-inference.ts new file mode 100644 index 000000000000..9003a4580d5a --- /dev/null +++ b/ui/desktop/src/acp/local-inference.ts @@ -0,0 +1,99 @@ +import type { + LocalInferenceDownloadProgressDto, + LocalInferenceHfModelInfoDto, + LocalInferenceHfModelVariantDto, + LocalInferenceModelDownloadRequest_unstable, + LocalInferenceModelDto, + LocalInferenceModelSettingsDto, +} from '@aaif/goose-sdk'; +import { getAcpClient } from './acpConnection'; + +export type LocalModelResponse = LocalInferenceModelDto; +export type DownloadProgress = LocalInferenceDownloadProgressDto; +export type DownloadModelRequest = LocalInferenceModelDownloadRequest_unstable; +export type HfModelInfo = LocalInferenceHfModelInfoDto; +export type HfModelVariant = LocalInferenceHfModelVariantDto; +export type ModelSettings = LocalInferenceModelSettingsDto; +export type SamplingConfig = NonNullable; +export type ToolCallingMode = NonNullable; +export type ChatTemplate = NonNullable; + +export type RepoVariantsResponse = { + variants: HfModelVariant[]; + recommendedIndex: number | null; + availableMemoryBytes: number; + downloadedQuants: string[]; + downloadedVariants: string[]; +}; + +export async function listLocalModels(): Promise { + const client = await getAcpClient(); + const response = await client.goose.localInferenceModelsList_unstable({}); + return response.models; +} + +export async function downloadHfModel(request: DownloadModelRequest): Promise { + const client = await getAcpClient(); + const response = await client.goose.localInferenceModelsDownload_unstable(request); + return response.modelId; +} + +export async function getLocalModelDownloadProgress( + modelId: string +): Promise { + const client = await getAcpClient(); + const response = await client.goose.localInferenceModelsDownloadProgress_unstable({ modelId }); + return response.progress ?? null; +} + +export async function cancelLocalModelDownload(modelId: string): Promise { + const client = await getAcpClient(); + await client.goose.localInferenceModelsDownloadCancel_unstable({ modelId }); +} + +export async function deleteLocalModel(modelId: string): Promise { + const client = await getAcpClient(); + await client.goose.localInferenceModelsDelete_unstable({ modelId }); +} + +export async function getModelSettings(modelId: string): Promise { + const client = await getAcpClient(); + const response = await client.goose.localInferenceModelsSettingsRead_unstable({ modelId }); + return response.settings; +} + +export async function updateModelSettings( + modelId: string, + settings: ModelSettings +): Promise { + const client = await getAcpClient(); + const response = await client.goose.localInferenceModelsSettingsUpdate_unstable({ + modelId, + settings, + }); + return response.settings; +} + +export async function searchHfModels(query: string, limit?: number): Promise { + const client = await getAcpClient(); + const response = await client.goose.localInferenceHuggingfaceSearch_unstable({ query, limit }); + return response.models; +} + +export async function getRepoFiles(repoId: string): Promise { + const client = await getAcpClient(); + const response = await client.goose.localInferenceHuggingfaceRepoVariants_unstable({ repoId }); + return { + variants: response.variants, + recommendedIndex: response.recommendedIndex ?? null, + availableMemoryBytes: response.availableMemoryBytes, + downloadedQuants: response.downloadedQuants, + downloadedVariants: response.downloadedVariants, + }; +} + +export async function listBuiltinChatTemplates(): Promise { + const client = await getAcpClient(); + const response = await client.goose.localInferenceChatTemplatesBuiltinList_unstable({}); + return response.templates; +} diff --git a/ui/desktop/src/api/index.ts b/ui/desktop/src/api/index.ts index 00453e033c82..afe1f8254327 100644 --- a/ui/desktop/src/api/index.ts +++ b/ui/desktop/src/api/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts -export { addExtension, agentAddExtension, agentRemoveExtension, cancelDownload, cancelLocalModelDownload, checkProvider, cleanupProviderCache, confirmToolAction, createCustomProvider, createSchedule, decodeRecipe, deleteLocalModel, deleteModel, deleteProviderSecret, deleteRecipe, deleteSchedule, diagnostics, downloadHfModel, downloadModel, encodeRecipe, forkSession, getCanonicalModelInfo, getCustomProvider, getDictationConfig, getDownloadProgress, getExtensions, getLocalModelDownloadProgress, getModelSettings, getPrompt, getPrompts, getProviderCatalog, getProviderCatalogTemplate, getProviderModelInfo, getProviderModels, getRepoFiles, getSession, getSessionExtensions, getSlashCommands, getTools, inspectRunningJob, killRunningJob, listBuiltinChatTemplates, listLocalModels, listModels, listProviderSecrets, listRecipes, listSchedules, type Options, parseRecipe, pauseSchedule, providers, readAllConfig, readConfig, recipeToYaml, removeConfig, removeCustomProvider, removeExtension, reply, resetPrompt, restartAgent, resumeAgent, runNowHandler, savePrompt, saveRecipe, scanRecipe, scheduleRecipe, searchHfModels, sendTelemetryEvent, sessionCancel, sessionEvents, sessionReply, sessionsHandler, setConfigProvider, setRecipeSlashCommand, startAgent, status, stopAgent, syncFeaturedModels, systemInfo, transcribeDictation, unpauseSchedule, updateAgentProvider, updateCustomProvider, updateFromSession, updateModelSettings, updateSchedule, updateSession, updateSessionName, updateSessionUserRecipeValues, updateWorkingDir, upsertConfig, validateConfig } from './sdk.gen'; -export type { ActionRequired, ActionRequiredData, AddExtensionData, AddExtensionErrors, AddExtensionRequest, AddExtensionResponse, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponse, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponse, AgentRemoveExtensionResponses, Annotations, Author, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CancelRequest, ChatRequest, ChatTemplate, CheckProviderData, CheckProviderRequest, CleanupProviderCacheData, CleanupProviderCacheErrors, CleanupProviderCacheResponse, CleanupProviderCacheResponses, ClientOptions, CommandType, ConfigKey, ConfigKeyQuery, ConfigResponse, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionRequest, ConfirmToolActionResponses, Content, ContentBlock, Conversation, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponse, CreateCustomProviderResponse2, CreateCustomProviderResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleRequest, CreateScheduleResponse, CreateScheduleResponses, CspMetadata, DeclarativeProviderConfig, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeRequest, DecodeRecipeResponse, DecodeRecipeResponse2, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteProviderSecretData, DeleteProviderSecretErrors, DeleteProviderSecretResponse, DeleteProviderSecretResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeRequest, DeleteRecipeResponse, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponse, DeleteScheduleResponses, DiagnosticsConfig, DiagnosticsData, DiagnosticsError, DiagnosticsErrors, DiagnosticsExtensions, DiagnosticsLevel, DiagnosticsLogs, DiagnosticsPrompt, DiagnosticsReport, DiagnosticsResponse, DiagnosticsResponses, DiagnosticsScheduledRecipe, DiagnosticsTextFile, DictationProvider, DictationProviderStatus, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponse, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelRequest, DownloadModelResponses, DownloadProgress, DownloadStatus, EmbeddedResource, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeRequest, EncodeRecipeResponse, EncodeRecipeResponse2, EncodeRecipeResponses, Envs, EnvVarConfig, ErrorResponse, ExtensionConfig, ExtensionData, ExtensionEntry, ExtensionLoadResult, ExtensionQuery, ExtensionResponse, ForkRequest, ForkResponse, ForkSessionData, ForkSessionErrors, ForkSessionResponse, ForkSessionResponses, FrontendToolRequest, GetCanonicalModelInfoData, GetCanonicalModelInfoResponse, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponse, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponse, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponse, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponse, GetExtensionsResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponse, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponse, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponse, GetPromptResponses, GetPromptsData, GetPromptsResponse, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponse, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponse, GetProviderCatalogTemplateResponses, GetProviderModelInfoData, GetProviderModelInfoErrors, GetProviderModelInfoResponse, GetProviderModelInfoResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponse, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponse, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponse, GetSessionExtensionsResponses, GetSessionResponse, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponse, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsQuery, GetToolsResponse, GetToolsResponses, GooseApp, GooseMode, HfGgufFile, HfModelInfo, HfModelVariant, HfQuantVariant, Icon, IconTheme, ImageContent, InferenceMetadata, InspectJobResponse, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponse, InspectRunningJobResponses, JsonObject, KillJobResponse, KillRunningJobData, KillRunningJobResponses, ListBuiltinChatTemplatesData, ListBuiltinChatTemplatesResponse, ListBuiltinChatTemplatesResponses, ListLocalModelsData, ListLocalModelsResponse, ListLocalModelsResponses, ListModelsData, ListModelsResponse, ListModelsResponses, ListProviderSecretsData, ListProviderSecretsErrors, ListProviderSecretsResponse, ListProviderSecretsResponses, ListRecipeResponse, ListRecipesData, ListRecipesErrors, ListRecipesResponse, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponse, ListSchedulesResponse2, ListSchedulesResponses, LoadedProvider, LocalModelResponse, McpAppResource, Message, MessageContent, MessageEvent, MessageMetadata, ModelCapabilities, ModelConfig, ModelDownloadStatus, ModelInfo, ModelInfoData, ModelInfoQuery, ModelInfoResponse, ModelSettings, ModelTemplate, ParseRecipeData, ParseRecipeError, ParseRecipeErrors, ParseRecipeRequest, ParseRecipeResponse, ParseRecipeResponse2, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponse, PauseScheduleResponses, Permission, PermissionLevel, PermissionsMetadata, PrincipalType, PromptContentResponse, PromptsListResponse, ProviderCatalogEntry, ProviderDetails, ProviderEngine, ProviderMetadata, ProviderModelInfoQuery, ProvidersData, ProviderSecret, ProviderSecretsResponse, ProviderSecretStatus, ProviderSecretStorage, ProvidersResponse, ProvidersResponse2, ProvidersResponses, ProviderTemplate, ProviderType, RawAudioContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ReadAllConfigData, ReadAllConfigResponse, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, Recipe, RecipeManifest, RecipeParameter, RecipeParameterInputType, RecipeParameterRequirement, RecipeToYamlData, RecipeToYamlError, RecipeToYamlErrors, RecipeToYamlRequest, RecipeToYamlResponse, RecipeToYamlResponse2, RecipeToYamlResponses, RedactedThinkingContent, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponse, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponse, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionRequest, RemoveExtensionResponse, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponse, ReplyResponses, RepoVariantsResponse, ResetPromptData, ResetPromptErrors, ResetPromptResponse, ResetPromptResponses, ResourceContents, ResourceMetadata, Response, RestartAgentData, RestartAgentErrors, RestartAgentRequest, RestartAgentResponse, RestartAgentResponse2, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentRequest, ResumeAgentResponse, ResumeAgentResponse2, ResumeAgentResponses, RetryConfig, Role, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponse, RunNowHandlerResponses, RunNowResponse, SamplingConfig, SavePromptData, SavePromptErrors, SavePromptRequest, SavePromptResponse, SavePromptResponses, SaveRecipeData, SaveRecipeError, SaveRecipeErrors, SaveRecipeRequest, SaveRecipeResponse, SaveRecipeResponse2, SaveRecipeResponses, ScanRecipeData, ScanRecipeRequest, ScanRecipeResponse, ScanRecipeResponse2, ScanRecipeResponses, ScheduledJob, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeRequest, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponse, SearchHfModelsResponses, SendTelemetryEventData, SendTelemetryEventResponses, Session, SessionCancelData, SessionCancelResponses, SessionDisplayInfo, SessionEventsData, SessionEventsErrors, SessionEventsResponse, SessionEventsResponses, SessionExtensionsResponse, SessionReplyData, SessionReplyErrors, SessionReplyRequest, SessionReplyResponse, SessionReplyResponse2, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponse, SessionsHandlerResponses, SessionsQuery, SessionType, SetConfigProviderData, SetProviderRequest, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, SetSlashCommandRequest, Settings, SlashCommand, SlashCommandsResponse, StartAgentData, StartAgentError, StartAgentErrors, StartAgentRequest, StartAgentResponse, StartAgentResponses, StatusData, StatusResponse, StatusResponses, StopAgentData, StopAgentErrors, StopAgentRequest, StopAgentResponse, StopAgentResponses, SubRecipe, SuccessCheck, SyncFeaturedModelsData, SyncFeaturedModelsResponses, SystemInfo, SystemInfoData, SystemInfoResponse, SystemInfoResponses, SystemNotificationContent, SystemNotificationType, TaskSupport, TelemetryEventRequest, Template, TextContent, ThinkingContent, ThinkingEffort, TokenState, Tool, ToolAnnotations, ToolCallingMode, ToolConfirmationRequest, ToolExecution, ToolInfo, ToolRequest, ToolResponse, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponse, TranscribeDictationResponses, TranscribeRequest, TranscribeResponse, UiMetadata, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponse, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderRequest, UpdateCustomProviderResponse, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionRequest, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponse, UpdateModelSettingsResponses, UpdateProviderRequest, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleRequest, UpdateScheduleResponse, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameRequest, UpdateSessionNameResponses, UpdateSessionRequest, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesError, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesRequest, UpdateSessionUserRecipeValuesResponse, UpdateSessionUserRecipeValuesResponse2, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirRequest, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigQuery, UpsertConfigResponse, UpsertConfigResponses, Usage, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponse, ValidateConfigResponses, WhisperModelResponse, WindowProps } from './types.gen'; +export { addExtension, agentAddExtension, agentRemoveExtension, cancelDownload, checkProvider, cleanupProviderCache, confirmToolAction, createCustomProvider, createSchedule, decodeRecipe, deleteModel, deleteProviderSecret, deleteRecipe, deleteSchedule, diagnostics, downloadModel, encodeRecipe, forkSession, getCanonicalModelInfo, getCustomProvider, getDictationConfig, getDownloadProgress, getExtensions, getPrompt, getPrompts, getProviderCatalog, getProviderCatalogTemplate, getProviderModelInfo, getProviderModels, getSession, getSessionExtensions, getSlashCommands, getTools, inspectRunningJob, killRunningJob, listModels, listProviderSecrets, listRecipes, listSchedules, type Options, parseRecipe, pauseSchedule, providers, readAllConfig, readConfig, recipeToYaml, removeConfig, removeCustomProvider, removeExtension, reply, resetPrompt, restartAgent, resumeAgent, runNowHandler, savePrompt, saveRecipe, scanRecipe, scheduleRecipe, sendTelemetryEvent, sessionCancel, sessionEvents, sessionReply, sessionsHandler, setConfigProvider, setRecipeSlashCommand, startAgent, status, stopAgent, systemInfo, transcribeDictation, unpauseSchedule, updateAgentProvider, updateCustomProvider, updateFromSession, updateSchedule, updateSession, updateSessionName, updateSessionUserRecipeValues, updateWorkingDir, upsertConfig, validateConfig } from './sdk.gen'; +export type { ActionRequired, ActionRequiredData, AddExtensionData, AddExtensionErrors, AddExtensionRequest, AddExtensionResponse, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponse, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponse, AgentRemoveExtensionResponses, Annotations, Author, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelRequest, ChatRequest, CheckProviderData, CheckProviderRequest, CleanupProviderCacheData, CleanupProviderCacheErrors, CleanupProviderCacheResponse, CleanupProviderCacheResponses, ClientOptions, CommandType, ConfigKey, ConfigKeyQuery, ConfigResponse, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionRequest, ConfirmToolActionResponses, Content, ContentBlock, Conversation, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponse, CreateCustomProviderResponse2, CreateCustomProviderResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleRequest, CreateScheduleResponse, CreateScheduleResponses, CspMetadata, DeclarativeProviderConfig, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeRequest, DecodeRecipeResponse, DecodeRecipeResponse2, DecodeRecipeResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteProviderSecretData, DeleteProviderSecretErrors, DeleteProviderSecretResponse, DeleteProviderSecretResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeRequest, DeleteRecipeResponse, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponse, DeleteScheduleResponses, DiagnosticsConfig, DiagnosticsData, DiagnosticsError, DiagnosticsErrors, DiagnosticsExtensions, DiagnosticsLevel, DiagnosticsLogs, DiagnosticsPrompt, DiagnosticsReport, DiagnosticsResponse, DiagnosticsResponses, DiagnosticsScheduledRecipe, DiagnosticsTextFile, DictationProvider, DictationProviderStatus, DownloadModelData, DownloadModelErrors, DownloadModelResponses, DownloadProgress, DownloadStatus, EmbeddedResource, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeRequest, EncodeRecipeResponse, EncodeRecipeResponse2, EncodeRecipeResponses, Envs, EnvVarConfig, ErrorResponse, ExtensionConfig, ExtensionData, ExtensionEntry, ExtensionLoadResult, ExtensionQuery, ExtensionResponse, ForkRequest, ForkResponse, ForkSessionData, ForkSessionErrors, ForkSessionResponse, ForkSessionResponses, FrontendToolRequest, GetCanonicalModelInfoData, GetCanonicalModelInfoResponse, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponse, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponse, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponse, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponse, GetExtensionsResponses, GetPromptData, GetPromptErrors, GetPromptResponse, GetPromptResponses, GetPromptsData, GetPromptsResponse, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponse, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponse, GetProviderCatalogTemplateResponses, GetProviderModelInfoData, GetProviderModelInfoErrors, GetProviderModelInfoResponse, GetProviderModelInfoResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponse, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponse, GetSessionExtensionsResponses, GetSessionResponse, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponse, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsQuery, GetToolsResponse, GetToolsResponses, GooseApp, GooseMode, Icon, IconTheme, ImageContent, InferenceMetadata, InspectJobResponse, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponse, InspectRunningJobResponses, JsonObject, KillJobResponse, KillRunningJobData, KillRunningJobResponses, ListModelsData, ListModelsResponse, ListModelsResponses, ListProviderSecretsData, ListProviderSecretsErrors, ListProviderSecretsResponse, ListProviderSecretsResponses, ListRecipeResponse, ListRecipesData, ListRecipesErrors, ListRecipesResponse, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponse, ListSchedulesResponse2, ListSchedulesResponses, LoadedProvider, McpAppResource, Message, MessageContent, MessageEvent, MessageMetadata, ModelCapabilities, ModelConfig, ModelInfo, ModelInfoData, ModelInfoQuery, ModelInfoResponse, ModelTemplate, ParseRecipeData, ParseRecipeError, ParseRecipeErrors, ParseRecipeRequest, ParseRecipeResponse, ParseRecipeResponse2, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponse, PauseScheduleResponses, Permission, PermissionLevel, PermissionsMetadata, PrincipalType, PromptContentResponse, PromptsListResponse, ProviderCatalogEntry, ProviderDetails, ProviderEngine, ProviderMetadata, ProviderModelInfoQuery, ProvidersData, ProviderSecret, ProviderSecretsResponse, ProviderSecretStatus, ProviderSecretStorage, ProvidersResponse, ProvidersResponse2, ProvidersResponses, ProviderTemplate, ProviderType, RawAudioContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ReadAllConfigData, ReadAllConfigResponse, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, Recipe, RecipeManifest, RecipeParameter, RecipeParameterInputType, RecipeParameterRequirement, RecipeToYamlData, RecipeToYamlError, RecipeToYamlErrors, RecipeToYamlRequest, RecipeToYamlResponse, RecipeToYamlResponse2, RecipeToYamlResponses, RedactedThinkingContent, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponse, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponse, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionRequest, RemoveExtensionResponse, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponse, ReplyResponses, ResetPromptData, ResetPromptErrors, ResetPromptResponse, ResetPromptResponses, ResourceContents, ResourceMetadata, Response, RestartAgentData, RestartAgentErrors, RestartAgentRequest, RestartAgentResponse, RestartAgentResponse2, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentRequest, ResumeAgentResponse, ResumeAgentResponse2, ResumeAgentResponses, RetryConfig, Role, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponse, RunNowHandlerResponses, RunNowResponse, SavePromptData, SavePromptErrors, SavePromptRequest, SavePromptResponse, SavePromptResponses, SaveRecipeData, SaveRecipeError, SaveRecipeErrors, SaveRecipeRequest, SaveRecipeResponse, SaveRecipeResponse2, SaveRecipeResponses, ScanRecipeData, ScanRecipeRequest, ScanRecipeResponse, ScanRecipeResponse2, ScanRecipeResponses, ScheduledJob, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeRequest, ScheduleRecipeResponses, SendTelemetryEventData, SendTelemetryEventResponses, Session, SessionCancelData, SessionCancelResponses, SessionDisplayInfo, SessionEventsData, SessionEventsErrors, SessionEventsResponse, SessionEventsResponses, SessionExtensionsResponse, SessionReplyData, SessionReplyErrors, SessionReplyRequest, SessionReplyResponse, SessionReplyResponse2, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponse, SessionsHandlerResponses, SessionsQuery, SessionType, SetConfigProviderData, SetProviderRequest, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, SetSlashCommandRequest, Settings, SlashCommand, SlashCommandsResponse, StartAgentData, StartAgentError, StartAgentErrors, StartAgentRequest, StartAgentResponse, StartAgentResponses, StatusData, StatusResponse, StatusResponses, StopAgentData, StopAgentErrors, StopAgentRequest, StopAgentResponse, StopAgentResponses, SubRecipe, SuccessCheck, SystemInfo, SystemInfoData, SystemInfoResponse, SystemInfoResponses, SystemNotificationContent, SystemNotificationType, TaskSupport, TelemetryEventRequest, Template, TextContent, ThinkingContent, ThinkingEffort, TokenState, Tool, ToolAnnotations, ToolConfirmationRequest, ToolExecution, ToolInfo, ToolRequest, ToolResponse, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponse, TranscribeDictationResponses, TranscribeRequest, TranscribeResponse, UiMetadata, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponse, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderRequest, UpdateCustomProviderResponse, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionRequest, UpdateFromSessionResponses, UpdateProviderRequest, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleRequest, UpdateScheduleResponse, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameRequest, UpdateSessionNameResponses, UpdateSessionRequest, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesError, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesRequest, UpdateSessionUserRecipeValuesResponse, UpdateSessionUserRecipeValuesResponse2, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirRequest, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigQuery, UpsertConfigResponse, UpsertConfigResponses, Usage, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponse, ValidateConfigResponses, WhisperModelResponse, WindowProps } from './types.gen'; diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index f952f1d78886..4786dcf2704e 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -2,7 +2,7 @@ import type { Client, Options as Options2, TDataShape } from './client'; import { client } from './client.gen'; -import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CheckProviderData, CleanupProviderCacheData, CleanupProviderCacheErrors, CleanupProviderCacheResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteProviderSecretData, DeleteProviderSecretErrors, DeleteProviderSecretResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ForkSessionData, ForkSessionErrors, ForkSessionResponses, GetCanonicalModelInfoData, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponses, GetPromptsData, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponses, GetProviderModelInfoData, GetProviderModelInfoErrors, GetProviderModelInfoResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListBuiltinChatTemplatesData, ListBuiltinChatTemplatesResponses, ListLocalModelsData, ListLocalModelsResponses, ListModelsData, ListModelsResponses, ListProviderSecretsData, ListProviderSecretsErrors, ListProviderSecretsResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecipeToYamlData, RecipeToYamlErrors, RecipeToYamlResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResetPromptData, ResetPromptErrors, ResetPromptResponses, RestartAgentData, RestartAgentErrors, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SavePromptData, SavePromptErrors, SavePromptResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionCancelData, SessionCancelResponses, SessionEventsData, SessionEventsErrors, SessionEventsResponses, SessionReplyData, SessionReplyErrors, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StatusData, StatusResponses, StopAgentData, StopAgentErrors, StopAgentResponses, SyncFeaturedModelsData, SyncFeaturedModelsResponses, SystemInfoData, SystemInfoResponses, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; +import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CheckProviderData, CleanupProviderCacheData, CleanupProviderCacheErrors, CleanupProviderCacheResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteProviderSecretData, DeleteProviderSecretErrors, DeleteProviderSecretResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, DownloadModelData, DownloadModelErrors, DownloadModelResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ForkSessionData, ForkSessionErrors, ForkSessionResponses, GetCanonicalModelInfoData, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetPromptData, GetPromptErrors, GetPromptResponses, GetPromptsData, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponses, GetProviderModelInfoData, GetProviderModelInfoErrors, GetProviderModelInfoResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListModelsData, ListModelsResponses, ListProviderSecretsData, ListProviderSecretsErrors, ListProviderSecretsResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecipeToYamlData, RecipeToYamlErrors, RecipeToYamlResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResetPromptData, ResetPromptErrors, ResetPromptResponses, RestartAgentData, RestartAgentErrors, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SavePromptData, SavePromptErrors, SavePromptResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionCancelData, SessionCancelResponses, SessionEventsData, SessionEventsErrors, SessionEventsResponses, SessionReplyData, SessionReplyErrors, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StatusData, StatusResponses, StopAgentData, StopAgentErrors, StopAgentResponses, SystemInfoData, SystemInfoResponses, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; export type Options = Options2 & { /** @@ -275,42 +275,6 @@ export const transcribeDictation = (option } }); -export const listBuiltinChatTemplates = (options?: Options) => (options?.client ?? client).get({ url: '/local-inference/chat-templates/builtin', ...options }); - -export const downloadHfModel = (options: Options) => (options.client ?? client).post({ - url: '/local-inference/download', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } -}); - -export const listLocalModels = (options?: Options) => (options?.client ?? client).get({ url: '/local-inference/models', ...options }); - -export const deleteLocalModel = (options: Options) => (options.client ?? client).delete({ url: '/local-inference/models/{model_id}', ...options }); - -export const cancelLocalModelDownload = (options: Options) => (options.client ?? client).delete({ url: '/local-inference/models/{model_id}/download', ...options }); - -export const getLocalModelDownloadProgress = (options: Options) => (options.client ?? client).get({ url: '/local-inference/models/{model_id}/download', ...options }); - -export const getModelSettings = (options: Options) => (options.client ?? client).get({ url: '/local-inference/models/{model_id}/settings', ...options }); - -export const updateModelSettings = (options: Options) => (options.client ?? client).put({ - url: '/local-inference/models/{model_id}/settings', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } -}); - -export const getRepoFiles = (options: Options) => (options.client ?? client).get({ url: '/local-inference/repo/{author}/{repo}/files', ...options }); - -export const searchHfModels = (options: Options) => (options.client ?? client).get({ url: '/local-inference/search', ...options }); - -export const syncFeaturedModels = (options?: Options) => (options?.client ?? client).post({ url: '/local-inference/sync-featured', ...options }); - export const decodeRecipe = (options: Options) => (options.client ?? client).post({ url: '/recipes/decode', ...options, diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 4cc881682070..83c68e99e55d 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -59,16 +59,6 @@ export type ChatRequest = { user_message: Message; }; -export type ChatTemplate = { - type: 'embedded'; -} | { - name: string; - type: 'builtin'; -} | { - template: string; - type: 'custom_inline'; -}; - export type CheckProviderRequest = { provider: string; }; @@ -316,21 +306,6 @@ export type DictationProviderStatus = { uses_provider_config: boolean; }; -export type DownloadModelRequest = { - /** - * Optional backend id for callers selecting a concrete variant row. - */ - backend_id?: string | null; - /** - * Model spec/download id like "bartowski/Llama-3.2-3B-Instruct-GGUF:Q4_K_M" or "google/gemma-4-31B-it" - */ - spec: string; - /** - * Optional backend-specific variant id, such as a GGUF quantization or MLX dtype. - */ - variant_id?: string | null; -}; - export type DownloadProgress = { /** * Bytes downloaded so far @@ -568,55 +543,6 @@ export type GooseApp = McpAppResource & (WindowProps | null) & { export type GooseMode = 'auto' | 'approve' | 'smart_approve' | 'chat'; -/** - * A single downloadable GGUF file (used internally and for downloads). - */ -export type HfGgufFile = { - download_url: string; - filename: string; - quantization: string; - size_bytes: number; -}; - -export type HfModelInfo = { - author: string; - downloads: number; - gguf_files: Array; - model_name: string; - repo_id: string; - variants?: Array; -}; - -export type HfModelVariant = { - backend_id: string; - description: string; - download_id: string; - download_url?: string | null; - filename?: string | null; - format: string; - label: string; - model_id: string; - quality_rank: number; - sharded?: boolean; - size_bytes: number; - supported?: boolean; - unsupported_reason?: string | null; - variant_id: string; -}; - -/** - * A quantization variant — groups sharded files into one logical entry. - */ -export type HfQuantVariant = { - description: string; - download_url: string; - filename: string; - quality_rank: number; - quantization: string; - sharded?: boolean; - size_bytes: number; -}; - export type Icon = { mimeType?: string; sizes?: Array; @@ -672,19 +598,6 @@ export type LoadedProvider = { is_editable: boolean; }; -export type LocalModelResponse = { - filename: string; - id: string; - mmproj_status?: ModelDownloadStatus | null; - quantization: string; - recommended: boolean; - repo_id: string; - settings: ModelSettings; - size_bytes: number; - status: ModelDownloadStatus; - vision_capable: boolean; -}; - /** * MCP App Resource * Represents a UI resource that can be rendered in an MCP App @@ -824,18 +737,6 @@ export type ModelConfig = { toolshim_model?: string | null; }; -export type ModelDownloadStatus = { - state: 'NotDownloaded'; -} | { - bytes_downloaded: number; - progress_percent: number; - speed_bps?: number | null; - state: 'Downloading'; - total_bytes: number; -} | { - state: 'Downloaded'; -}; - /** * Information about a model's capabilities */ @@ -897,43 +798,6 @@ export type ModelInfoResponse = { source: string; }; -export type ModelSettings = { - /** - * Backend implementation to use for this model. Defaults to llama.cpp. - */ - backend_id?: string | null; - chat_template?: ChatTemplate; - context_size?: number | null; - draft_model?: string | null; - enable_thinking?: boolean; - flash_attention?: boolean | null; - frequency_penalty?: number; - /** - * Estimated tokens per image for budget planning before mtmd tokenization. - * The actual count is determined after tokenization via `chunks.total_tokens()`. - */ - image_token_estimate?: number; - max_output_tokens?: number | null; - /** - * Size of the mmproj file in bytes, used for memory accounting. - */ - mmproj_size_bytes?: number; - n_batch?: number | null; - n_gpu_layers?: number | null; - n_threads?: number | null; - presence_penalty?: number; - repeat_last_n?: number; - repeat_penalty?: number; - sampling?: SamplingConfig; - tool_calling?: ToolCallingMode; - use_mlock?: boolean; - /** - * Whether this model architecture supports vision input. - * Derived from associated mmproj metadata, not user-configurable. - */ - vision_capable?: boolean; -}; - export type ModelTemplate = { capabilities: ModelCapabilities; context_limit: number; @@ -1200,14 +1064,6 @@ export type RemoveExtensionRequest = { session_id: string; }; -export type RepoVariantsResponse = { - available_memory_bytes: number; - downloaded_quants: Array; - downloaded_variants: Array; - recommended_index?: number | null; - variants: Array; -}; - export type ResourceContents = { _meta?: { [key: string]: unknown; @@ -1285,22 +1141,6 @@ export type RunNowResponse = { session_id: string; }; -export type SamplingConfig = { - type: 'Greedy'; -} | { - min_p: number; - seed?: number | null; - temperature: number; - top_k: number; - top_p: number; - type: 'Temperature'; -} | { - eta: number; - seed?: number | null; - tau: number; - type: 'MirostatV2'; -}; - export type SavePromptRequest = { content: string; }; @@ -1572,8 +1412,6 @@ export type ToolAnnotations = { title?: string; }; -export type ToolCallingMode = 'auto' | 'force_native' | 'force_emulated'; - export type ToolConfirmationRequest = { arguments: JsonObject; id: string; @@ -3054,251 +2892,6 @@ export type TranscribeDictationResponses = { export type TranscribeDictationResponse = TranscribeDictationResponses[keyof TranscribeDictationResponses]; -export type ListBuiltinChatTemplatesData = { - body?: never; - path?: never; - query?: never; - url: '/local-inference/chat-templates/builtin'; -}; - -export type ListBuiltinChatTemplatesResponses = { - /** - * llama.cpp built-in chat template names - */ - 200: Array; -}; - -export type ListBuiltinChatTemplatesResponse = ListBuiltinChatTemplatesResponses[keyof ListBuiltinChatTemplatesResponses]; - -export type DownloadHfModelData = { - body: DownloadModelRequest; - path?: never; - query?: never; - url: '/local-inference/download'; -}; - -export type DownloadHfModelErrors = { - /** - * Invalid request - */ - 400: unknown; -}; - -export type DownloadHfModelResponses = { - /** - * Download started - */ - 202: string; -}; - -export type DownloadHfModelResponse = DownloadHfModelResponses[keyof DownloadHfModelResponses]; - -export type ListLocalModelsData = { - body?: never; - path?: never; - query?: never; - url: '/local-inference/models'; -}; - -export type ListLocalModelsResponses = { - /** - * List of available local LLM models - */ - 200: Array; -}; - -export type ListLocalModelsResponse = ListLocalModelsResponses[keyof ListLocalModelsResponses]; - -export type DeleteLocalModelData = { - body?: never; - path: { - model_id: string; - }; - query?: never; - url: '/local-inference/models/{model_id}'; -}; - -export type DeleteLocalModelErrors = { - /** - * Model not found - */ - 404: unknown; -}; - -export type DeleteLocalModelResponses = { - /** - * Model deleted - */ - 200: unknown; -}; - -export type CancelLocalModelDownloadData = { - body?: never; - path: { - model_id: string; - }; - query?: never; - url: '/local-inference/models/{model_id}/download'; -}; - -export type CancelLocalModelDownloadErrors = { - /** - * No active download - */ - 404: unknown; -}; - -export type CancelLocalModelDownloadResponses = { - /** - * Download cancelled - */ - 200: unknown; -}; - -export type GetLocalModelDownloadProgressData = { - body?: never; - path: { - model_id: string; - }; - query?: never; - url: '/local-inference/models/{model_id}/download'; -}; - -export type GetLocalModelDownloadProgressErrors = { - /** - * No active download - */ - 404: unknown; -}; - -export type GetLocalModelDownloadProgressResponses = { - /** - * Download progress - */ - 200: DownloadProgress; -}; - -export type GetLocalModelDownloadProgressResponse = GetLocalModelDownloadProgressResponses[keyof GetLocalModelDownloadProgressResponses]; - -export type GetModelSettingsData = { - body?: never; - path: { - model_id: string; - }; - query?: never; - url: '/local-inference/models/{model_id}/settings'; -}; - -export type GetModelSettingsErrors = { - /** - * Model not found - */ - 404: unknown; -}; - -export type GetModelSettingsResponses = { - /** - * Model settings - */ - 200: ModelSettings; -}; - -export type GetModelSettingsResponse = GetModelSettingsResponses[keyof GetModelSettingsResponses]; - -export type UpdateModelSettingsData = { - body: ModelSettings; - path: { - model_id: string; - }; - query?: never; - url: '/local-inference/models/{model_id}/settings'; -}; - -export type UpdateModelSettingsErrors = { - /** - * Model not found - */ - 404: unknown; - /** - * Failed to save settings - */ - 500: unknown; -}; - -export type UpdateModelSettingsResponses = { - /** - * Settings updated - */ - 200: ModelSettings; -}; - -export type UpdateModelSettingsResponse = UpdateModelSettingsResponses[keyof UpdateModelSettingsResponses]; - -export type GetRepoFilesData = { - body?: never; - path: { - author: string; - repo: string; - }; - query?: never; - url: '/local-inference/repo/{author}/{repo}/files'; -}; - -export type GetRepoFilesResponses = { - /** - * GGUF files in the repo - */ - 200: RepoVariantsResponse; -}; - -export type GetRepoFilesResponse = GetRepoFilesResponses[keyof GetRepoFilesResponses]; - -export type SearchHfModelsData = { - body?: never; - path?: never; - query: { - /** - * Search query - */ - q: string; - /** - * Max results - */ - limit?: number | null; - }; - url: '/local-inference/search'; -}; - -export type SearchHfModelsErrors = { - /** - * Search failed - */ - 500: unknown; -}; - -export type SearchHfModelsResponses = { - /** - * Search results - */ - 200: Array; -}; - -export type SearchHfModelsResponse = SearchHfModelsResponses[keyof SearchHfModelsResponses]; - -export type SyncFeaturedModelsData = { - body?: never; - path?: never; - query?: never; - url: '/local-inference/sync-featured'; -}; - -export type SyncFeaturedModelsResponses = { - /** - * Featured models synced to registry - */ - 200: unknown; -}; - export type DecodeRecipeData = { body: DecodeRecipeRequest; path?: never; diff --git a/ui/desktop/src/components/onboarding/LocalModelPicker.tsx b/ui/desktop/src/components/onboarding/LocalModelPicker.tsx index 6a27076e09d2..17a3c7441d3b 100644 --- a/ui/desktop/src/components/onboarding/LocalModelPicker.tsx +++ b/ui/desktop/src/components/onboarding/LocalModelPicker.tsx @@ -1,13 +1,12 @@ import { useState, useEffect, useCallback, useRef } from 'react'; import { listLocalModels, - syncFeaturedModels, downloadHfModel, getLocalModelDownloadProgress, cancelLocalModelDownload, type DownloadProgress, type LocalModelResponse, -} from '../../api'; +} from '../../acp/local-inference'; import { trackOnboardingSetupFailed } from '../../utils/analytics'; import { defineMessages, useIntl } from '../../i18n'; @@ -124,16 +123,15 @@ export default function LocalModelPicker({ onConfigured }: LocalModelPickerProps useEffect(() => { const load = async () => { try { - await syncFeaturedModels(); - const response = await listLocalModels({ throwOnError: true }); - if (response.data) { - setModels(response.data); + const models = await listLocalModels(); + if (models) { + setModels(models); - const alreadyDownloaded = response.data.find((m) => m.status.state === 'Downloaded'); + const alreadyDownloaded = models.find((m) => m.status.state === 'Downloaded'); if (alreadyDownloaded) { setSelectedModelId(alreadyDownloaded.id); } else { - const recommended = response.data.find((m: LocalModelResponse) => m.recommended); + const recommended = models.find((m: LocalModelResponse) => m.recommended); if (recommended) setSelectedModelId(recommended.id); } } @@ -165,7 +163,7 @@ export default function LocalModelPicker({ onConfigured }: LocalModelPickerProps } try { - await downloadHfModel({ body: { spec: model.id }, throwOnError: true }); + await downloadHfModel({ spec: model.id }); } catch (error) { console.error('Failed to start download:', error); setErrorMessage(intl.formatMessage(i18n.failedToStartDownload)); @@ -176,24 +174,27 @@ export default function LocalModelPicker({ onConfigured }: LocalModelPickerProps pollRef.current = setInterval(async () => { try { - const response = await getLocalModelDownloadProgress({ - path: { model_id: modelId }, - throwOnError: true, - }); - if (response.data) { - setDownloadProgress(response.data); - if (response.data.status === 'completed') { - cleanup(); - finishSetup(modelId); - } else if (response.data.status === 'failed') { - cleanup(); - setErrorMessage(response.data.error || 'Download failed.'); - trackOnboardingSetupFailed(LOCAL_PROVIDER, response.data.error || 'download_failed'); - setPhase('error'); - } else if (response.data.status === 'cancelled') { - cleanup(); - setPhase('select'); - } + const progress = await getLocalModelDownloadProgress(modelId); + if (!progress) { + cleanup(); + setErrorMessage(intl.formatMessage(i18n.lostConnection)); + trackOnboardingSetupFailed(LOCAL_PROVIDER, 'progress_missing'); + setPhase('error'); + return; + } + + setDownloadProgress(progress); + if (progress.status === 'completed') { + cleanup(); + finishSetup(modelId); + } else if (progress.status === 'failed') { + cleanup(); + setErrorMessage(progress.error || 'Download failed.'); + trackOnboardingSetupFailed(LOCAL_PROVIDER, progress.error || 'download_failed'); + setPhase('error'); + } else if (progress.status === 'cancelled') { + cleanup(); + setPhase('select'); } } catch { cleanup(); @@ -208,7 +209,7 @@ export default function LocalModelPicker({ onConfigured }: LocalModelPickerProps if (phase === 'downloading' && selectedModelId) { cleanup(); try { - await cancelLocalModelDownload({ path: { model_id: selectedModelId } }); + await cancelLocalModelDownload(selectedModelId); } catch { // best-effort } @@ -296,7 +297,7 @@ export default function LocalModelPicker({ onConfigured }: LocalModelPickerProps )}

- {formatSize(recommended.size_bytes)} + {formatSize(recommended.sizeBytes)}

@@ -350,7 +351,7 @@ export default function LocalModelPicker({ onConfigured }: LocalModelPickerProps {model.id} - {formatSize(model.size_bytes)} + {formatSize(model.sizeBytes)} {model.status.state === 'Downloaded' && ( @@ -375,7 +376,7 @@ export default function LocalModelPicker({ onConfigured }: LocalModelPickerProps {selectedModel?.status.state === 'Downloaded' ? intl.formatMessage(i18n.useModel, { modelId: selectedModel.id }) : selectedModel - ? intl.formatMessage(i18n.downloadModel, { modelId: selectedModel.id, size: formatSize(selectedModel.size_bytes) }) + ? intl.formatMessage(i18n.downloadModel, { modelId: selectedModel.id, size: formatSize(selectedModel.sizeBytes) }) : intl.formatMessage(i18n.selectModel)} @@ -394,30 +395,30 @@ export default function LocalModelPicker({ onConfigured }: LocalModelPickerProps
- {formatBytes(downloadProgress.bytes_downloaded)} of{' '} - {formatBytes(downloadProgress.total_bytes)} + {formatBytes(downloadProgress.bytesDownloaded)} of{' '} + {formatBytes(downloadProgress.totalBytes)} - {downloadProgress.progress_percent.toFixed(0)}% + {downloadProgress.progressPercent.toFixed(0)}%
- {downloadProgress.speed_bps ? ( - {formatBytes(downloadProgress.speed_bps)}/s + {downloadProgress.speedBps ? ( + {formatBytes(downloadProgress.speedBps)}/s ) : ( )} - {downloadProgress.eta_seconds != null && downloadProgress.eta_seconds > 0 && ( + {downloadProgress.etaSeconds != null && downloadProgress.etaSeconds > 0 && ( ~ - {downloadProgress.eta_seconds < 60 - ? `${Math.round(downloadProgress.eta_seconds)}s` - : `${Math.round(downloadProgress.eta_seconds / 60)}m`}{' '} + {downloadProgress.etaSeconds < 60 + ? `${Math.round(downloadProgress.etaSeconds)}s` + : `${Math.round(downloadProgress.etaSeconds / 60)}m`}{' '} remaining )} diff --git a/ui/desktop/src/components/settings/localInference/HuggingFaceModelSearch.tsx b/ui/desktop/src/components/settings/localInference/HuggingFaceModelSearch.tsx index e37f4f26068a..466da9401a7d 100644 --- a/ui/desktop/src/components/settings/localInference/HuggingFaceModelSearch.tsx +++ b/ui/desktop/src/components/settings/localInference/HuggingFaceModelSearch.tsx @@ -17,7 +17,8 @@ import { type DownloadModelRequest, type HfModelInfo, type HfModelVariant, -} from '../../../api'; + type RepoVariantsResponse, +} from '../../../acp/local-inference'; import { defineMessages, useIntl } from '../../../i18n'; const i18n = defineMessages({ @@ -127,61 +128,43 @@ export const HuggingFaceModelSearch = ({ setSearching(true); setError(null); try { - const response = await searchHfModels({ - query: { q, limit: 20 }, - }); - if (response.data) { - // Pre-fetch variants for all results and filter out repos with no compatible local variants - const modelsWithVariants = await Promise.all( - response.data.map(async (model) => { - try { - const [author, repo] = model.repo_id.split('/'); - const filesResponse = await getRepoFiles({ path: { author, repo } }); - if (filesResponse.data && filesResponse.data.variants.length > 0) { - return { model, data: filesResponse.data }; - } - } catch { - // Skip repos we can't fetch + const models = await searchHfModels(q, 20); + const modelsWithVariants = await Promise.all( + models.map(async (model) => { + try { + const repoData = await getRepoFiles(model.repoId); + if (repoData.variants.length > 0) { + return { model, data: repoData }; } - return null; - }) - ); - - const validResults = modelsWithVariants.filter(Boolean) as { - model: HfModelInfo; - data: { - variants: HfModelVariant[]; - recommended_index?: number | null; - available_memory_bytes: number; - downloaded_quants: string[]; - downloaded_variants: string[]; - }; - }[]; - - setResults(validResults.map((r) => r.model)); - setRepoData((prev) => { - const next = { ...prev }; - for (const r of validResults) { - next[r.model.repo_id] = { - variants: r.data.variants, - recommendedIndex: r.data.recommended_index ?? null, - availableMemoryBytes: r.data.available_memory_bytes, - downloadedQuants: new Set(r.data.downloaded_quants), - downloadedVariants: new Set(r.data.downloaded_variants), - }; + } catch { + // Skip repos we can't fetch } - return next; - }); + return null; + }) + ); - if (validResults.length === 0) { - setError(intl.formatMessage(i18n.noGgufModels)); + const validResults = modelsWithVariants.filter(Boolean) as { + model: HfModelInfo; + data: RepoVariantsResponse; + }[]; + + setResults(validResults.map((r) => r.model)); + setRepoData((prev) => { + const next = { ...prev }; + for (const r of validResults) { + next[r.model.repoId] = { + variants: r.data.variants, + recommendedIndex: r.data.recommendedIndex ?? null, + availableMemoryBytes: r.data.availableMemoryBytes, + downloadedQuants: new Set(r.data.downloadedQuants), + downloadedVariants: new Set(r.data.downloadedVariants), + }; } - } else { - console.error('Search response:', response); - const errMsg = response.error - ? intl.formatMessage(i18n.searchError, { details: JSON.stringify(response.error) }) - : intl.formatMessage(i18n.searchNoData); - setError(errMsg); + return next; + }); + + if (validResults.length === 0) { + setError(intl.formatMessage(i18n.noGgufModels)); } } catch (e) { console.error('Search failed:', e); @@ -209,22 +192,17 @@ export const HuggingFaceModelSearch = ({ if (!repoData[repoId]?.variants.length) { setLoadingFiles((prev) => new Set(prev).add(repoId)); try { - const [author, repo] = repoId.split('/'); - const response = await getRepoFiles({ - path: { author, repo }, - }); - if (response.data) { - setRepoData((prev) => ({ - ...prev, - [repoId]: { - variants: response.data!.variants, - recommendedIndex: response.data!.recommended_index ?? null, - availableMemoryBytes: response.data!.available_memory_bytes, - downloadedQuants: new Set(response.data!.downloaded_quants), - downloadedVariants: new Set(response.data!.downloaded_variants), - }, - })); - } + const response = await getRepoFiles(repoId); + setRepoData((prev) => ({ + ...prev, + [repoId]: { + variants: response.variants, + recommendedIndex: response.recommendedIndex ?? null, + availableMemoryBytes: response.availableMemoryBytes, + downloadedQuants: new Set(response.downloadedQuants), + downloadedVariants: new Set(response.downloadedVariants), + }, + })); } catch (e) { console.error('Failed to fetch repo files:', e); } finally { @@ -238,20 +216,16 @@ export const HuggingFaceModelSearch = ({ }; const startDownload = async (repoId: string, variant: HfModelVariant) => { - const downloadKey = variant.download_id; + const downloadKey = variant.downloadId; const request: DownloadModelRequest = { spec: repoId, - backend_id: variant.backend_id, - variant_id: variant.variant_id, + backendId: variant.backendId, + variantId: variant.variantId, }; setDownloading((prev) => new Set(prev).add(downloadKey)); try { - const response = await downloadHfModel({ - body: request, - }); - if (response.data) { - onDownloadStarted(response.data, request); - } + const modelId = await downloadHfModel(request); + onDownloadStarted(modelId, request); } catch (e) { console.error('Download failed:', e); } finally { @@ -289,8 +263,8 @@ export const HuggingFaceModelSearch = ({ {results.length > 0 && (
{results.map((model) => { - const isExpanded = expandedRepo === model.repo_id; - const data = repoData[model.repo_id]; + const isExpanded = expandedRepo === model.repoId; + const data = repoData[model.repoId]; const variants = data?.variants || []; const recommendedIndex = data?.recommendedIndex ?? null; const availableMemory = data?.availableMemoryBytes ?? 0; @@ -298,15 +272,15 @@ export const HuggingFaceModelSearch = ({ const downloadedVariants = data?.downloadedVariants ?? new Set(); return ( -
+