Split provider inventory out of providers (#9826)

This commit is contained in:
Jack Amadeo
2026-06-17 16:42:38 -04:00
committed by GitHub
parent b0f5db2a07
commit c239ae0042
22 changed files with 404 additions and 309 deletions
@@ -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::<String>(key).is_ok(),
HuggingFaceProvider::inventory_configured,
|| huggingface_auth::has_configured_token().unwrap_or(false),
)
}
+2 -5
View File
@@ -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<InventoryIdentityInput> {
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<PathBuf> {
resolve_acp_command(command)
}
fn resolve_acp_command(command: &str) -> Result<PathBuf> {
+2 -16
View File
@@ -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<InventoryIdentityInput> {
acp_inventory_identity(AMP_ACP_PROVIDER_NAME, AMP_ACP_BINARY)
}
fn inventory_configured() -> bool {
acp_adapter_installed(AMP_ACP_BINARY)
}
}
-28
View File
@@ -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<Self::Provider>> {
Box::pin(Self::from_env(model))
}
fn supports_inventory_refresh() -> bool {
true
}
fn inventory_identity() -> Result<InventoryIdentityInput> {
let config = crate::config::Config::global();
let mut identity =
InventoryIdentityInput::new(ANTHROPIC_PROVIDER_NAME, ANTHROPIC_PROVIDER_NAME)
.with_public(
"host",
config
.get_param::<String>("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::<std::collections::HashMap<String, String>>("ANTHROPIC_CUSTOM_HEADERS")
{
identity = identity.with_secret("headers", serialize_string_map(&headers)?);
}
Ok(identity)
}
}
#[async_trait]
+1 -30
View File
@@ -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<InventoryIdentityInput>
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)]
+7 -8
View File
@@ -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<Self::Provider>> {
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(
+2 -16
View File
@@ -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<InventoryIdentityInput> {
acp_inventory_identity(CLAUDE_ACP_PROVIDER_NAME, CLAUDE_ACP_BINARY)
}
fn inventory_configured() -> bool {
acp_adapter_installed(CLAUDE_ACP_BINARY)
}
}
+1 -15
View File
@@ -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<InventoryIdentityInput> {
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
+2 -16
View File
@@ -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<InventoryIdentityInput> {
acp_inventory_identity(COPILOT_ACP_PROVIDER_NAME, COPILOT_ACP_BINARY)
}
fn inventory_configured() -> bool {
acp_adapter_installed(COPILOT_ACP_BINARY)
}
}
-4
View File
@@ -613,10 +613,6 @@ impl ProviderDef for DatabricksProvider {
) -> BoxFuture<'static, Result<Self::Provider>> {
Box::pin(Self::from_env(model))
}
fn supports_inventory_refresh() -> bool {
true
}
}
#[async_trait]
@@ -369,10 +369,6 @@ impl ProviderDef for DatabricksV2Provider {
) -> BoxFuture<'static, Result<Self::Provider>> {
Box::pin(Self::from_env(model))
}
fn supports_inventory_refresh() -> bool {
true
}
}
#[async_trait]
+1 -23
View File
@@ -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<Self::Provider>> {
Box::pin(Self::from_env(model))
}
fn supports_inventory_refresh() -> bool {
true
}
fn inventory_identity() -> Result<InventoryIdentityInput> {
let config = crate::config::Config::global();
let mut identity = InventoryIdentityInput::new(GOOGLE_PROVIDER_NAME, GOOGLE_PROVIDER_NAME)
.with_public(
"host",
config
.get_param::<String>("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]
-15
View File
@@ -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<InventoryIdentityInput> {
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 {
+58 -14
View File
@@ -55,42 +55,86 @@ static REGISTRY: OnceCell<RwLock<ProviderRegistry>> = OnceCell::const_new();
async fn init_registry() -> RwLock<ProviderRegistry> {
let mut registry = ProviderRegistry::new().with_providers(|registry| {
registry.register::<AmpAcpProvider>(false);
registry.register::<AnthropicProvider>(true);
use super::inventory::registrations;
registry.register_with_inventory::<AmpAcpProvider>(
false,
Some(registrations::amp_acp_inventory()),
);
registry.register_with_inventory::<AnthropicProvider>(
true,
Some(registrations::anthropic_inventory()),
);
registry.register::<AvianProvider>(false);
registry.register::<AzureProvider>(false);
#[cfg(feature = "aws-providers")]
registry.register::<BedrockProvider>(false);
#[cfg(feature = "local-inference")]
registry.register::<LocalInferenceProvider>(false);
registry.register::<ChatGptCodexProvider>(true);
registry.register::<ClaudeAcpProvider>(false);
registry.register_with_inventory::<ChatGptCodexProvider>(
true,
Some(registrations::chatgpt_codex_inventory()),
);
registry.register_with_inventory::<ClaudeAcpProvider>(
false,
Some(registrations::claude_acp_inventory()),
);
registry.register::<ClaudeCodeProvider>(true);
registry.register::<CodexAcpProvider>(false);
registry.register::<CopilotAcpProvider>(false);
registry.register_with_inventory::<CodexAcpProvider>(
false,
Some(registrations::codex_acp_inventory()),
);
registry.register_with_inventory::<CopilotAcpProvider>(
false,
Some(registrations::copilot_acp_inventory()),
);
registry.register::<CodexProvider>(true);
registry.register::<CursorAgentProvider>(false);
registry.register::<DatabricksProvider>(true);
registry.register::<DatabricksV2Provider>(false);
registry.register_with_inventory::<DatabricksProvider>(
true,
Some(registrations::refresh_only()),
);
registry.register_with_inventory::<DatabricksV2Provider>(
false,
Some(registrations::refresh_only()),
);
registry.register::<GcpVertexAIProvider>(false);
registry.register::<GeminiCliProvider>(false);
registry.register::<GeminiOAuthProvider>(true);
registry.register::<GithubCopilotProvider>(false);
registry.register::<GoogleProvider>(true);
registry.register::<HuggingFaceProvider>(true);
registry.register_with_inventory::<GoogleProvider>(
true,
Some(registrations::google_inventory()),
);
registry.register_with_inventory::<HuggingFaceProvider>(
true,
Some(registrations::huggingface_inventory()),
);
registry.register::<KimiCodeProvider>(true);
registry.register::<LiteLLMProvider>(false);
registry.register::<NanoGptProvider>(true);
registry.register::<OllamaProvider>(true);
registry.register::<OpenAiProvider>(true);
registry.register_with_inventory::<OllamaProvider>(
true,
Some(registrations::ollama_inventory()),
);
registry.register_with_inventory::<OpenAiProvider>(
true,
Some(registrations::openai_inventory()),
);
registry.register::<OpenRouterProvider>(true);
registry.register::<PiAcpProvider>(false);
registry.register_with_inventory::<PiAcpProvider>(
false,
Some(registrations::pi_acp_inventory()),
);
#[cfg(feature = "aws-providers")]
registry.register::<SageMakerTgiProvider>(false);
registry.register::<SnowflakeProvider>(false);
registry.register::<TetrateProvider>(true);
registry.register::<XaiProvider>(false);
registry.register::<XaiOAuthProvider>(true);
registry.register_with_inventory::<XaiOAuthProvider>(
true,
Some(registrations::xai_oauth_inventory()),
);
});
// Register cleanup functions for providers with cached state
registry.set_cleanup(
@@ -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;
@@ -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::<String>("OPENAI_HOST")
.unwrap_or_else(|_| "https://api.openai.com".to_string()),
)
.with_public(
"base_path",
config
.get_param::<String>("OPENAI_BASE_PATH")
.unwrap_or_else(|_| OPEN_AI_DEFAULT_BASE_PATH.to_string()),
);
if let Ok(organization) = config.get_param::<String>("OPENAI_ORGANIZATION") {
identity = identity.with_public("organization", organization);
}
if let Ok(project) = config.get_param::<String>("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::<String>("OPENAI_HOST") {
if host != "https://api.openai.com" {
return true;
}
}
config
.get_secret::<serde_json::Value>("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::<String>("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::<std::collections::HashMap<String, String>>("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::<String>("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::<String>("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)
}
@@ -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<InventoryIdentityResolver> =
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<dyn Fn() -> Result<InventoryIdentityInput> + Send + Sync>;
pub type InventoryConfiguredResolver = Arc<dyn Fn() -> bool + Send + Sync>;
#[derive(Clone)]
pub struct InventoryRegistration {
pub supports_refresh: bool,
pub identity: InventoryIdentityResolver,
pub configured: Option<InventoryConfiguredResolver>,
}
impl InventoryRegistration {
pub fn new<G>(supports_refresh: bool, identity: G) -> Self
where
G: Fn() -> Result<InventoryIdentityInput> + Send + Sync + 'static,
{
Self {
supports_refresh,
identity: Arc::new(identity),
configured: None,
}
}
pub fn with_configured<H>(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<InventoryRegistration>,
) -> 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(
&registration.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,
},
}
}
}
+2 -23
View File
@@ -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::<String>("OLLAMA_HOST").is_ok()
}
@@ -266,26 +265,6 @@ impl ProviderDef for OllamaProvider {
) -> BoxFuture<'static, Result<Self::Provider>> {
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<InventoryIdentityInput> {
let config = crate::config::Config::global();
Ok(
InventoryIdentityInput::new(OLLAMA_PROVIDER_NAME, OLLAMA_PROVIDER_NAME).with_public(
"host",
config
.get_param::<String>("OLLAMA_HOST")
.unwrap_or_else(|_| OLLAMA_HOST.to_string()),
),
)
}
}
#[async_trait]
+2 -55
View File
@@ -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<Self::Provider>> {
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::<String>("OPENAI_HOST") {
if host != "https://api.openai.com" {
return true;
}
}
// Standard OpenAI endpoint requires an API key.
config
.get_secret::<serde_json::Value>("OPENAI_API_KEY")
.is_ok()
}
fn inventory_identity() -> Result<InventoryIdentityInput> {
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::<String>("OPENAI_HOST")
.unwrap_or_else(|_| "https://api.openai.com".to_string()),
)
.with_public(
"base_path",
config
.get_param::<String>("OPENAI_BASE_PATH")
.unwrap_or_else(|_| OPEN_AI_DEFAULT_BASE_PATH.to_string()),
);
if let Ok(organization) = config.get_param::<String>("OPENAI_ORGANIZATION") {
identity = identity.with_public("organization", organization);
}
if let Ok(project) = config.get_param::<String>("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]
+2 -16
View File
@@ -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<InventoryIdentityInput> {
acp_inventory_identity(PI_ACP_PROVIDER_NAME, PI_ACP_BINARY)
}
fn inventory_configured() -> bool {
acp_adapter_installed(PI_ACP_BINARY)
}
}
+19 -12
View File
@@ -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<dyn Fn() -> BoxFuture<'static, Result<()>> + Send + Sync>;
pub type ProviderInventoryIdentityResolver =
Arc<dyn Fn() -> Result<InventoryIdentityInput> + Send + Sync>;
pub type ProviderInventoryConfiguredResolver = Arc<dyn Fn() -> 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<ProviderCleanup>,
provider_type: ProviderType,
supports_inventory_refresh: bool,
@@ -118,10 +113,22 @@ impl ProviderRegistry {
pub fn register<F>(&mut self, preferred: bool)
where
F: ProviderDef + 'static,
{
self.register_with_inventory::<F>(preferred, None);
}
pub fn register_with_inventory<F>(
&mut self,
preferred: bool,
inventory_registration: Option<InventoryRegistration>,
) 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<dyn Provider>)
})
}),
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<ProviderInventoryConfiguredResolver>,
inventory_configured: Option<super::inventory::InventoryConfiguredResolver>,
) where
P: ProviderDef + 'static,
F: Fn(ModelConfig) -> Result<P::Provider> + Send + Sync + 'static,
+6 -7
View File
@@ -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