From c239ae004239e333e82a001e335008c5ac55a2c2 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Wed, 17 Jun 2026 16:42:38 -0400 Subject: [PATCH] Split provider inventory out of providers (#9826) --- .../goose/src/config/declarative_providers.rs | 5 +- crates/goose/src/providers/acp_tooling.rs | 7 +- crates/goose/src/providers/amp_acp.rs | 18 +- crates/goose/src/providers/anthropic.rs | 28 --- crates/goose/src/providers/base.rs | 31 +-- crates/goose/src/providers/chatgpt_codex.rs | 15 +- crates/goose/src/providers/claude_acp.rs | 18 +- crates/goose/src/providers/codex_acp.rs | 16 +- crates/goose/src/providers/copilot_acp.rs | 18 +- crates/goose/src/providers/databricks.rs | 4 - crates/goose/src/providers/databricks_v2.rs | 4 - crates/goose/src/providers/google.rs | 24 +-- crates/goose/src/providers/huggingface.rs | 15 -- crates/goose/src/providers/init.rs | 72 +++++-- crates/goose/src/providers/inventory/mod.rs | 8 + .../src/providers/inventory/registrations.rs | 194 ++++++++++++++++++ .../goose/src/providers/inventory/resolver.rs | 92 +++++++++ crates/goose/src/providers/ollama.rs | 25 +-- crates/goose/src/providers/openai.rs | 57 +---- crates/goose/src/providers/pi_acp.rs | 18 +- .../goose/src/providers/provider_registry.rs | 31 +-- crates/goose/src/providers/xai_oauth.rs | 13 +- 22 files changed, 404 insertions(+), 309 deletions(-) create mode 100644 crates/goose/src/providers/inventory/registrations.rs create mode 100644 crates/goose/src/providers/inventory/resolver.rs diff --git a/crates/goose/src/config/declarative_providers.rs b/crates/goose/src/config/declarative_providers.rs index a3a8fdc10..e391ecec0 100644 --- a/crates/goose/src/config/declarative_providers.rs +++ b/crates/goose/src/config/declarative_providers.rs @@ -1,8 +1,9 @@ use crate::config::paths::Paths; use crate::config::Config; use crate::providers::anthropic::AnthropicProvider; -use crate::providers::base::{ModelInfo, ProviderDef, ProviderType}; +use crate::providers::base::{ModelInfo, ProviderType}; use crate::providers::huggingface::HuggingFaceProvider; +use crate::providers::huggingface_auth; use crate::providers::inventory::declarative_inventory_identity; use crate::providers::ollama::OllamaProvider; use crate::providers::openai::OpenAiProvider; @@ -665,7 +666,7 @@ fn huggingface_declarative_inventory_configured(config: &DeclarativeProviderConf huggingface_declarative_inventory_configured_from_sources( config, |key| Config::global().get_secret::(key).is_ok(), - HuggingFaceProvider::inventory_configured, + || huggingface_auth::has_configured_token().unwrap_or(false), ) } diff --git a/crates/goose/src/providers/acp_tooling.rs b/crates/goose/src/providers/acp_tooling.rs index 91c9351b6..ef69d8d96 100644 --- a/crates/goose/src/providers/acp_tooling.rs +++ b/crates/goose/src/providers/acp_tooling.rs @@ -1,5 +1,4 @@ use crate::config::search_path::SearchPaths; -use crate::providers::inventory::InventoryIdentityInput; use anyhow::Result; use std::path::PathBuf; @@ -7,10 +6,8 @@ pub fn acp_adapter_installed(command: &str) -> bool { resolve_acp_command(command).is_ok() } -pub fn acp_inventory_identity(provider_id: &str, command: &str) -> Result { - let resolved_command = resolve_acp_command(command)?; - Ok(InventoryIdentityInput::new(provider_id, provider_id) - .with_public("command", resolved_command.display().to_string())) +pub fn resolved_acp_command(command: &str) -> Result { + resolve_acp_command(command) } fn resolve_acp_command(command: &str) -> Result { diff --git a/crates/goose/src/providers/amp_acp.rs b/crates/goose/src/providers/amp_acp.rs index 96d4f3c40..cfeead29d 100644 --- a/crates/goose/src/providers/amp_acp.rs +++ b/crates/goose/src/providers/amp_acp.rs @@ -9,13 +9,11 @@ use crate::acp::{ use crate::config::search_path::SearchPaths; use crate::config::{Config, GooseMode}; use crate::model::ModelConfig; -use crate::providers::acp_tooling::{acp_adapter_installed, acp_inventory_identity}; use crate::providers::base::{current_working_dir, ProviderDef, ProviderMetadata}; -use crate::providers::inventory::InventoryIdentityInput; -const AMP_ACP_PROVIDER_NAME: &str = "amp-acp"; +pub(crate) const AMP_ACP_PROVIDER_NAME: &str = "amp-acp"; const AMP_ACP_DOC_URL: &str = "https://ampcode.com"; -const AMP_ACP_BINARY: &str = "amp-acp"; +pub(crate) const AMP_ACP_BINARY: &str = "amp-acp"; pub struct AmpAcpProvider; @@ -84,16 +82,4 @@ impl ProviderDef for AmpAcpProvider { AcpProvider::connect(metadata.name, model, goose_mode, provider_config).await }) } - - fn supports_inventory_refresh() -> bool { - false - } - - fn inventory_identity() -> Result { - acp_inventory_identity(AMP_ACP_PROVIDER_NAME, AMP_ACP_BINARY) - } - - fn inventory_configured() -> bool { - acp_adapter_installed(AMP_ACP_BINARY) - } } diff --git a/crates/goose/src/providers/anthropic.rs b/crates/goose/src/providers/anthropic.rs index d5a4a607f..f8494188c 100644 --- a/crates/goose/src/providers/anthropic.rs +++ b/crates/goose/src/providers/anthropic.rs @@ -15,7 +15,6 @@ use super::formats::anthropic::{ create_request_with_options_for_provider, response_to_streaming_message, thinking_type, AnthropicFormatOptions, ThinkingType, ANTHROPIC_PROVIDER_NAME, }; -use super::inventory::{config_secret_value, serialize_string_map, InventoryIdentityInput}; use super::openai_compatible::handle_status; use super::openai_compatible::map_http_error_to_provider_error; use super::retry::ProviderRetry; @@ -267,33 +266,6 @@ impl ProviderDef for AnthropicProvider { ) -> BoxFuture<'static, Result> { Box::pin(Self::from_env(model)) } - - fn supports_inventory_refresh() -> bool { - true - } - - fn inventory_identity() -> Result { - let config = crate::config::Config::global(); - let mut identity = - InventoryIdentityInput::new(ANTHROPIC_PROVIDER_NAME, ANTHROPIC_PROVIDER_NAME) - .with_public( - "host", - config - .get_param::("ANTHROPIC_HOST") - .unwrap_or_else(|_| "https://api.anthropic.com".to_string()), - ); - - if let Some(api_key) = config_secret_value(config, "ANTHROPIC_API_KEY") { - identity = identity.with_secret("api_key", api_key); - } - if let Ok(headers) = config - .get_secret::>("ANTHROPIC_CUSTOM_HEADERS") - { - identity = identity.with_secret("headers", serialize_string_map(&headers)?); - } - - Ok(identity) - } } #[async_trait] diff --git a/crates/goose/src/providers/base.rs b/crates/goose/src/providers/base.rs index cf547aeb3..c6db8dec9 100644 --- a/crates/goose/src/providers/base.rs +++ b/crates/goose/src/providers/base.rs @@ -15,10 +15,9 @@ use serde::{Deserialize, Serialize}; pub const DEFAULT_PROVIDER_TIMEOUT_SECS: u64 = 600; use super::canonical::{map_to_canonical_model, CanonicalModelRegistry}; -use super::inventory::{default_inventory_identity, InventoryIdentityInput}; use super::retry::RetryConfig; use crate::config::base::ConfigValue; -use crate::config::{Config, ExtensionConfig, GooseMode}; +use crate::config::{ExtensionConfig, GooseMode}; use crate::conversation::message::{Message, MessageContent}; use crate::conversation::Conversation; use crate::model::ModelConfig; @@ -421,34 +420,6 @@ pub trait ProviderDef: Send + Sync { // Non-subprocess providers can rely on the default because cwd is irrelevant. Self::from_env(model, extensions) } - - fn supports_inventory_refresh() -> bool - where - Self: Sized, - { - false - } - - fn inventory_identity() -> Result - where - Self: Sized, - { - let metadata = Self::metadata(); - Ok(default_inventory_identity( - &metadata.name, - &metadata.name, - &metadata.config_keys, - Config::global(), - )) - } - - fn inventory_configured() -> bool - where - Self: Sized, - { - let metadata = Self::metadata(); - super::inventory::default_inventory_configured(&metadata.config_keys, Config::global()) - } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] diff --git a/crates/goose/src/providers/chatgpt_codex.rs b/crates/goose/src/providers/chatgpt_codex.rs index 4ce83f1f4..8d8731ba3 100644 --- a/crates/goose/src/providers/chatgpt_codex.rs +++ b/crates/goose/src/providers/chatgpt_codex.rs @@ -319,7 +319,7 @@ struct TokenData { } #[derive(Debug, Clone)] -struct TokenCache { +pub(crate) struct TokenCache { cache_path: PathBuf, } @@ -328,7 +328,7 @@ fn get_cache_path() -> PathBuf { } impl TokenCache { - fn new() -> Self { + pub(crate) fn new() -> Self { let cache_path = get_cache_path(); if let Some(parent) = cache_path.parent() { let _ = std::fs::create_dir_all(parent); @@ -343,6 +343,9 @@ impl TokenCache { None } } + pub(crate) fn has_token(&self) -> bool { + self.load().is_some() + } fn save(&self, token_data: &TokenData) -> Result<()> { if let Some(parent) = self.cache_path.parent() { @@ -976,10 +979,6 @@ impl ProviderDef for ChatGptCodexProvider { ) -> BoxFuture<'static, Result> { Box::pin(Self::from_env(model)) } - - fn inventory_configured() -> bool { - TokenCache::new().load().is_some() - } } #[async_trait] @@ -1098,7 +1097,7 @@ mod tests { let _guard = env_lock::lock_env([("GOOSE_PATH_ROOT", Some(root_path.as_str()))]); TokenCache::new().clear(); - assert!(!ChatGptCodexProvider::inventory_configured()); + assert!(!TokenCache::new().has_token()); TokenCache::new() .save(&TokenData { @@ -1110,7 +1109,7 @@ mod tests { }) .unwrap(); - assert!(ChatGptCodexProvider::inventory_configured()); + assert!(TokenCache::new().has_token()); } #[test_case( diff --git a/crates/goose/src/providers/claude_acp.rs b/crates/goose/src/providers/claude_acp.rs index 93f31976c..d4c6c11c7 100644 --- a/crates/goose/src/providers/claude_acp.rs +++ b/crates/goose/src/providers/claude_acp.rs @@ -9,13 +9,11 @@ use crate::acp::{ use crate::config::search_path::SearchPaths; use crate::config::{Config, GooseMode}; use crate::model::ModelConfig; -use crate::providers::acp_tooling::{acp_adapter_installed, acp_inventory_identity}; use crate::providers::base::{current_working_dir, ProviderDef, ProviderMetadata}; -use crate::providers::inventory::InventoryIdentityInput; -const CLAUDE_ACP_PROVIDER_NAME: &str = "claude-acp"; +pub(crate) const CLAUDE_ACP_PROVIDER_NAME: &str = "claude-acp"; const CLAUDE_ACP_DOC_URL: &str = "https://github.com/agentclientprotocol/claude-agent-acp"; -const CLAUDE_ACP_BINARY: &str = "claude-agent-acp"; +pub(crate) const CLAUDE_ACP_BINARY: &str = "claude-agent-acp"; pub struct ClaudeAcpProvider; @@ -88,16 +86,4 @@ impl ProviderDef for ClaudeAcpProvider { AcpProvider::connect(metadata.name, model, goose_mode, provider_config).await }) } - - fn supports_inventory_refresh() -> bool { - true - } - - fn inventory_identity() -> Result { - acp_inventory_identity(CLAUDE_ACP_PROVIDER_NAME, CLAUDE_ACP_BINARY) - } - - fn inventory_configured() -> bool { - acp_adapter_installed(CLAUDE_ACP_BINARY) - } } diff --git a/crates/goose/src/providers/codex_acp.rs b/crates/goose/src/providers/codex_acp.rs index 9447e67b8..e84142a84 100644 --- a/crates/goose/src/providers/codex_acp.rs +++ b/crates/goose/src/providers/codex_acp.rs @@ -9,11 +9,9 @@ use crate::acp::{ use crate::config::search_path::SearchPaths; use crate::config::{Config, GooseMode}; use crate::model::ModelConfig; -use crate::providers::acp_tooling::{acp_adapter_installed, acp_inventory_identity}; use crate::providers::base::{current_working_dir, ProviderDef, ProviderMetadata}; -use crate::providers::inventory::InventoryIdentityInput; -const CODEX_ACP_PROVIDER_NAME: &str = "codex-acp"; +pub(crate) const CODEX_ACP_PROVIDER_NAME: &str = "codex-acp"; const CODEX_ACP_DOC_URL: &str = "https://github.com/zed-industries/codex-acp"; pub struct CodexAcpProvider; @@ -107,18 +105,6 @@ impl ProviderDef for CodexAcpProvider { AcpProvider::connect(metadata.name, model, goose_mode, provider_config).await }) } - - fn supports_inventory_refresh() -> bool { - true - } - - fn inventory_identity() -> Result { - acp_inventory_identity(CODEX_ACP_PROVIDER_NAME, CODEX_ACP_PROVIDER_NAME) - } - - fn inventory_configured() -> bool { - acp_adapter_installed(CODEX_ACP_PROVIDER_NAME) - } } // Codex sandbox scope determines what needs approval: operations within the diff --git a/crates/goose/src/providers/copilot_acp.rs b/crates/goose/src/providers/copilot_acp.rs index 1c0d05a2a..0b9336cde 100644 --- a/crates/goose/src/providers/copilot_acp.rs +++ b/crates/goose/src/providers/copilot_acp.rs @@ -9,13 +9,11 @@ use crate::acp::{ use crate::config::search_path::SearchPaths; use crate::config::{Config, GooseMode}; use crate::model::ModelConfig; -use crate::providers::acp_tooling::{acp_adapter_installed, acp_inventory_identity}; use crate::providers::base::{current_working_dir, ProviderDef, ProviderMetadata}; -use crate::providers::inventory::InventoryIdentityInput; -const COPILOT_ACP_PROVIDER_NAME: &str = "copilot-acp"; +pub(crate) const COPILOT_ACP_PROVIDER_NAME: &str = "copilot-acp"; const COPILOT_ACP_DOC_URL: &str = "https://github.com/github/copilot-cli"; -const COPILOT_ACP_BINARY: &str = "copilot"; +pub(crate) const COPILOT_ACP_BINARY: &str = "copilot"; const MODE_AGENT: &str = "https://agentclientprotocol.com/protocol/session-modes#agent"; const MODE_PLAN: &str = "https://agentclientprotocol.com/protocol/session-modes#plan"; @@ -94,16 +92,4 @@ impl ProviderDef for CopilotAcpProvider { AcpProvider::connect(metadata.name, model, goose_mode, provider_config).await }) } - - fn supports_inventory_refresh() -> bool { - true - } - - fn inventory_identity() -> Result { - acp_inventory_identity(COPILOT_ACP_PROVIDER_NAME, COPILOT_ACP_BINARY) - } - - fn inventory_configured() -> bool { - acp_adapter_installed(COPILOT_ACP_BINARY) - } } diff --git a/crates/goose/src/providers/databricks.rs b/crates/goose/src/providers/databricks.rs index 6e1304237..bd5ed4e8b 100644 --- a/crates/goose/src/providers/databricks.rs +++ b/crates/goose/src/providers/databricks.rs @@ -613,10 +613,6 @@ impl ProviderDef for DatabricksProvider { ) -> BoxFuture<'static, Result> { Box::pin(Self::from_env(model)) } - - fn supports_inventory_refresh() -> bool { - true - } } #[async_trait] diff --git a/crates/goose/src/providers/databricks_v2.rs b/crates/goose/src/providers/databricks_v2.rs index 9d0bc2766..9f0ae6b4d 100644 --- a/crates/goose/src/providers/databricks_v2.rs +++ b/crates/goose/src/providers/databricks_v2.rs @@ -369,10 +369,6 @@ impl ProviderDef for DatabricksV2Provider { ) -> BoxFuture<'static, Result> { Box::pin(Self::from_env(model)) } - - fn supports_inventory_refresh() -> bool { - true - } } #[async_trait] diff --git a/crates/goose/src/providers/google.rs b/crates/goose/src/providers/google.rs index 590215bc8..199f9c445 100644 --- a/crates/goose/src/providers/google.rs +++ b/crates/goose/src/providers/google.rs @@ -9,7 +9,6 @@ use goose_providers::errors::ProviderError; use crate::model::ModelConfig; use crate::providers::base::{ConfigKey, Provider, ProviderDef, ProviderMetadata}; use crate::providers::formats::google::{create_request, response_to_streaming_message}; -use crate::providers::inventory::{config_secret_value, InventoryIdentityInput}; use anyhow::Result; use async_stream::try_stream; use async_trait::async_trait; @@ -23,7 +22,7 @@ use tokio_stream::StreamExt; use tokio_util::codec::{FramedRead, LinesCodec}; use tokio_util::io::StreamReader; -const GOOGLE_PROVIDER_NAME: &str = "google"; +pub(crate) const GOOGLE_PROVIDER_NAME: &str = "google"; pub const GOOGLE_API_HOST: &str = "https://generativelanguage.googleapis.com"; pub const GOOGLE_DEFAULT_MODEL: &str = "gemini-2.5-pro"; pub const GOOGLE_DEFAULT_FAST_MODEL: &str = "gemini-2.5-flash"; @@ -136,27 +135,6 @@ impl ProviderDef for GoogleProvider { ) -> BoxFuture<'static, Result> { Box::pin(Self::from_env(model)) } - - fn supports_inventory_refresh() -> bool { - true - } - - fn inventory_identity() -> Result { - let config = crate::config::Config::global(); - let mut identity = InventoryIdentityInput::new(GOOGLE_PROVIDER_NAME, GOOGLE_PROVIDER_NAME) - .with_public( - "host", - config - .get_param::("GOOGLE_HOST") - .unwrap_or_else(|_| GOOGLE_API_HOST.to_string()), - ); - - if let Some(api_key) = config_secret_value(config, "GOOGLE_API_KEY") { - identity = identity.with_secret("api_key", api_key); - } - - Ok(identity) - } } #[async_trait] diff --git a/crates/goose/src/providers/huggingface.rs b/crates/goose/src/providers/huggingface.rs index 5416b00e5..166f92881 100644 --- a/crates/goose/src/providers/huggingface.rs +++ b/crates/goose/src/providers/huggingface.rs @@ -4,7 +4,6 @@ use super::base::{ DEFAULT_PROVIDER_TIMEOUT_SECS, }; use super::huggingface_auth; -use super::inventory::{default_inventory_identity, InventoryIdentityInput}; use super::openai_compatible::OpenAiCompatibleProvider; use crate::config::declarative_providers::DeclarativeProviderConfig; use crate::config::{Config, ConfigError}; @@ -229,20 +228,6 @@ impl ProviderDef for HuggingFaceProvider { }) }) } - - fn inventory_identity() -> Result { - let metadata = Self::metadata(); - Ok(default_inventory_identity( - &metadata.name, - &metadata.name, - &metadata.config_keys, - Config::global(), - )) - } - - fn inventory_configured() -> bool { - huggingface_auth::has_configured_token().unwrap_or(false) - } } fn missing_token_error() -> anyhow::Error { diff --git a/crates/goose/src/providers/init.rs b/crates/goose/src/providers/init.rs index 06d5773de..cc329560b 100644 --- a/crates/goose/src/providers/init.rs +++ b/crates/goose/src/providers/init.rs @@ -55,42 +55,86 @@ static REGISTRY: OnceCell> = OnceCell::const_new(); async fn init_registry() -> RwLock { let mut registry = ProviderRegistry::new().with_providers(|registry| { - registry.register::(false); - registry.register::(true); + use super::inventory::registrations; + + registry.register_with_inventory::( + false, + Some(registrations::amp_acp_inventory()), + ); + registry.register_with_inventory::( + true, + Some(registrations::anthropic_inventory()), + ); registry.register::(false); registry.register::(false); #[cfg(feature = "aws-providers")] registry.register::(false); #[cfg(feature = "local-inference")] registry.register::(false); - registry.register::(true); - registry.register::(false); + registry.register_with_inventory::( + true, + Some(registrations::chatgpt_codex_inventory()), + ); + registry.register_with_inventory::( + false, + Some(registrations::claude_acp_inventory()), + ); registry.register::(true); - registry.register::(false); - registry.register::(false); + registry.register_with_inventory::( + false, + Some(registrations::codex_acp_inventory()), + ); + registry.register_with_inventory::( + false, + Some(registrations::copilot_acp_inventory()), + ); registry.register::(true); registry.register::(false); - registry.register::(true); - registry.register::(false); + registry.register_with_inventory::( + true, + Some(registrations::refresh_only()), + ); + registry.register_with_inventory::( + false, + Some(registrations::refresh_only()), + ); registry.register::(false); registry.register::(false); registry.register::(true); registry.register::(false); - registry.register::(true); - registry.register::(true); + registry.register_with_inventory::( + true, + Some(registrations::google_inventory()), + ); + registry.register_with_inventory::( + true, + Some(registrations::huggingface_inventory()), + ); registry.register::(true); registry.register::(false); registry.register::(true); - registry.register::(true); - registry.register::(true); + registry.register_with_inventory::( + true, + Some(registrations::ollama_inventory()), + ); + registry.register_with_inventory::( + true, + Some(registrations::openai_inventory()), + ); registry.register::(true); - registry.register::(false); + registry.register_with_inventory::( + false, + Some(registrations::pi_acp_inventory()), + ); #[cfg(feature = "aws-providers")] registry.register::(false); registry.register::(false); registry.register::(true); registry.register::(false); - registry.register::(true); + registry.register_with_inventory::( + true, + Some(registrations::xai_oauth_inventory()), + ); }); // Register cleanup functions for providers with cached state registry.set_cleanup( diff --git a/crates/goose/src/providers/inventory/mod.rs b/crates/goose/src/providers/inventory/mod.rs index 589f2ea3e..28d9ca73f 100644 --- a/crates/goose/src/providers/inventory/mod.rs +++ b/crates/goose/src/providers/inventory/mod.rs @@ -1,3 +1,11 @@ +pub mod registrations; +mod resolver; + +pub use resolver::{ + default_inventory_identity_resolver, InventoryConfiguredResolver, InventoryIdentityResolver, + InventoryRegistration, InventoryResolvers, +}; + use super::base::{ConfigKey, ModelInfo, Provider, ProviderType}; use super::canonical::{map_provider_name, map_to_canonical_model, CanonicalModelRegistry}; use super::catalog::ProviderSetupCategory; diff --git a/crates/goose/src/providers/inventory/registrations.rs b/crates/goose/src/providers/inventory/registrations.rs new file mode 100644 index 000000000..f85c2639a --- /dev/null +++ b/crates/goose/src/providers/inventory/registrations.rs @@ -0,0 +1,194 @@ +use super::{ + config_secret_value, default_inventory_identity, default_inventory_identity_resolver, + serialize_string_map, InventoryIdentityInput, InventoryRegistration, +}; +use crate::config::Config; +use crate::providers::acp_tooling::{acp_adapter_installed, resolved_acp_command}; +use crate::providers::amp_acp::{AMP_ACP_BINARY, AMP_ACP_PROVIDER_NAME}; +use crate::providers::base::ProviderDef; +use crate::providers::chatgpt_codex::TokenCache as ChatGptCodexTokenCache; +use crate::providers::claude_acp::{CLAUDE_ACP_BINARY, CLAUDE_ACP_PROVIDER_NAME}; +use crate::providers::codex_acp::CODEX_ACP_PROVIDER_NAME; +use crate::providers::copilot_acp::{COPILOT_ACP_BINARY, COPILOT_ACP_PROVIDER_NAME}; +use crate::providers::formats::anthropic::ANTHROPIC_PROVIDER_NAME; +use crate::providers::google::{GOOGLE_API_HOST, GOOGLE_PROVIDER_NAME}; +use crate::providers::huggingface::HuggingFaceProvider; +use crate::providers::huggingface_auth; +use crate::providers::ollama::{ollama_host_configured, OLLAMA_PROVIDER_NAME}; +use crate::providers::openai::{OPEN_AI_DEFAULT_BASE_PATH, OPEN_AI_PROVIDER_NAME}; +use crate::providers::pi_acp::{PI_ACP_BINARY, PI_ACP_PROVIDER_NAME}; +use crate::providers::xai_oauth::TokenCache as XaiOAuthTokenCache; + +pub fn openai_inventory() -> InventoryRegistration { + InventoryRegistration::new(true, || { + let config = Config::global(); + let mut identity = + InventoryIdentityInput::new(OPEN_AI_PROVIDER_NAME, OPEN_AI_PROVIDER_NAME) + .with_public( + "host", + config + .get_param::("OPENAI_HOST") + .unwrap_or_else(|_| "https://api.openai.com".to_string()), + ) + .with_public( + "base_path", + config + .get_param::("OPENAI_BASE_PATH") + .unwrap_or_else(|_| OPEN_AI_DEFAULT_BASE_PATH.to_string()), + ); + + if let Ok(organization) = config.get_param::("OPENAI_ORGANIZATION") { + identity = identity.with_public("organization", organization); + } + if let Ok(project) = config.get_param::("OPENAI_PROJECT") { + identity = identity.with_public("project", project); + } + if let Some(api_key) = config_secret_value(config, "OPENAI_API_KEY") { + identity = identity.with_secret("api_key", api_key); + } + if let Some(custom_headers) = config_secret_value(config, "OPENAI_CUSTOM_HEADERS") { + identity = identity.with_secret("custom_headers", custom_headers); + } + + Ok(identity) + }) + .with_configured(|| { + let config = Config::global(); + if let Ok(host) = config.get_param::("OPENAI_HOST") { + if host != "https://api.openai.com" { + return true; + } + } + config + .get_secret::("OPENAI_API_KEY") + .is_ok() + }) +} + +pub fn anthropic_inventory() -> InventoryRegistration { + InventoryRegistration::new(true, || { + let config = Config::global(); + let mut identity = + InventoryIdentityInput::new(ANTHROPIC_PROVIDER_NAME, ANTHROPIC_PROVIDER_NAME) + .with_public( + "host", + config + .get_param::("ANTHROPIC_HOST") + .unwrap_or_else(|_| "https://api.anthropic.com".to_string()), + ); + + if let Some(api_key) = config_secret_value(config, "ANTHROPIC_API_KEY") { + identity = identity.with_secret("api_key", api_key); + } + if let Ok(headers) = config + .get_secret::>("ANTHROPIC_CUSTOM_HEADERS") + { + identity = identity.with_secret("headers", serialize_string_map(&headers)?); + } + Ok(identity) + }) +} + +pub fn google_inventory() -> InventoryRegistration { + InventoryRegistration::new(true, || { + let config = Config::global(); + let mut identity = InventoryIdentityInput::new(GOOGLE_PROVIDER_NAME, GOOGLE_PROVIDER_NAME) + .with_public( + "host", + config + .get_param::("GOOGLE_HOST") + .unwrap_or_else(|_| GOOGLE_API_HOST.to_string()), + ); + if let Some(api_key) = config_secret_value(config, "GOOGLE_API_KEY") { + identity = identity.with_secret("api_key", api_key); + } + Ok(identity) + }) +} + +pub fn ollama_inventory() -> InventoryRegistration { + InventoryRegistration::new(true, || { + let config = Config::global(); + Ok( + InventoryIdentityInput::new(OLLAMA_PROVIDER_NAME, OLLAMA_PROVIDER_NAME).with_public( + "host", + config + .get_param::("OLLAMA_HOST") + .unwrap_or_else(|_| "http://localhost:11434".to_string()), + ), + ) + }) + .with_configured(|| ollama_host_configured(Config::global())) +} + +pub fn huggingface_inventory() -> InventoryRegistration { + InventoryRegistration::new(false, || { + let metadata = HuggingFaceProvider::metadata(); + Ok(default_inventory_identity( + &metadata.name, + &metadata.name, + &metadata.config_keys, + Config::global(), + )) + }) + .with_configured(|| huggingface_auth::has_configured_token().unwrap_or(false)) +} + +pub fn refresh_only() -> InventoryRegistration { + InventoryRegistration { + supports_refresh: true, + identity: default_inventory_identity_resolver(), + configured: None, + } +} + +pub fn chatgpt_codex_inventory() -> InventoryRegistration { + InventoryRegistration { + supports_refresh: false, + identity: default_inventory_identity_resolver(), + configured: None, + } + .with_configured(|| ChatGptCodexTokenCache::new().has_token()) +} + +pub fn xai_oauth_inventory() -> InventoryRegistration { + InventoryRegistration { + supports_refresh: false, + identity: default_inventory_identity_resolver(), + configured: None, + } + .with_configured(|| XaiOAuthTokenCache::new().has_token()) +} + +pub fn acp_inventory( + provider_id: &'static str, + command: &'static str, + supports_refresh: bool, +) -> InventoryRegistration { + InventoryRegistration::new(supports_refresh, move || { + let resolved_command = resolved_acp_command(command)?; + Ok(InventoryIdentityInput::new(provider_id, provider_id) + .with_public("command", resolved_command.display().to_string())) + }) + .with_configured(move || acp_adapter_installed(command)) +} + +pub fn amp_acp_inventory() -> InventoryRegistration { + acp_inventory(AMP_ACP_PROVIDER_NAME, AMP_ACP_BINARY, false) +} + +pub fn claude_acp_inventory() -> InventoryRegistration { + acp_inventory(CLAUDE_ACP_PROVIDER_NAME, CLAUDE_ACP_BINARY, true) +} + +pub fn codex_acp_inventory() -> InventoryRegistration { + acp_inventory(CODEX_ACP_PROVIDER_NAME, CODEX_ACP_PROVIDER_NAME, true) +} + +pub fn copilot_acp_inventory() -> InventoryRegistration { + acp_inventory(COPILOT_ACP_PROVIDER_NAME, COPILOT_ACP_BINARY, true) +} + +pub fn pi_acp_inventory() -> InventoryRegistration { + acp_inventory(PI_ACP_PROVIDER_NAME, PI_ACP_BINARY, false) +} diff --git a/crates/goose/src/providers/inventory/resolver.rs b/crates/goose/src/providers/inventory/resolver.rs new file mode 100644 index 000000000..2c7181ac1 --- /dev/null +++ b/crates/goose/src/providers/inventory/resolver.rs @@ -0,0 +1,92 @@ +use super::{default_inventory_configured, default_inventory_identity, InventoryIdentityInput}; +use crate::config::Config; +use crate::providers::base::ProviderMetadata; +use anyhow::Result; +use once_cell::sync::Lazy; +use std::sync::Arc; + +static DEFAULT_INVENTORY_IDENTITY_RESOLVER: Lazy = + Lazy::new(|| Arc::new(|| unreachable!("default inventory identity resolver marker"))); + +pub fn default_inventory_identity_resolver() -> InventoryIdentityResolver { + Arc::clone(&DEFAULT_INVENTORY_IDENTITY_RESOLVER) +} + +pub type InventoryIdentityResolver = Arc Result + Send + Sync>; +pub type InventoryConfiguredResolver = Arc bool + Send + Sync>; + +#[derive(Clone)] +pub struct InventoryRegistration { + pub supports_refresh: bool, + pub identity: InventoryIdentityResolver, + pub configured: Option, +} + +impl InventoryRegistration { + pub fn new(supports_refresh: bool, identity: G) -> Self + where + G: Fn() -> Result + Send + Sync + 'static, + { + Self { + supports_refresh, + identity: Arc::new(identity), + configured: None, + } + } + + pub fn with_configured(mut self, configured: H) -> Self + where + H: Fn() -> bool + Send + Sync + 'static, + { + self.configured = Some(Arc::new(configured)); + self + } +} + +#[derive(Clone)] +pub struct InventoryResolvers { + pub supports_refresh: bool, + pub identity: InventoryIdentityResolver, + pub configured: InventoryConfiguredResolver, +} + +impl InventoryResolvers { + pub fn for_metadata( + metadata: &ProviderMetadata, + registration: Option, + ) -> Self { + let metadata_for_identity = metadata.clone(); + let default_identity = Arc::new(move || { + Ok(default_inventory_identity( + &metadata_for_identity.name, + &metadata_for_identity.name, + &metadata_for_identity.config_keys, + Config::global(), + )) + }); + + let config_keys = metadata.config_keys.clone(); + let default_configured = + Arc::new(move || default_inventory_configured(&config_keys, Config::global())); + + match registration { + Some(registration) => Self { + supports_refresh: registration.supports_refresh, + identity: if Arc::ptr_eq( + ®istration.identity, + &default_inventory_identity_resolver(), + ) { + default_identity + } else { + registration.identity + }, + configured: registration.configured.unwrap_or(default_configured), + }, + None => Self { + supports_refresh: false, + identity: default_identity, + configured: default_configured, + }, + } + } +} diff --git a/crates/goose/src/providers/ollama.rs b/crates/goose/src/providers/ollama.rs index 75ef5e854..9cb8ce716 100644 --- a/crates/goose/src/providers/ollama.rs +++ b/crates/goose/src/providers/ollama.rs @@ -3,7 +3,6 @@ use super::base::{ ConfigKey, MessageStream, Provider, ProviderDef, ProviderMetadata, DEFAULT_PROVIDER_TIMEOUT_SECS, }; -use super::inventory::InventoryIdentityInput; use super::openai_compatible::handle_status; use super::retry::{ProviderRetry, RetryConfig}; use super::utils::RequestLog; @@ -29,7 +28,7 @@ use tokio_util::codec::{FramedRead, LinesCodec}; use tokio_util::io::StreamReader; use url::Url; -const OLLAMA_PROVIDER_NAME: &str = "ollama"; +pub(crate) const OLLAMA_PROVIDER_NAME: &str = "ollama"; pub const OLLAMA_HOST: &str = "localhost"; pub const OLLAMA_TIMEOUT: u64 = DEFAULT_PROVIDER_TIMEOUT_SECS; pub const OLLAMA_DEFAULT_PORT: u16 = 11434; @@ -127,7 +126,7 @@ fn apply_ollama_options(payload: &mut Value, model_config: &ModelConfig) { } } -fn ollama_host_configured(config: &crate::config::Config) -> bool { +pub(crate) fn ollama_host_configured(config: &crate::config::Config) -> bool { config.get_param::("OLLAMA_HOST").is_ok() } @@ -266,26 +265,6 @@ impl ProviderDef for OllamaProvider { ) -> BoxFuture<'static, Result> { Box::pin(Self::from_env(model)) } - - fn supports_inventory_refresh() -> bool { - true - } - - fn inventory_configured() -> bool { - ollama_host_configured(crate::config::Config::global()) - } - - fn inventory_identity() -> Result { - let config = crate::config::Config::global(); - Ok( - InventoryIdentityInput::new(OLLAMA_PROVIDER_NAME, OLLAMA_PROVIDER_NAME).with_public( - "host", - config - .get_param::("OLLAMA_HOST") - .unwrap_or_else(|_| OLLAMA_HOST.to_string()), - ), - ) - } } #[async_trait] diff --git a/crates/goose/src/providers/openai.rs b/crates/goose/src/providers/openai.rs index 1a28a6718..3c6b7ba6b 100644 --- a/crates/goose/src/providers/openai.rs +++ b/crates/goose/src/providers/openai.rs @@ -6,7 +6,6 @@ use super::embedding::{EmbeddingCapable, EmbeddingRequest, EmbeddingResponse}; use super::formats::openai_responses::{ create_responses_request, get_responses_usage, responses_api_to_message, ResponsesApiResponse, }; -use super::inventory::{config_secret_value, InventoryIdentityInput}; use super::openai_compatible::{ handle_response_openai_compat, handle_status, stream_openai_compat, stream_responses_compat, }; @@ -31,8 +30,8 @@ use crate::providers::base::MessageStream; use crate::providers::utils::RequestLog; use rmcp::model::Tool; -const OPEN_AI_PROVIDER_NAME: &str = "openai"; -const OPEN_AI_DEFAULT_BASE_PATH: &str = "v1/chat/completions"; +pub(crate) const OPEN_AI_PROVIDER_NAME: &str = "openai"; +pub(crate) const OPEN_AI_DEFAULT_BASE_PATH: &str = "v1/chat/completions"; const OPEN_AI_VERSIONLESS_BASE_PATH: &str = "chat/completions"; const OPEN_AI_DEFAULT_RESPONSES_PATH: &str = "v1/responses"; const OPEN_AI_DEFAULT_MODELS_PATH: &str = "v1/models"; @@ -772,58 +771,6 @@ impl ProviderDef for OpenAiProvider { ) -> BoxFuture<'static, Result> { Box::pin(Self::from_env(model)) } - - fn supports_inventory_refresh() -> bool { - true - } - - fn inventory_configured() -> bool { - let config = crate::config::Config::global(); - // If the host is explicitly set to something non-default, trust the user's - // custom setup (e.g. a local server that doesn't require an API key). - if let Ok(host) = config.get_param::("OPENAI_HOST") { - if host != "https://api.openai.com" { - return true; - } - } - // Standard OpenAI endpoint requires an API key. - config - .get_secret::("OPENAI_API_KEY") - .is_ok() - } - - fn inventory_identity() -> Result { - let config = crate::config::Config::global(); - let mut identity = - InventoryIdentityInput::new(OPEN_AI_PROVIDER_NAME, OPEN_AI_PROVIDER_NAME) - .with_public( - "host", - config - .get_param::("OPENAI_HOST") - .unwrap_or_else(|_| "https://api.openai.com".to_string()), - ) - .with_public( - "base_path", - config - .get_param::("OPENAI_BASE_PATH") - .unwrap_or_else(|_| OPEN_AI_DEFAULT_BASE_PATH.to_string()), - ); - - if let Ok(organization) = config.get_param::("OPENAI_ORGANIZATION") { - identity = identity.with_public("organization", organization); - } - if let Ok(project) = config.get_param::("OPENAI_PROJECT") { - identity = identity.with_public("project", project); - } - if let Some(api_key) = config_secret_value(config, "OPENAI_API_KEY") { - identity = identity.with_secret("api_key", api_key); - } - if let Some(custom_headers) = config_secret_value(config, "OPENAI_CUSTOM_HEADERS") { - identity = identity.with_secret("custom_headers", custom_headers); - } - - Ok(identity) - } } #[async_trait] diff --git a/crates/goose/src/providers/pi_acp.rs b/crates/goose/src/providers/pi_acp.rs index 5e36292a4..5bab316f7 100644 --- a/crates/goose/src/providers/pi_acp.rs +++ b/crates/goose/src/providers/pi_acp.rs @@ -9,13 +9,11 @@ use crate::acp::{ use crate::config::search_path::SearchPaths; use crate::config::{Config, GooseMode}; use crate::model::ModelConfig; -use crate::providers::acp_tooling::{acp_adapter_installed, acp_inventory_identity}; use crate::providers::base::{current_working_dir, ProviderDef, ProviderMetadata}; -use crate::providers::inventory::InventoryIdentityInput; -const PI_ACP_PROVIDER_NAME: &str = "pi-acp"; +pub(crate) const PI_ACP_PROVIDER_NAME: &str = "pi-acp"; const PI_ACP_DOC_URL: &str = "https://github.com/anthropics/pi"; -const PI_ACP_BINARY: &str = "pi-acp"; +pub(crate) const PI_ACP_BINARY: &str = "pi-acp"; pub struct PiAcpProvider; @@ -81,16 +79,4 @@ impl ProviderDef for PiAcpProvider { AcpProvider::connect(metadata.name, model, goose_mode, provider_config).await }) } - - fn supports_inventory_refresh() -> bool { - false - } - - fn inventory_identity() -> Result { - acp_inventory_identity(PI_ACP_PROVIDER_NAME, PI_ACP_BINARY) - } - - fn inventory_configured() -> bool { - acp_adapter_installed(PI_ACP_BINARY) - } } diff --git a/crates/goose/src/providers/provider_registry.rs b/crates/goose/src/providers/provider_registry.rs index b3da205e5..7900bc6f8 100644 --- a/crates/goose/src/providers/provider_registry.rs +++ b/crates/goose/src/providers/provider_registry.rs @@ -1,5 +1,5 @@ use super::base::{ConfigKey, ModelInfo, Provider, ProviderDef, ProviderMetadata, ProviderType}; -use super::inventory::InventoryIdentityInput; +use super::inventory::{InventoryIdentityInput, InventoryRegistration, InventoryResolvers}; use crate::config::{DeclarativeProviderConfig, ExtensionConfig}; use crate::model::ModelConfig; use anyhow::Result; @@ -20,17 +20,12 @@ pub type ProviderConstructor = Arc< pub type ProviderCleanup = Arc BoxFuture<'static, Result<()>> + Send + Sync>; -pub type ProviderInventoryIdentityResolver = - Arc Result + Send + Sync>; - -pub type ProviderInventoryConfiguredResolver = Arc bool + Send + Sync>; - #[derive(Clone)] pub struct ProviderEntry { metadata: ProviderMetadata, pub(crate) constructor: ProviderConstructor, - pub(crate) inventory_identity: ProviderInventoryIdentityResolver, - pub(crate) inventory_configured: ProviderInventoryConfiguredResolver, + pub(crate) inventory_identity: super::inventory::InventoryIdentityResolver, + pub(crate) inventory_configured: super::inventory::InventoryConfiguredResolver, pub(crate) cleanup: Option, provider_type: ProviderType, supports_inventory_refresh: bool, @@ -118,10 +113,22 @@ impl ProviderRegistry { pub fn register(&mut self, preferred: bool) where F: ProviderDef + 'static, + { + self.register_with_inventory::(preferred, None); + } + + pub fn register_with_inventory( + &mut self, + preferred: bool, + inventory_registration: Option, + ) where + F: ProviderDef + 'static, { let metadata = F::metadata(); let name = metadata.name.clone(); + let inventory = InventoryResolvers::for_metadata(&metadata, inventory_registration); + self.entries.insert( name, ProviderEntry { @@ -137,15 +144,15 @@ impl ProviderRegistry { Ok(Arc::new(provider) as Arc) }) }), - inventory_identity: Arc::new(F::inventory_identity), - inventory_configured: Arc::new(F::inventory_configured), + inventory_identity: inventory.identity, + inventory_configured: inventory.configured, cleanup: None, provider_type: if preferred { ProviderType::Preferred } else { ProviderType::Builtin }, - supports_inventory_refresh: F::supports_inventory_refresh(), + supports_inventory_refresh: inventory.supports_refresh, }, ); } @@ -203,7 +210,7 @@ impl ProviderRegistry { supports_inventory_refresh: bool, constructor: F, inventory_identity: G, - inventory_configured: Option, + inventory_configured: Option, ) where P: ProviderDef + 'static, F: Fn(ModelConfig) -> Result + Send + Sync + 'static, diff --git a/crates/goose/src/providers/xai_oauth.rs b/crates/goose/src/providers/xai_oauth.rs index 2e4e1afb0..e0badec1a 100644 --- a/crates/goose/src/providers/xai_oauth.rs +++ b/crates/goose/src/providers/xai_oauth.rs @@ -72,7 +72,7 @@ struct XaiAuthState { } impl XaiAuthState { - fn new() -> Self { + pub(crate) fn new() -> Self { Self { oauth_mutex: TokioMutex::new(()), refresh_mutex: TokioMutex::new(()), @@ -97,7 +97,7 @@ struct TokenData { } #[derive(Debug, Clone)] -struct TokenCache { +pub(crate) struct TokenCache { cache_path: PathBuf, } @@ -106,7 +106,7 @@ fn get_cache_path() -> PathBuf { } impl TokenCache { - fn new() -> Self { + pub(crate) fn new() -> Self { let cache_path = get_cache_path(); if let Some(parent) = cache_path.parent() { let _ = std::fs::create_dir_all(parent); @@ -118,6 +118,9 @@ impl TokenCache { let contents = std::fs::read_to_string(&self.cache_path).ok()?; serde_json::from_str(&contents).ok() } + pub(crate) fn has_token(&self) -> bool { + self.load().is_some() + } fn save(&self, token_data: &TokenData) -> Result<()> { if let Some(parent) = self.cache_path.parent() { @@ -805,10 +808,6 @@ impl ProviderDef for XaiOAuthProvider { }) }) } - - fn inventory_configured() -> bool { - TokenCache::new().load().is_some() - } } /// Adapter so the same `XaiOAuthAuthProvider` can be both owned by the