feat: move goose2 provider catalog behind ACP layer (#9030)

Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Matt Toohey <contact@matttoohey.com>
This commit is contained in:
Kalvin C
2026-05-05 18:36:19 -07:00
committed by GitHub
parent 8464718588
commit b17fd4f287
56 changed files with 3235 additions and 895 deletions
+86 -2
View File
@@ -427,7 +427,7 @@ pub struct ProviderConfigChangeResponse {
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ProviderCatalogEntryDto {
pub struct ProviderTemplateCatalogEntryDto {
pub provider_id: String,
pub name: String,
pub format: String,
@@ -437,6 +437,73 @@ pub struct ProviderCatalogEntryDto {
pub env_var: String,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ProviderSetupCategoryDto {
Agent,
#[default]
Model,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ProviderSetupMethodDto {
None,
SingleApiKey,
ConfigFields,
HostWithOauthFallback,
OauthBrowser,
OauthDeviceCode,
CloudCredentials,
Local,
CliAuth,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ProviderSetupGroupDto {
Default,
Additional,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ProviderSetupFieldDto {
pub key: String,
pub label: String,
pub secret: bool,
pub required: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub placeholder: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_value: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ProviderSetupCatalogEntryDto {
pub provider_id: String,
pub name: String,
pub category: ProviderSetupCategoryDto,
pub description: String,
pub setup_method: ProviderSetupMethodDto,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub native_connect_query: Option<String>,
#[serde(default)]
pub fields: Vec<ProviderSetupFieldDto>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub binary_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub doc_url: Option<String>,
pub group: ProviderSetupGroupDto,
pub show_only_when_installed: bool,
#[serde(default)]
pub aliases: Vec<String>,
pub supports_install: bool,
pub supports_auth: bool,
pub supports_auth_status: bool,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ProviderTemplateCapabilitiesDto {
@@ -484,7 +551,22 @@ pub struct ProviderCatalogListRequest {
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
#[serde(rename_all = "camelCase")]
pub struct ProviderCatalogListResponse {
pub providers: Vec<ProviderCatalogEntryDto>,
pub providers: Vec<ProviderTemplateCatalogEntryDto>,
}
/// List provider setup catalog entries
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(
method = "_goose/providers/setup/catalog/list",
response = ProviderSetupCatalogListResponse
)]
#[serde(rename_all = "camelCase")]
pub struct ProviderSetupCatalogListRequest {}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
#[serde(rename_all = "camelCase")]
pub struct ProviderSetupCatalogListResponse {
pub providers: Vec<ProviderSetupCatalogEntryDto>,
}
/// Return the editable template for one catalog provider.
@@ -949,6 +1031,8 @@ pub struct ProviderInventoryEntryDto {
pub configured: bool,
/// Provider classification such as `Preferred`, `Builtin`, `Declarative`, or `Custom`.
pub provider_type: String,
/// Whether this inventory entry represents an agent provider or a model provider.
pub category: ProviderSetupCategoryDto,
/// Required configuration keys and setup metadata.
pub config_keys: Vec<ProviderConfigKey>,
/// Step-by-step setup instructions, when present.
+5
View File
@@ -70,6 +70,11 @@
"requestType": "ProviderCatalogListRequest",
"responseType": "ProviderCatalogListResponse"
},
{
"method": "_goose/providers/setup/catalog/list",
"requestType": "ProviderSetupCatalogListRequest",
"responseType": "ProviderSetupCatalogListResponse"
},
{
"method": "_goose/providers/catalog/template",
"requestType": "ProviderCatalogTemplateRequest",
+188 -2
View File
@@ -351,6 +351,10 @@
"type": "string",
"description": "Provider classification such as `Preferred`, `Builtin`, `Declarative`, or `Custom`."
},
"category": {
"$ref": "#/$defs/ProviderSetupCategoryDto",
"description": "Whether this inventory entry represents an agent provider or a model provider."
},
"configKeys": {
"type": "array",
"items": {
@@ -420,6 +424,7 @@
"defaultModel",
"configured",
"providerType",
"category",
"configKeys",
"setupSteps",
"supportsRefresh",
@@ -429,6 +434,13 @@
],
"description": "Provider inventory entry."
},
"ProviderSetupCategoryDto": {
"type": "string",
"enum": [
"agent",
"model"
]
},
"ProviderConfigKey": {
"type": "object",
"properties": {
@@ -533,7 +545,7 @@
"providers": {
"type": "array",
"items": {
"$ref": "#/$defs/ProviderCatalogEntryDto"
"$ref": "#/$defs/ProviderTemplateCatalogEntryDto"
}
}
},
@@ -543,7 +555,7 @@
"x-side": "agent",
"x-method": "_goose/providers/catalog/list"
},
"ProviderCatalogEntryDto": {
"ProviderTemplateCatalogEntryDto": {
"type": "object",
"properties": {
"providerId": {
@@ -579,6 +591,163 @@
"envVar"
]
},
"ProviderSetupCatalogListRequest": {
"type": "object",
"description": "List provider setup catalog entries",
"x-side": "agent",
"x-method": "_goose/providers/setup/catalog/list"
},
"ProviderSetupCatalogListResponse": {
"type": "object",
"properties": {
"providers": {
"type": "array",
"items": {
"$ref": "#/$defs/ProviderSetupCatalogEntryDto"
}
}
},
"required": [
"providers"
],
"x-side": "agent",
"x-method": "_goose/providers/setup/catalog/list"
},
"ProviderSetupCatalogEntryDto": {
"type": "object",
"properties": {
"providerId": {
"type": "string"
},
"name": {
"type": "string"
},
"category": {
"$ref": "#/$defs/ProviderSetupCategoryDto"
},
"description": {
"type": "string"
},
"setupMethod": {
"$ref": "#/$defs/ProviderSetupMethodDto"
},
"nativeConnectQuery": {
"type": [
"string",
"null"
]
},
"fields": {
"type": "array",
"items": {
"$ref": "#/$defs/ProviderSetupFieldDto"
},
"default": []
},
"binaryName": {
"type": [
"string",
"null"
]
},
"docUrl": {
"type": [
"string",
"null"
]
},
"group": {
"$ref": "#/$defs/ProviderSetupGroupDto"
},
"showOnlyWhenInstalled": {
"type": "boolean"
},
"aliases": {
"type": "array",
"items": {
"type": "string"
},
"default": []
},
"supportsInstall": {
"type": "boolean"
},
"supportsAuth": {
"type": "boolean"
},
"supportsAuthStatus": {
"type": "boolean"
}
},
"required": [
"providerId",
"name",
"category",
"description",
"setupMethod",
"group",
"showOnlyWhenInstalled",
"supportsInstall",
"supportsAuth",
"supportsAuthStatus"
]
},
"ProviderSetupMethodDto": {
"type": "string",
"enum": [
"none",
"single_api_key",
"config_fields",
"host_with_oauth_fallback",
"oauth_browser",
"oauth_device_code",
"cloud_credentials",
"local",
"cli_auth"
]
},
"ProviderSetupFieldDto": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"label": {
"type": "string"
},
"secret": {
"type": "boolean"
},
"required": {
"type": "boolean"
},
"placeholder": {
"type": [
"string",
"null"
]
},
"defaultValue": {
"type": [
"string",
"null"
]
}
},
"required": [
"key",
"label",
"secret",
"required"
]
},
"ProviderSetupGroupDto": {
"type": "string",
"enum": [
"default",
"additional"
]
},
"ProviderCatalogTemplateRequest": {
"type": "object",
"properties": {
@@ -2265,6 +2434,15 @@
"description": "Params for _goose/providers/catalog/list",
"title": "ProviderCatalogListRequest"
},
{
"allOf": [
{
"$ref": "#/$defs/ProviderSetupCatalogListRequest"
}
],
"description": "Params for _goose/providers/setup/catalog/list",
"title": "ProviderSetupCatalogListRequest"
},
{
"allOf": [
{
@@ -2683,6 +2861,14 @@
],
"title": "ProviderCatalogListResponse"
},
{
"allOf": [
{
"$ref": "#/$defs/ProviderSetupCatalogListResponse"
}
],
"title": "ProviderSetupCatalogListResponse"
},
{
"allOf": [
{
+1
View File
@@ -3969,6 +3969,7 @@ print(\"hello, world\")
default_model: "unused".to_string(),
configured: true,
provider_type: crate::providers::base::ProviderType::Builtin,
category: crate::providers::catalog::ProviderSetupCategory::Model,
config_keys: vec![],
setup_steps: vec![],
supports_refresh: true,
@@ -120,6 +120,14 @@ impl GooseAcpAgent {
self.on_list_provider_catalog(req).await
}
#[custom_method(ProviderSetupCatalogListRequest)]
async fn dispatch_list_provider_setup_catalog(
&self,
req: ProviderSetupCatalogListRequest,
) -> Result<ProviderSetupCatalogListResponse, sacp::Error> {
self.on_list_provider_setup_catalog(req).await
}
#[custom_method(ProviderCatalogTemplateRequest)]
async fn dispatch_get_provider_catalog_template(
&self,
+96 -2
View File
@@ -11,6 +11,7 @@ fn inventory_entry_to_dto(entry: ProviderInventoryEntry) -> ProviderInventoryEnt
default_model: entry.default_model,
configured: entry.configured,
provider_type: format!("{:?}", entry.provider_type),
category: provider_setup_category_to_dto(entry.category),
config_keys: entry
.config_keys
.into_iter()
@@ -114,8 +115,8 @@ fn provider_config_field_value(
fn provider_catalog_entry_to_dto(
entry: crate::providers::catalog::ProviderCatalogEntry,
) -> ProviderCatalogEntryDto {
ProviderCatalogEntryDto {
) -> ProviderTemplateCatalogEntryDto {
ProviderTemplateCatalogEntryDto {
provider_id: entry.id,
name: entry.name,
format: entry.format,
@@ -126,6 +127,87 @@ fn provider_catalog_entry_to_dto(
}
}
fn provider_setup_category_to_dto(
category: crate::providers::catalog::ProviderSetupCategory,
) -> ProviderSetupCategoryDto {
match category {
crate::providers::catalog::ProviderSetupCategory::Agent => ProviderSetupCategoryDto::Agent,
crate::providers::catalog::ProviderSetupCategory::Model => ProviderSetupCategoryDto::Model,
}
}
fn provider_setup_method_to_dto(
method: crate::providers::catalog::ProviderSetupMethod,
) -> ProviderSetupMethodDto {
match method {
crate::providers::catalog::ProviderSetupMethod::None => ProviderSetupMethodDto::None,
crate::providers::catalog::ProviderSetupMethod::SingleApiKey => {
ProviderSetupMethodDto::SingleApiKey
}
crate::providers::catalog::ProviderSetupMethod::ConfigFields => {
ProviderSetupMethodDto::ConfigFields
}
crate::providers::catalog::ProviderSetupMethod::HostWithOauthFallback => {
ProviderSetupMethodDto::HostWithOauthFallback
}
crate::providers::catalog::ProviderSetupMethod::OauthBrowser => {
ProviderSetupMethodDto::OauthBrowser
}
crate::providers::catalog::ProviderSetupMethod::OauthDeviceCode => {
ProviderSetupMethodDto::OauthDeviceCode
}
crate::providers::catalog::ProviderSetupMethod::CloudCredentials => {
ProviderSetupMethodDto::CloudCredentials
}
crate::providers::catalog::ProviderSetupMethod::Local => ProviderSetupMethodDto::Local,
crate::providers::catalog::ProviderSetupMethod::CliAuth => ProviderSetupMethodDto::CliAuth,
}
}
fn provider_setup_group_to_dto(
group: crate::providers::catalog::ProviderSetupGroup,
) -> ProviderSetupGroupDto {
match group {
crate::providers::catalog::ProviderSetupGroup::Default => ProviderSetupGroupDto::Default,
crate::providers::catalog::ProviderSetupGroup::Additional => {
ProviderSetupGroupDto::Additional
}
}
}
fn provider_setup_entry_to_dto(
entry: crate::providers::catalog::ProviderSetupCatalogEntry,
) -> ProviderSetupCatalogEntryDto {
ProviderSetupCatalogEntryDto {
provider_id: entry.provider_id,
name: entry.display_name,
category: provider_setup_category_to_dto(entry.category),
description: entry.description,
setup_method: provider_setup_method_to_dto(entry.setup_method),
native_connect_query: entry.native_connect_query,
fields: entry
.fields
.into_iter()
.map(|field| ProviderSetupFieldDto {
key: field.key,
label: field.label,
secret: field.secret,
required: field.required,
placeholder: field.placeholder,
default_value: field.default_value,
})
.collect(),
binary_name: entry.binary_name,
doc_url: entry.docs_url,
group: provider_setup_group_to_dto(entry.group),
show_only_when_installed: entry.show_only_when_installed,
aliases: entry.aliases,
supports_install: entry.setup_capabilities.install,
supports_auth: entry.setup_capabilities.auth,
supports_auth_status: entry.setup_capabilities.auth_status,
}
}
fn provider_template_to_dto(
template: crate::providers::catalog::ProviderTemplate,
) -> ProviderTemplateDto {
@@ -380,6 +462,18 @@ impl GooseAcpAgent {
Ok(ProviderCatalogListResponse { providers })
}
pub(super) async fn on_list_provider_setup_catalog(
&self,
_req: ProviderSetupCatalogListRequest,
) -> Result<ProviderSetupCatalogListResponse, sacp::Error> {
let providers = crate::providers::catalog::get_setup_catalog_entries()
.await
.into_iter()
.map(provider_setup_entry_to_dto)
.collect();
Ok(ProviderSetupCatalogListResponse { providers })
}
pub(super) async fn on_get_provider_catalog_template(
&self,
req: ProviderCatalogTemplateRequest,
+868 -1
View File
@@ -2,7 +2,10 @@ use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use super::canonical::CanonicalModelRegistry;
use super::{
base::{ConfigKey, ProviderMetadata},
canonical::CanonicalModelRegistry,
};
const PROVIDER_METADATA_JSON: &str = include_str!("canonical/data/provider_metadata.json");
@@ -110,6 +113,784 @@ pub struct ModelCapabilities {
pub temperature: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProviderSetupCategory {
Agent,
Model,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProviderSetupMethod {
None,
SingleApiKey,
ConfigFields,
HostWithOauthFallback,
OauthBrowser,
OauthDeviceCode,
CloudCredentials,
Local,
CliAuth,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProviderSetupGroup {
Default,
Additional,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderSetupField {
pub key: String,
pub label: String,
pub secret: bool,
pub required: bool,
pub placeholder: Option<String>,
pub default_value: Option<String>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct ProviderSetupCapabilities {
pub install: bool,
pub auth: bool,
pub auth_status: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderSetupCatalogEntry {
pub provider_id: String,
pub display_name: String,
pub category: ProviderSetupCategory,
pub description: String,
pub setup_method: ProviderSetupMethod,
pub docs_url: Option<String>,
pub group: ProviderSetupGroup,
pub fields: Vec<ProviderSetupField>,
pub aliases: Vec<String>,
pub native_connect_query: Option<String>,
pub binary_name: Option<String>,
pub setup_capabilities: ProviderSetupCapabilities,
pub show_only_when_installed: bool,
}
#[derive(Debug, Clone, Copy)]
struct CuratedSetupMetadata {
provider_id: &'static str,
category: ProviderSetupCategory,
setup_method: ProviderSetupMethod,
group: ProviderSetupGroup,
display_name: Option<&'static str>,
description: Option<&'static str>,
docs_url: Option<&'static str>,
aliases: &'static [&'static str],
native_connect_query: Option<&'static str>,
binary_name: Option<&'static str>,
setup_capabilities: ProviderSetupCapabilities,
show_only_when_installed: bool,
synthetic: bool,
secret_field_default: Option<CuratedFieldMetadata>,
field_overrides: &'static [CuratedFieldMetadata],
}
#[derive(Debug, Clone, Copy)]
struct CuratedFieldMetadata {
key: &'static str,
label: &'static str,
placeholder: Option<&'static str>,
default_value: Option<&'static str>,
}
const fn setup_capabilities(
install: bool,
auth: bool,
auth_status: bool,
) -> ProviderSetupCapabilities {
ProviderSetupCapabilities {
install,
auth,
auth_status,
}
}
const API_KEY_FIELD: CuratedFieldMetadata = CuratedFieldMetadata {
key: "",
label: "API Key",
placeholder: Some("Paste your API key"),
default_value: None,
};
const SETUP_METADATA: &[CuratedSetupMetadata] = &[
CuratedSetupMetadata {
provider_id: "goose",
category: ProviderSetupCategory::Agent,
setup_method: ProviderSetupMethod::None,
group: ProviderSetupGroup::Default,
display_name: Some("Goose"),
description: Some("Block's open-source coding agent"),
docs_url: None,
aliases: &["goose"],
native_connect_query: None,
binary_name: None,
setup_capabilities: setup_capabilities(false, false, false),
show_only_when_installed: false,
synthetic: true,
secret_field_default: None,
field_overrides: &[],
},
CuratedSetupMetadata {
provider_id: "claude-acp",
category: ProviderSetupCategory::Agent,
setup_method: ProviderSetupMethod::CliAuth,
group: ProviderSetupGroup::Default,
display_name: Some("Claude Code"),
description: Some("Anthropic's agentic coding tool"),
docs_url: Some("https://docs.anthropic.com/en/docs/claude-code"),
aliases: &["claude-acp", "claude_code", "claude"],
native_connect_query: None,
binary_name: Some("claude-agent-acp"),
setup_capabilities: setup_capabilities(true, true, true),
show_only_when_installed: false,
synthetic: false,
secret_field_default: None,
field_overrides: &[],
},
CuratedSetupMetadata {
provider_id: "codex-acp",
category: ProviderSetupCategory::Agent,
setup_method: ProviderSetupMethod::CliAuth,
group: ProviderSetupGroup::Default,
display_name: Some("Codex"),
description: Some("OpenAI's coding agent"),
docs_url: Some("https://github.com/openai/codex"),
aliases: &["codex-acp", "codex_cli", "codex"],
native_connect_query: None,
binary_name: Some("codex-acp"),
setup_capabilities: setup_capabilities(true, true, true),
show_only_when_installed: false,
synthetic: false,
secret_field_default: None,
field_overrides: &[],
},
CuratedSetupMetadata {
provider_id: "copilot-acp",
category: ProviderSetupCategory::Agent,
setup_method: ProviderSetupMethod::CliAuth,
group: ProviderSetupGroup::Default,
display_name: Some("GitHub Copilot"),
description: Some("GitHub's AI pair programmer"),
docs_url: Some("https://docs.github.com/en/copilot/github-copilot-in-the-cli"),
aliases: &["copilot-acp", "github_copilot", "github_copilot_cli"],
native_connect_query: None,
binary_name: Some("copilot"),
setup_capabilities: setup_capabilities(true, true, false),
show_only_when_installed: false,
synthetic: false,
secret_field_default: None,
field_overrides: &[],
},
CuratedSetupMetadata {
provider_id: "amp-acp",
category: ProviderSetupCategory::Agent,
setup_method: ProviderSetupMethod::CliAuth,
group: ProviderSetupGroup::Default,
display_name: None,
description: Some("Sourcegraph's coding agent"),
docs_url: Some("https://ampcode.com"),
aliases: &["amp-acp", "amp"],
native_connect_query: None,
binary_name: Some("amp-acp"),
setup_capabilities: setup_capabilities(true, true, true),
show_only_when_installed: false,
synthetic: false,
secret_field_default: None,
field_overrides: &[],
},
CuratedSetupMetadata {
provider_id: "cursor-agent",
category: ProviderSetupCategory::Agent,
setup_method: ProviderSetupMethod::CliAuth,
group: ProviderSetupGroup::Default,
display_name: None,
description: Some("Cursor's AI agent"),
docs_url: Some("https://docs.cursor.com/en/cli/overview"),
aliases: &["cursor-agent", "cursor_agent", "cursor"],
native_connect_query: None,
binary_name: Some("cursor-agent"),
setup_capabilities: setup_capabilities(true, true, true),
show_only_when_installed: false,
synthetic: false,
secret_field_default: None,
field_overrides: &[],
},
CuratedSetupMetadata {
provider_id: "pi-acp",
category: ProviderSetupCategory::Agent,
setup_method: ProviderSetupMethod::CliAuth,
group: ProviderSetupGroup::Default,
display_name: None,
description: Some("Open-source AI coding agent"),
docs_url: Some("https://github.com/badlogic/pi-mono"),
aliases: &["pi-acp", "pi"],
native_connect_query: None,
binary_name: Some("pi-acp"),
setup_capabilities: setup_capabilities(false, false, false),
show_only_when_installed: true,
synthetic: false,
secret_field_default: None,
field_overrides: &[],
},
CuratedSetupMetadata {
provider_id: "anthropic",
category: ProviderSetupCategory::Model,
setup_method: ProviderSetupMethod::SingleApiKey,
group: ProviderSetupGroup::Default,
display_name: None,
description: Some("Claude models"),
docs_url: Some("https://console.anthropic.com/settings/keys"),
aliases: &[],
native_connect_query: None,
binary_name: None,
setup_capabilities: setup_capabilities(false, false, false),
show_only_when_installed: false,
synthetic: false,
secret_field_default: Some(API_KEY_FIELD),
field_overrides: &[],
},
CuratedSetupMetadata {
provider_id: "google",
category: ProviderSetupCategory::Model,
setup_method: ProviderSetupMethod::SingleApiKey,
group: ProviderSetupGroup::Default,
display_name: Some("Google Gemini"),
description: Some("Gemini models"),
docs_url: Some("https://aistudio.google.com/apikey"),
aliases: &[],
native_connect_query: None,
binary_name: None,
setup_capabilities: setup_capabilities(false, false, false),
show_only_when_installed: false,
synthetic: false,
secret_field_default: Some(API_KEY_FIELD),
field_overrides: &[],
},
CuratedSetupMetadata {
provider_id: "chatgpt_codex",
category: ProviderSetupCategory::Model,
setup_method: ProviderSetupMethod::OauthDeviceCode,
group: ProviderSetupGroup::Default,
display_name: Some("ChatGPT"),
description: Some("OpenAI via ChatGPT subscription"),
docs_url: Some("https://chatgpt.com"),
aliases: &[],
native_connect_query: Some("ChatGPT Codex"),
binary_name: None,
setup_capabilities: setup_capabilities(false, true, false),
show_only_when_installed: false,
synthetic: false,
secret_field_default: None,
field_overrides: &[],
},
CuratedSetupMetadata {
provider_id: "openai",
category: ProviderSetupCategory::Model,
setup_method: ProviderSetupMethod::ConfigFields,
group: ProviderSetupGroup::Default,
display_name: None,
description: Some("GPT and o-series models"),
docs_url: Some("https://platform.openai.com/api-keys"),
aliases: &[],
native_connect_query: None,
binary_name: None,
setup_capabilities: setup_capabilities(false, false, false),
show_only_when_installed: false,
synthetic: false,
secret_field_default: Some(API_KEY_FIELD),
field_overrides: &[],
},
CuratedSetupMetadata {
provider_id: "mistral",
category: ProviderSetupCategory::Model,
setup_method: ProviderSetupMethod::SingleApiKey,
group: ProviderSetupGroup::Default,
display_name: None,
description: None,
docs_url: Some("https://console.mistral.ai/api-keys"),
aliases: &[],
native_connect_query: None,
binary_name: None,
setup_capabilities: setup_capabilities(false, false, false),
show_only_when_installed: false,
synthetic: false,
secret_field_default: Some(API_KEY_FIELD),
field_overrides: &[],
},
CuratedSetupMetadata {
provider_id: "ollama",
category: ProviderSetupCategory::Model,
setup_method: ProviderSetupMethod::ConfigFields,
group: ProviderSetupGroup::Default,
display_name: None,
description: Some("Run local or self-hosted models"),
docs_url: Some("https://ollama.com"),
aliases: &[],
native_connect_query: None,
binary_name: None,
setup_capabilities: setup_capabilities(false, false, false),
show_only_when_installed: false,
synthetic: false,
secret_field_default: None,
field_overrides: &[CuratedFieldMetadata {
key: "OLLAMA_HOST",
label: "Host",
placeholder: Some("localhost or http://localhost:11434"),
default_value: Some("http://localhost:11434"),
}],
},
CuratedSetupMetadata {
provider_id: "openrouter",
category: ProviderSetupCategory::Model,
setup_method: ProviderSetupMethod::SingleApiKey,
group: ProviderSetupGroup::Default,
display_name: None,
description: Some("Unified API for many models"),
docs_url: Some("https://openrouter.ai/keys"),
aliases: &[],
native_connect_query: None,
binary_name: None,
setup_capabilities: setup_capabilities(false, false, false),
show_only_when_installed: false,
synthetic: false,
secret_field_default: Some(API_KEY_FIELD),
field_overrides: &[],
},
CuratedSetupMetadata {
provider_id: "databricks",
category: ProviderSetupCategory::Model,
setup_method: ProviderSetupMethod::HostWithOauthFallback,
group: ProviderSetupGroup::Default,
display_name: None,
description: Some("Databricks Foundation Models"),
docs_url: None,
aliases: &[],
native_connect_query: None,
binary_name: None,
setup_capabilities: setup_capabilities(false, true, false),
show_only_when_installed: false,
synthetic: false,
secret_field_default: None,
field_overrides: &[
CuratedFieldMetadata {
key: "DATABRICKS_HOST",
label: "Host URL",
placeholder: Some("https://dbc-...cloud.databricks.com"),
default_value: None,
},
CuratedFieldMetadata {
key: "DATABRICKS_TOKEN",
label: "Access Token",
placeholder: Some("Paste your access token"),
default_value: None,
},
],
},
CuratedSetupMetadata {
provider_id: "github_copilot",
category: ProviderSetupCategory::Model,
setup_method: ProviderSetupMethod::OauthDeviceCode,
group: ProviderSetupGroup::Default,
display_name: Some("GitHub Copilot Models"),
description: Some("Models via GitHub Copilot subscription"),
docs_url: None,
aliases: &[],
native_connect_query: Some("GitHub Copilot"),
binary_name: None,
setup_capabilities: setup_capabilities(false, true, false),
show_only_when_installed: false,
synthetic: false,
secret_field_default: None,
field_overrides: &[],
},
CuratedSetupMetadata {
provider_id: "custom_deepseek",
category: ProviderSetupCategory::Model,
setup_method: ProviderSetupMethod::SingleApiKey,
group: ProviderSetupGroup::Additional,
display_name: None,
description: Some("DeepSeek chat and reasoning models"),
docs_url: Some("https://platform.deepseek.com/api_keys"),
aliases: &[],
native_connect_query: None,
binary_name: None,
setup_capabilities: setup_capabilities(false, false, false),
show_only_when_installed: false,
synthetic: false,
secret_field_default: Some(API_KEY_FIELD),
field_overrides: &[],
},
CuratedSetupMetadata {
provider_id: "xai",
category: ProviderSetupCategory::Model,
setup_method: ProviderSetupMethod::SingleApiKey,
group: ProviderSetupGroup::Additional,
display_name: None,
description: Some("Grok models"),
docs_url: None,
aliases: &[],
native_connect_query: None,
binary_name: None,
setup_capabilities: setup_capabilities(false, false, false),
show_only_when_installed: false,
synthetic: false,
secret_field_default: Some(API_KEY_FIELD),
field_overrides: &[],
},
CuratedSetupMetadata {
provider_id: "groq",
category: ProviderSetupCategory::Model,
setup_method: ProviderSetupMethod::SingleApiKey,
group: ProviderSetupGroup::Additional,
display_name: Some("Groq"),
description: None,
docs_url: Some("https://console.groq.com/keys"),
aliases: &[],
native_connect_query: None,
binary_name: None,
setup_capabilities: setup_capabilities(false, false, false),
show_only_when_installed: false,
synthetic: false,
secret_field_default: Some(API_KEY_FIELD),
field_overrides: &[],
},
CuratedSetupMetadata {
provider_id: "azure_openai",
category: ProviderSetupCategory::Model,
setup_method: ProviderSetupMethod::ConfigFields,
group: ProviderSetupGroup::Additional,
display_name: None,
description: None,
docs_url: None,
aliases: &[],
native_connect_query: None,
binary_name: None,
setup_capabilities: setup_capabilities(false, false, false),
show_only_when_installed: false,
synthetic: false,
secret_field_default: None,
field_overrides: &[
CuratedFieldMetadata {
key: "AZURE_OPENAI_ENDPOINT",
label: "Endpoint",
placeholder: Some("https://your-resource.openai.azure.com"),
default_value: None,
},
CuratedFieldMetadata {
key: "AZURE_OPENAI_DEPLOYMENT_NAME",
label: "Deployment",
placeholder: Some("gpt-4o"),
default_value: None,
},
CuratedFieldMetadata {
key: "AZURE_OPENAI_API_KEY",
label: "API Key",
placeholder: Some("Paste your API key"),
default_value: None,
},
],
},
CuratedSetupMetadata {
provider_id: "aws_bedrock",
category: ProviderSetupCategory::Model,
setup_method: ProviderSetupMethod::CloudCredentials,
group: ProviderSetupGroup::Additional,
display_name: Some("AWS Bedrock"),
description: Some("Models on AWS"),
docs_url: None,
aliases: &[],
native_connect_query: None,
binary_name: None,
setup_capabilities: setup_capabilities(false, false, false),
show_only_when_installed: false,
synthetic: false,
secret_field_default: None,
field_overrides: &[CuratedFieldMetadata {
key: "AWS_REGION",
label: "AWS Region",
placeholder: Some("us-west-2"),
default_value: None,
}],
},
CuratedSetupMetadata {
provider_id: "gcp_vertex_ai",
category: ProviderSetupCategory::Model,
setup_method: ProviderSetupMethod::CloudCredentials,
group: ProviderSetupGroup::Additional,
display_name: None,
description: Some("Models on Google Cloud"),
docs_url: None,
aliases: &[],
native_connect_query: None,
binary_name: None,
setup_capabilities: setup_capabilities(false, false, false),
show_only_when_installed: false,
synthetic: false,
secret_field_default: None,
field_overrides: &[
CuratedFieldMetadata {
key: "GCP_PROJECT_ID",
label: "Project ID",
placeholder: Some("my-gcp-project"),
default_value: None,
},
CuratedFieldMetadata {
key: "GCP_LOCATION",
label: "Location",
placeholder: Some("us-central1"),
default_value: None,
},
],
},
CuratedSetupMetadata {
provider_id: "litellm",
category: ProviderSetupCategory::Model,
setup_method: ProviderSetupMethod::ConfigFields,
group: ProviderSetupGroup::Additional,
display_name: None,
description: Some("LiteLLM proxy gateway"),
docs_url: None,
aliases: &[],
native_connect_query: None,
binary_name: None,
setup_capabilities: setup_capabilities(false, false, false),
show_only_when_installed: false,
synthetic: false,
secret_field_default: None,
field_overrides: &[
CuratedFieldMetadata {
key: "LITELLM_HOST",
label: "Host URL",
placeholder: Some("https://your-proxy.example.com"),
default_value: None,
},
CuratedFieldMetadata {
key: "LITELLM_API_KEY",
label: "API Key",
placeholder: Some("Paste your API key"),
default_value: None,
},
],
},
CuratedSetupMetadata {
provider_id: "lmstudio",
category: ProviderSetupCategory::Model,
setup_method: ProviderSetupMethod::ConfigFields,
group: ProviderSetupGroup::Additional,
display_name: None,
description: None,
docs_url: Some("https://lmstudio.ai/docs/app/api"),
aliases: &[],
native_connect_query: None,
binary_name: None,
setup_capabilities: setup_capabilities(false, false, false),
show_only_when_installed: false,
synthetic: false,
secret_field_default: None,
field_overrides: &[CuratedFieldMetadata {
key: "LMSTUDIO_HOST",
label: "Host URL",
placeholder: Some("http://localhost:1234/v1/chat/completions"),
default_value: None,
}],
},
CuratedSetupMetadata {
provider_id: "nvidia",
category: ProviderSetupCategory::Model,
setup_method: ProviderSetupMethod::SingleApiKey,
group: ProviderSetupGroup::Additional,
display_name: None,
description: None,
docs_url: Some("https://build.nvidia.com/models"),
aliases: &[],
native_connect_query: None,
binary_name: None,
setup_capabilities: setup_capabilities(false, false, false),
show_only_when_installed: false,
synthetic: false,
secret_field_default: Some(API_KEY_FIELD),
field_overrides: &[],
},
CuratedSetupMetadata {
provider_id: "cerebras",
category: ProviderSetupCategory::Model,
setup_method: ProviderSetupMethod::SingleApiKey,
group: ProviderSetupGroup::Additional,
display_name: None,
description: None,
docs_url: Some("https://cloud.cerebras.ai/platform"),
aliases: &[],
native_connect_query: None,
binary_name: None,
setup_capabilities: setup_capabilities(false, false, false),
show_only_when_installed: false,
synthetic: false,
secret_field_default: Some(API_KEY_FIELD),
field_overrides: &[],
},
CuratedSetupMetadata {
provider_id: "snowflake",
category: ProviderSetupCategory::Model,
setup_method: ProviderSetupMethod::ConfigFields,
group: ProviderSetupGroup::Additional,
display_name: None,
description: Some("Snowflake Cortex"),
docs_url: None,
aliases: &[],
native_connect_query: None,
binary_name: None,
setup_capabilities: setup_capabilities(false, false, false),
show_only_when_installed: false,
synthetic: false,
secret_field_default: None,
field_overrides: &[
CuratedFieldMetadata {
key: "SNOWFLAKE_HOST",
label: "Host URL",
placeholder: Some("https://your-account.snowflakecomputing.com"),
default_value: None,
},
CuratedFieldMetadata {
key: "SNOWFLAKE_TOKEN",
label: "Access Token",
placeholder: Some("Paste your access token"),
default_value: None,
},
],
},
];
fn field_label(key: &str) -> String {
let label = key
.strip_prefix("GOOSE_")
.unwrap_or(key)
.replace('_', " ")
.to_lowercase();
label
.split_whitespace()
.map(|word| {
if matches!(
word,
"api" | "url" | "id" | "openai" | "aws" | "gcp" | "llm" | "oauth"
) {
word.to_uppercase()
} else {
let mut chars = word.chars();
match chars.next() {
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
}
})
.collect::<Vec<_>>()
.join(" ")
}
fn field_override<'a>(
key: &str,
config_key: &ConfigKey,
curated: &'a CuratedSetupMetadata,
) -> Option<&'a CuratedFieldMetadata> {
if let Some(field) = curated
.field_overrides
.iter()
.find(|field| field.key == key)
{
return Some(field);
}
if config_key.secret {
return curated.secret_field_default.as_ref();
}
None
}
fn setup_field(config_key: &ConfigKey, curated: &CuratedSetupMetadata) -> ProviderSetupField {
let field_override = field_override(&config_key.name, config_key, curated);
ProviderSetupField {
key: config_key.name.clone(),
label: field_override
.map(|field| field.label.to_string())
.unwrap_or_else(|| field_label(&config_key.name)),
secret: config_key.secret,
required: config_key.required,
placeholder: field_override.and_then(|field| field.placeholder.map(str::to_string)),
default_value: field_override
.and_then(|field| field.default_value.map(str::to_string))
.or_else(|| config_key.default.clone()),
}
}
fn setup_entry_from_metadata(
curated: &CuratedSetupMetadata,
metadata: &ProviderMetadata,
) -> ProviderSetupCatalogEntry {
ProviderSetupCatalogEntry {
provider_id: curated.provider_id.to_string(),
display_name: curated
.display_name
.unwrap_or(metadata.display_name.as_str())
.to_string(),
category: curated.category,
description: curated
.description
.unwrap_or(metadata.description.as_str())
.to_string(),
setup_method: curated.setup_method,
docs_url: curated.docs_url.map(str::to_string).or_else(|| {
(!metadata.model_doc_link.is_empty()).then(|| metadata.model_doc_link.clone())
}),
group: curated.group,
fields: metadata
.config_keys
.iter()
.filter(|key| key.primary)
.map(|key| setup_field(key, curated))
.collect(),
aliases: curated
.aliases
.iter()
.map(|alias| alias.to_string())
.collect(),
native_connect_query: curated.native_connect_query.map(str::to_string),
binary_name: curated.binary_name.map(str::to_string),
setup_capabilities: curated.setup_capabilities,
show_only_when_installed: curated.show_only_when_installed,
}
}
fn synthetic_goose_setup_entry(curated: &CuratedSetupMetadata) -> ProviderSetupCatalogEntry {
ProviderSetupCatalogEntry {
provider_id: curated.provider_id.to_string(),
display_name: curated.display_name.unwrap_or("Goose").to_string(),
category: ProviderSetupCategory::Agent,
description: curated.description.unwrap_or_default().to_string(),
setup_method: ProviderSetupMethod::None,
docs_url: curated.docs_url.map(str::to_string),
group: curated.group,
fields: Vec::new(),
aliases: curated
.aliases
.iter()
.map(|alias| alias.to_string())
.collect(),
native_connect_query: None,
binary_name: None,
setup_capabilities: curated.setup_capabilities,
show_only_when_installed: false,
}
}
pub async fn get_providers_by_format(format: ProviderFormat) -> Vec<ProviderCatalogEntry> {
let native_provider_ids = super::init::providers()
.await
@@ -154,6 +935,34 @@ pub async fn get_providers_by_format(format: ProviderFormat) -> Vec<ProviderCata
entries
}
pub async fn get_setup_catalog_entries() -> Vec<ProviderSetupCatalogEntry> {
let registry_metadata = super::providers()
.await
.into_iter()
.map(|(metadata, _)| (metadata.name.clone(), metadata))
.collect::<HashMap<_, _>>();
SETUP_METADATA
.iter()
.filter_map(|curated| {
if curated.synthetic {
return Some(synthetic_goose_setup_entry(curated));
}
registry_metadata
.get(curated.provider_id)
.map(|metadata| setup_entry_from_metadata(curated, metadata))
})
.collect()
}
pub fn get_provider_setup_category(provider_id: &str) -> Option<ProviderSetupCategory> {
SETUP_METADATA
.iter()
.find(|curated| curated.provider_id == provider_id)
.map(|curated| curated.category)
}
pub fn get_provider_template(provider_id: &str) -> Option<ProviderTemplate> {
let metadata = PROVIDER_METADATA.get(provider_id)?;
@@ -242,4 +1051,62 @@ mod tests {
"z.ai template should have models"
);
}
#[tokio::test]
async fn setup_catalog_includes_goose_and_curated_fields() {
let entries = get_setup_catalog_entries().await;
let goose = entries
.iter()
.find(|entry| entry.provider_id == "goose")
.expect("setup catalog should include synthetic goose");
assert_eq!(goose.category, ProviderSetupCategory::Agent);
assert_eq!(goose.setup_method, ProviderSetupMethod::None);
assert!(goose.fields.is_empty());
let ollama = entries
.iter()
.find(|entry| entry.provider_id == "ollama")
.expect("setup catalog should include ollama");
assert_eq!(ollama.setup_method, ProviderSetupMethod::ConfigFields);
assert_eq!(ollama.fields.len(), 1);
assert_eq!(ollama.fields[0].key, "OLLAMA_HOST");
assert_eq!(ollama.fields[0].label, "Host");
assert_eq!(
ollama.fields[0].default_value.as_deref(),
Some("http://localhost:11434")
);
let databricks = entries
.iter()
.find(|entry| entry.provider_id == "databricks")
.expect("setup catalog should include databricks");
assert_eq!(
databricks.setup_method,
ProviderSetupMethod::HostWithOauthFallback
);
assert_eq!(
databricks
.fields
.iter()
.map(|field| field.key.as_str())
.collect::<Vec<_>>(),
["DATABRICKS_HOST", "DATABRICKS_TOKEN"]
);
}
#[tokio::test]
async fn setup_catalog_excludes_uncurated_deprecated_providers() {
let provider_ids = get_setup_catalog_entries()
.await
.into_iter()
.map(|entry| entry.provider_id)
.collect::<std::collections::HashSet<_>>();
assert!(provider_ids.contains("claude-acp"));
assert!(provider_ids.contains("codex-acp"));
assert!(!provider_ids.contains("claude_code"));
assert!(!provider_ids.contains("codex"));
assert!(!provider_ids.contains("gemini_cli"));
}
}
@@ -1,5 +1,6 @@
use super::base::{ConfigKey, ModelInfo, ProviderType};
use super::canonical::{map_provider_name, map_to_canonical_model, CanonicalModelRegistry};
use super::catalog::ProviderSetupCategory;
use crate::config::declarative_providers::{DeclarativeProviderConfig, ProviderEngine};
use crate::config::Config;
use crate::session::session_manager::SessionStorage;
@@ -23,6 +24,7 @@ pub struct ProviderInventoryEntry {
pub default_model: String,
pub configured: bool,
pub provider_type: ProviderType,
pub category: ProviderSetupCategory,
pub config_keys: Vec<ConfigKey>,
pub setup_steps: Vec<String>,
pub supports_refresh: bool,
@@ -248,6 +250,7 @@ struct ProviderDescriptor {
identity: InventoryIdentity,
configured: bool,
provider_type: ProviderType,
category: ProviderSetupCategory,
config_keys: Vec<ConfigKey>,
setup_steps: Vec<String>,
supports_refresh: bool,
@@ -289,6 +292,7 @@ impl ProviderInventoryService {
default_model: descriptor.default_model,
configured: descriptor.configured,
provider_type: descriptor.provider_type,
category: descriptor.category,
config_keys: descriptor.config_keys,
setup_steps: descriptor.setup_steps,
supports_refresh: descriptor.supports_refresh,
@@ -582,6 +586,8 @@ impl ProviderInventoryService {
identity,
configured: entry.inventory_configured(),
provider_type: entry.provider_type(),
category: crate::providers::catalog::get_provider_setup_category(&metadata.name)
.unwrap_or(ProviderSetupCategory::Model),
config_keys: metadata.config_keys.clone(),
setup_steps: metadata.setup_steps.clone(),
supports_refresh: entry.supports_inventory_refresh(),
@@ -72,6 +72,69 @@ fn acp_catalog_and_custom_provider_methods_use_core_provider_store() {
"OpenAI-compatible catalog should include z.ai"
);
let setup_catalog = send_custom(
conn.cx(),
"_goose/providers/setup/catalog/list",
serde_json::json!({}),
)
.await
.expect("provider setup catalog list should succeed");
let setup_providers = setup_catalog
.get("providers")
.and_then(|providers| providers.as_array())
.expect("setup catalog response should include providers");
for provider_id in [
"goose",
"anthropic",
"openai",
"claude-acp",
"codex-acp",
"copilot-acp",
"amp-acp",
"cursor-agent",
"pi-acp",
] {
assert!(
setup_providers
.iter()
.any(|provider| provider.get("providerId")
== Some(&serde_json::json!(provider_id))),
"setup catalog should include {provider_id}"
);
}
for provider_id in ["codex", "claude_code", "gemini_cli"] {
assert!(
setup_providers
.iter()
.all(|provider| provider.get("providerId")
!= Some(&serde_json::json!(provider_id))),
"setup catalog should exclude deprecated provider {provider_id}"
);
}
let codex_setup = setup_providers
.iter()
.find(|provider| provider.get("providerId") == Some(&serde_json::json!("codex-acp")))
.expect("setup catalog should include codex-acp");
assert_eq!(
codex_setup.get("category"),
Some(&serde_json::json!("agent"))
);
assert_eq!(
codex_setup.get("setupMethod"),
Some(&serde_json::json!("cli_auth"))
);
assert_eq!(
codex_setup.get("supportsInstall"),
Some(&serde_json::json!(true))
);
assert!(
codex_setup
.get("aliases")
.and_then(|aliases| aliases.as_array())
.is_some_and(|aliases| aliases.contains(&serde_json::json!("codex"))),
"codex-acp setup aliases should include codex"
);
let template = send_custom(
conn.cx(),
"_goose/providers/catalog/template",