consolidate provider ACP methods onto inventory (#8710)

Signed-off-by: Bradley Axen <baxen@squareup.com>
This commit is contained in:
Bradley Axen
2026-04-21 09:19:20 -07:00
committed by GitHub
parent 2067a564ce
commit dfc5b0b803
15 changed files with 275 additions and 573 deletions
-10
View File
@@ -45,16 +45,6 @@
"requestType": "ListProvidersRequest", "requestType": "ListProvidersRequest",
"responseType": "ListProvidersResponse" "responseType": "ListProvidersResponse"
}, },
{
"method": "_goose/providers/details",
"requestType": "GetProviderDetailsRequest",
"responseType": "GetProviderDetailsResponse"
},
{
"method": "_goose/providers/inventory",
"requestType": "GetProviderInventoryRequest",
"responseType": "GetProviderInventoryResponse"
},
{ {
"method": "_goose/providers/inventory/refresh", "method": "_goose/providers/inventory/refresh",
"requestType": "RefreshProviderInventoryRequest", "requestType": "RefreshProviderInventoryRequest",
+74 -206
View File
@@ -196,173 +196,6 @@
"x-method": "_goose/session/extensions" "x-method": "_goose/session/extensions"
}, },
"ListProvidersRequest": { "ListProvidersRequest": {
"type": "object",
"description": "List providers available through goose, including the config-default sentinel.",
"x-side": "agent",
"x-method": "_goose/providers/list"
},
"ListProvidersResponse": {
"type": "object",
"properties": {
"providers": {
"type": "array",
"items": {
"$ref": "#/$defs/ProviderListEntry"
}
}
},
"required": [
"providers"
],
"description": "Provider list response.",
"x-side": "agent",
"x-method": "_goose/providers/list"
},
"ProviderListEntry": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"label": {
"type": "string"
}
},
"required": [
"id",
"label"
]
},
"GetProviderDetailsRequest": {
"type": "object",
"description": "List providers with full metadata (config keys, setup steps, etc.).",
"x-side": "agent",
"x-method": "_goose/providers/details"
},
"GetProviderDetailsResponse": {
"type": "object",
"properties": {
"providers": {
"type": "array",
"items": {
"$ref": "#/$defs/ProviderDetailEntry"
}
}
},
"required": [
"providers"
],
"description": "Provider details response.",
"x-side": "agent",
"x-method": "_goose/providers/details"
},
"ProviderDetailEntry": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"displayName": {
"type": "string"
},
"description": {
"type": "string"
},
"defaultModel": {
"type": "string"
},
"isConfigured": {
"type": "boolean"
},
"providerType": {
"type": "string"
},
"configKeys": {
"type": "array",
"items": {
"$ref": "#/$defs/ProviderConfigKey"
}
},
"setupSteps": {
"type": "array",
"items": {
"type": "string"
},
"default": []
},
"knownModels": {
"type": "array",
"items": {
"$ref": "#/$defs/ModelEntry"
},
"default": []
}
},
"required": [
"name",
"displayName",
"description",
"defaultModel",
"isConfigured",
"providerType",
"configKeys"
]
},
"ProviderConfigKey": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"required": {
"type": "boolean"
},
"secret": {
"type": "boolean"
},
"default": {
"type": [
"string",
"null"
],
"default": null
},
"oauthFlow": {
"type": "boolean",
"default": false
},
"deviceCodeFlow": {
"type": "boolean",
"default": false
},
"primary": {
"type": "boolean",
"default": false
}
},
"required": [
"name",
"required",
"secret"
]
},
"ModelEntry": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"contextLimit": {
"type": "integer",
"minimum": 0
}
},
"required": [
"name",
"contextLimit"
]
},
"GetProviderInventoryRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
"providerIds": { "providerIds": {
@@ -374,11 +207,11 @@
"default": [] "default": []
} }
}, },
"description": "Read per-provider inventory. Always returns immediately from stored state.", "description": "List providers with setup metadata and the current model inventory snapshot.",
"x-side": "agent", "x-side": "agent",
"x-method": "_goose/providers/inventory" "x-method": "_goose/providers/list"
}, },
"GetProviderInventoryResponse": { "ListProvidersResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"entries": { "entries": {
@@ -391,9 +224,9 @@
"required": [ "required": [
"entries" "entries"
], ],
"description": "Provider inventory response.", "description": "Provider list response.",
"x-side": "agent", "x-side": "agent",
"x-method": "_goose/providers/inventory" "x-method": "_goose/providers/list"
}, },
"ProviderInventoryEntryDto": { "ProviderInventoryEntryDto": {
"type": "object", "type": "object",
@@ -406,10 +239,36 @@
"type": "string", "type": "string",
"description": "Human-readable provider name." "description": "Human-readable provider name."
}, },
"description": {
"type": "string",
"description": "Description of the provider's capabilities."
},
"defaultModel": {
"type": "string",
"description": "The default/recommended model for this provider."
},
"configured": { "configured": {
"type": "boolean", "type": "boolean",
"description": "Whether Goose has enough configuration to use this provider." "description": "Whether Goose has enough configuration to use this provider."
}, },
"providerType": {
"type": "string",
"description": "Provider classification such as `Preferred`, `Builtin`, `Declarative`, or `Custom`."
},
"configKeys": {
"type": "array",
"items": {
"$ref": "#/$defs/ProviderConfigKey"
},
"description": "Required configuration keys and setup metadata."
},
"setupSteps": {
"type": "array",
"items": {
"type": "string"
},
"description": "Step-by-step setup instructions, when present."
},
"supportsRefresh": { "supportsRefresh": {
"type": "boolean", "type": "boolean",
"description": "Whether this provider supports background inventory refresh." "description": "Whether this provider supports background inventory refresh."
@@ -461,7 +320,12 @@
"required": [ "required": [
"providerId", "providerId",
"providerName", "providerName",
"description",
"defaultModel",
"configured", "configured",
"providerType",
"configKeys",
"setupSteps",
"supportsRefresh", "supportsRefresh",
"refreshing", "refreshing",
"models", "models",
@@ -469,6 +333,44 @@
], ],
"description": "Provider inventory entry." "description": "Provider inventory entry."
}, },
"ProviderConfigKey": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"required": {
"type": "boolean"
},
"secret": {
"type": "boolean"
},
"default": {
"type": [
"string",
"null"
],
"default": null
},
"oauthFlow": {
"type": "boolean",
"default": false
},
"deviceCodeFlow": {
"type": "boolean",
"default": false
},
"primary": {
"type": "boolean",
"default": false
}
},
"required": [
"name",
"required",
"secret"
]
},
"ProviderInventoryModelDto": { "ProviderInventoryModelDto": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -1504,24 +1406,6 @@
"description": "Params for _goose/providers/list", "description": "Params for _goose/providers/list",
"title": "ListProvidersRequest" "title": "ListProvidersRequest"
}, },
{
"allOf": [
{
"$ref": "#/$defs/GetProviderDetailsRequest"
}
],
"description": "Params for _goose/providers/details",
"title": "GetProviderDetailsRequest"
},
{
"allOf": [
{
"$ref": "#/$defs/GetProviderInventoryRequest"
}
],
"description": "Params for _goose/providers/inventory",
"title": "GetProviderInventoryRequest"
},
{ {
"allOf": [ "allOf": [
{ {
@@ -1825,22 +1709,6 @@
], ],
"title": "ListProvidersResponse" "title": "ListProvidersResponse"
}, },
{
"allOf": [
{
"$ref": "#/$defs/GetProviderDetailsResponse"
}
],
"title": "GetProviderDetailsResponse"
},
{
"allOf": [
{
"$ref": "#/$defs/GetProviderInventoryResponse"
}
],
"title": "GetProviderInventoryResponse"
},
{ {
"allOf": [ "allOf": [
{ {
+37 -76
View File
@@ -426,7 +426,16 @@ fn inventory_entry_to_dto(entry: ProviderInventoryEntry) -> ProviderInventoryEnt
ProviderInventoryEntryDto { ProviderInventoryEntryDto {
provider_id: entry.provider_id, provider_id: entry.provider_id,
provider_name: entry.provider_name, provider_name: entry.provider_name,
description: entry.description,
default_model: entry.default_model,
configured: entry.configured, configured: entry.configured,
provider_type: format!("{:?}", entry.provider_type),
config_keys: entry
.config_keys
.into_iter()
.map(provider_config_key_to_dto)
.collect(),
setup_steps: entry.setup_steps,
supports_refresh: entry.supports_refresh, supports_refresh: entry.supports_refresh,
refreshing: entry.refreshing, refreshing: entry.refreshing,
models: entry models: entry
@@ -449,6 +458,18 @@ fn inventory_entry_to_dto(entry: ProviderInventoryEntry) -> ProviderInventoryEnt
} }
} }
fn provider_config_key_to_dto(key: goose::providers::base::ConfigKey) -> ProviderConfigKey {
ProviderConfigKey {
name: key.name,
required: key.required,
secret: key.secret,
default: key.default,
oauth_flow: key.oauth_flow,
device_code_flow: key.device_code_flow,
primary: key.primary,
}
}
fn build_model_state(current_model: &str, inventory: &ProviderInventoryEntry) -> SessionModelState { fn build_model_state(current_model: &str, inventory: &ProviderInventoryEntry) -> SessionModelState {
let mut available_models = inventory let mut available_models = inventory
.models .models
@@ -467,11 +488,16 @@ fn build_model_state(current_model: &str, inventory: &ProviderInventoryEntry) ->
SessionModelState::new(ModelId::new(current_model), available_models) SessionModelState::new(ModelId::new(current_model), available_models)
} }
async fn list_provider_entries(current_provider: Option<&str>) -> Vec<ProviderListEntry> { struct ProviderOptionEntry {
id: String,
label: String,
}
async fn list_provider_entries(current_provider: Option<&str>) -> Vec<ProviderOptionEntry> {
let mut providers = goose::providers::providers() let mut providers = goose::providers::providers()
.await .await
.into_iter() .into_iter()
.map(|(metadata, _)| ProviderListEntry { .map(|(metadata, _)| ProviderOptionEntry {
id: metadata.name, id: metadata.name,
label: metadata.display_name, label: metadata.display_name,
}) })
@@ -485,7 +511,7 @@ async fn list_provider_entries(current_provider: Option<&str>) -> Vec<ProviderLi
.iter() .iter()
.any(|provider| provider.id == current_provider) .any(|provider| provider.id == current_provider)
{ {
providers.push(ProviderListEntry { providers.push(ProviderOptionEntry {
id: current_provider.to_string(), id: current_provider.to_string(),
label: current_provider.to_string(), label: current_provider.to_string(),
}); });
@@ -494,7 +520,7 @@ async fn list_provider_entries(current_provider: Option<&str>) -> Vec<ProviderLi
} }
let mut entries = Vec::with_capacity(providers.len() + 1); let mut entries = Vec::with_capacity(providers.len() + 1);
entries.push(ProviderListEntry { entries.push(ProviderOptionEntry {
id: DEFAULT_PROVIDER_ID.to_string(), id: DEFAULT_PROVIDER_ID.to_string(),
label: DEFAULT_PROVIDER_LABEL.to_string(), label: DEFAULT_PROVIDER_LABEL.to_string(),
}); });
@@ -2783,84 +2809,14 @@ impl GooseAcpAgent {
#[custom_method(ListProvidersRequest)] #[custom_method(ListProvidersRequest)]
async fn on_list_providers( async fn on_list_providers(
&self, &self,
_req: ListProvidersRequest, req: ListProvidersRequest,
) -> Result<ListProvidersResponse, sacp::Error> { ) -> Result<ListProvidersResponse, sacp::Error> {
Ok(ListProvidersResponse {
providers: list_provider_entries(None).await,
})
}
#[custom_method(GetProviderDetailsRequest)]
async fn on_get_provider_details(
&self,
_req: GetProviderDetailsRequest,
) -> Result<GetProviderDetailsResponse, sacp::Error> {
let config = self.load_config().ok();
let all = goose::providers::providers().await;
let entries = all
.into_iter()
.map(|(metadata, provider_type)| {
let is_configured = config
.as_ref()
.map(|c| {
metadata.config_keys.iter().all(|k| {
if !k.required {
return true;
}
if k.secret {
c.get_secret::<String>(&k.name).is_ok()
} else {
c.get_param::<String>(&k.name).is_ok()
}
})
})
.unwrap_or(false);
ProviderDetailEntry {
name: metadata.name.clone(),
display_name: metadata.display_name.clone(),
description: metadata.description.clone(),
default_model: metadata.default_model.clone(),
is_configured,
provider_type: format!("{:?}", provider_type),
config_keys: metadata
.config_keys
.iter()
.map(|k| ProviderConfigKey {
name: k.name.clone(),
required: k.required,
secret: k.secret,
default: k.default.clone(),
oauth_flow: k.oauth_flow,
device_code_flow: k.device_code_flow,
primary: k.primary,
})
.collect(),
setup_steps: metadata.setup_steps.clone(),
known_models: metadata
.known_models
.iter()
.map(|m| ModelEntry {
name: m.name.clone(),
context_limit: m.context_limit,
})
.collect(),
}
})
.collect();
Ok(GetProviderDetailsResponse { providers: entries })
}
#[custom_method(GetProviderInventoryRequest)]
async fn on_get_provider_inventory(
&self,
req: GetProviderInventoryRequest,
) -> Result<GetProviderInventoryResponse, sacp::Error> {
let entries = self let entries = self
.provider_inventory .provider_inventory
.entries(&req.provider_ids) .entries(&req.provider_ids)
.await .await
.map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?;
Ok(GetProviderInventoryResponse { Ok(ListProvidersResponse {
entries: entries.into_iter().map(inventory_entry_to_dto).collect(), entries: entries.into_iter().map(inventory_entry_to_dto).collect(),
}) })
} }
@@ -4150,7 +4106,12 @@ print(\"hello, world\")
let inventory = ProviderInventoryEntry { let inventory = ProviderInventoryEntry {
provider_id: "mock".to_string(), provider_id: "mock".to_string(),
provider_name: "Mock".to_string(), provider_name: "Mock".to_string(),
description: "Mock".to_string(),
default_model: "unused".to_string(),
configured: true, configured: true,
provider_type: goose::providers::base::ProviderType::Builtin,
config_keys: vec![],
setup_steps: vec![],
supports_refresh: true, supports_refresh: true,
refreshing: false, refreshing: false,
models: models models: models
+14 -17
View File
@@ -112,32 +112,29 @@ fn test_custom_get_extensions() {
} }
#[test] #[test]
fn test_custom_list_providers() { fn test_custom_provider_inventory_includes_metadata() {
run_test(async { run_test(async {
let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await; let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
let conn = AcpServerConnection::new(TestConnectionConfig::default(), openai).await; let conn = AcpServerConnection::new(TestConnectionConfig::default(), openai).await;
let response = send_custom(conn.cx(), "_goose/providers/list", serde_json::json!({})) let response = send_custom(conn.cx(), "_goose/providers/list", serde_json::json!({}))
.await .await
.expect("provider list should succeed"); .expect("provider inventory should succeed");
let providers = response let providers = response
.get("providers") .get("entries")
.and_then(|value| value.as_array()) .and_then(|value| value.as_array())
.expect("missing providers array"); .expect("missing entries array");
let openai = providers
.iter()
.find(|provider| provider.get("providerId") == Some(&serde_json::json!("openai")))
.expect("expected openai inventory entry");
assert!( assert!(openai.get("providerName").is_some(), "missing providerName");
providers.iter().any(|provider| { assert!(openai.get("description").is_some(), "missing description");
provider.get("id") == Some(&serde_json::json!("goose")) assert!(openai.get("defaultModel").is_some(), "missing defaultModel");
&& provider.get("label") == Some(&serde_json::json!("Goose (Default)")) assert!(openai.get("providerType").is_some(), "missing providerType");
}), assert!(openai.get("configKeys").is_some(), "missing configKeys");
"expected Goose default provider sentinel" assert!(openai.get("setupSteps").is_some(), "missing setupSteps");
);
assert!(
providers
.iter()
.any(|provider| provider.get("id") == Some(&serde_json::json!("openai"))),
"expected at least one concrete provider from the goose registry"
);
}); });
} }
+15 -60
View File
@@ -181,24 +181,6 @@ pub struct RemoveSecretRequest {
pub key: String, pub key: String,
} }
/// List providers available through goose, including the config-default sentinel.
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(method = "_goose/providers/list", response = ListProvidersResponse)]
pub struct ListProvidersRequest {}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ProviderListEntry {
pub id: String,
pub label: String,
}
/// Provider list response.
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
pub struct ListProvidersResponse {
pub providers: Vec<ProviderListEntry>,
}
/// Archive a session (soft delete). /// Archive a session (soft delete).
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(method = "_goose/session/archive", response = EmptyResponse)] #[request(method = "_goose/session/archive", response = EmptyResponse)]
@@ -246,40 +228,6 @@ pub struct ImportSessionResponse {
pub message_count: u64, pub message_count: u64,
} }
/// List providers with full metadata (config keys, setup steps, etc.).
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(method = "_goose/providers/details", response = GetProviderDetailsResponse)]
pub struct GetProviderDetailsRequest {}
/// Provider details response.
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
pub struct GetProviderDetailsResponse {
pub providers: Vec<ProviderDetailEntry>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ProviderDetailEntry {
pub name: String,
pub display_name: String,
pub description: String,
pub default_model: String,
pub is_configured: bool,
pub provider_type: String,
pub config_keys: Vec<ProviderConfigKey>,
#[serde(default)]
pub setup_steps: Vec<String>,
#[serde(default)]
pub known_models: Vec<ModelEntry>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ModelEntry {
pub name: String,
pub context_limit: usize,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ProviderConfigKey { pub struct ProviderConfigKey {
@@ -494,22 +442,19 @@ pub struct DictationConfigResponse {
pub providers: HashMap<String, DictationProviderStatusEntry>, pub providers: HashMap<String, DictationProviderStatusEntry>,
} }
/// Read per-provider inventory. Always returns immediately from stored state. /// List providers with setup metadata and the current model inventory snapshot.
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request( #[request(method = "_goose/providers/list", response = ListProvidersResponse)]
method = "_goose/providers/inventory",
response = GetProviderInventoryResponse
)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct GetProviderInventoryRequest { pub struct ListProvidersRequest {
/// Only return entries for these providers. Empty means all. /// Only return entries for these providers. Empty means all.
#[serde(default)] #[serde(default)]
pub provider_ids: Vec<String>, pub provider_ids: Vec<String>,
} }
/// Provider inventory response. /// Provider list response.
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
pub struct GetProviderInventoryResponse { pub struct ListProvidersResponse {
pub entries: Vec<ProviderInventoryEntryDto>, pub entries: Vec<ProviderInventoryEntryDto>,
} }
@@ -584,8 +529,18 @@ pub struct ProviderInventoryEntryDto {
pub provider_id: String, pub provider_id: String,
/// Human-readable provider name. /// Human-readable provider name.
pub provider_name: String, pub provider_name: String,
/// Description of the provider's capabilities.
pub description: String,
/// The default/recommended model for this provider.
pub default_model: String,
/// Whether Goose has enough configuration to use this provider. /// Whether Goose has enough configuration to use this provider.
pub configured: bool, pub configured: bool,
/// Provider classification such as `Preferred`, `Builtin`, `Declarative`, or `Custom`.
pub provider_type: String,
/// Required configuration keys and setup metadata.
pub config_keys: Vec<ProviderConfigKey>,
/// Step-by-step setup instructions, when present.
pub setup_steps: Vec<String>,
/// Whether this provider supports background inventory refresh. /// Whether this provider supports background inventory refresh.
pub supports_refresh: bool, pub supports_refresh: bool,
/// Whether a refresh is currently in flight. /// Whether a refresh is currently in flight.
+21 -1
View File
@@ -1,4 +1,4 @@
use super::base::{ConfigKey, ModelInfo}; use super::base::{ConfigKey, ModelInfo, ProviderType};
use super::canonical::{map_provider_name, map_to_canonical_model, CanonicalModelRegistry}; use super::canonical::{map_provider_name, map_to_canonical_model, CanonicalModelRegistry};
use crate::config::declarative_providers::{DeclarativeProviderConfig, ProviderEngine}; use crate::config::declarative_providers::{DeclarativeProviderConfig, ProviderEngine};
use crate::config::Config; use crate::config::Config;
@@ -19,7 +19,12 @@ const STALE_AFTER_HOURS: i64 = 24;
pub struct ProviderInventoryEntry { pub struct ProviderInventoryEntry {
pub provider_id: String, pub provider_id: String,
pub provider_name: String, pub provider_name: String,
pub description: String,
pub default_model: String,
pub configured: bool, pub configured: bool,
pub provider_type: ProviderType,
pub config_keys: Vec<ConfigKey>,
pub setup_steps: Vec<String>,
pub supports_refresh: bool, pub supports_refresh: bool,
pub refreshing: bool, pub refreshing: bool,
pub models: Vec<InventoryModel>, pub models: Vec<InventoryModel>,
@@ -162,8 +167,13 @@ struct InventorySnapshot {
struct ProviderDescriptor { struct ProviderDescriptor {
provider_id: String, provider_id: String,
provider_name: String, provider_name: String,
description: String,
default_model: String,
identity: InventoryIdentity, identity: InventoryIdentity,
configured: bool, configured: bool,
provider_type: ProviderType,
config_keys: Vec<ConfigKey>,
setup_steps: Vec<String>,
supports_refresh: bool, supports_refresh: bool,
static_models: Vec<ModelInfo>, static_models: Vec<ModelInfo>,
model_selection_hint: Option<String>, model_selection_hint: Option<String>,
@@ -199,7 +209,12 @@ impl ProviderInventoryService {
Ok(Some(ProviderInventoryEntry { Ok(Some(ProviderInventoryEntry {
provider_id: descriptor.provider_id, provider_id: descriptor.provider_id,
provider_name: descriptor.provider_name, provider_name: descriptor.provider_name,
description: descriptor.description,
default_model: descriptor.default_model,
configured: descriptor.configured, configured: descriptor.configured,
provider_type: descriptor.provider_type,
config_keys: descriptor.config_keys,
setup_steps: descriptor.setup_steps,
supports_refresh: descriptor.supports_refresh, supports_refresh: descriptor.supports_refresh,
refreshing, refreshing,
models, models,
@@ -419,8 +434,13 @@ impl ProviderInventoryService {
Ok(Some(ProviderDescriptor { Ok(Some(ProviderDescriptor {
provider_id: metadata.name.clone(), provider_id: metadata.name.clone(),
provider_name: metadata.display_name.clone(), provider_name: metadata.display_name.clone(),
description: metadata.description.clone(),
default_model: metadata.default_model.clone(),
identity, identity,
configured: entry.inventory_configured(), configured: entry.inventory_configured(),
provider_type: entry.provider_type(),
config_keys: metadata.config_keys.clone(),
setup_steps: metadata.setup_steps.clone(),
supports_refresh: entry.supports_inventory_refresh(), supports_refresh: entry.supports_inventory_refresh(),
static_models: metadata.known_models, static_models: metadata.known_models,
model_selection_hint: metadata.model_selection_hint, model_selection_hint: metadata.model_selection_hint,
@@ -10,7 +10,7 @@ export async function getProviderInventory(
): Promise<ProviderInventoryEntryDto[]> { ): Promise<ProviderInventoryEntryDto[]> {
const client = await getClient(); const client = await getClient();
const t0 = performance.now(); const t0 = performance.now();
const response = await client.goose.GooseProvidersInventory({ providerIds }); const response = await client.goose.GooseProvidersList({ providerIds });
perfLog( perfLog(
`[perf:inventory] getProviderInventory done in ${(performance.now() - t0).toFixed(1)}ms (n=${response.entries.length})`, `[perf:inventory] getProviderInventory done in ${(performance.now() - t0).toFixed(1)}ms (n=${response.entries.length})`,
); );
+16 -7
View File
@@ -20,16 +20,25 @@ export interface AcpSessionInfo {
} }
const DEPRECATED_PROVIDER_IDS = new Set(["claude-code", "codex", "gemini-cli"]); const DEPRECATED_PROVIDER_IDS = new Set(["claude-code", "codex", "gemini-cli"]);
const DEFAULT_PROVIDER: AcpProvider = {
id: "goose",
label: "Goose (Default)",
};
export async function listProviders(): Promise<AcpProvider[]> { export async function listProviders(): Promise<AcpProvider[]> {
const client = await getClient(); const client = await getClient();
const result = await client.goose.GooseProvidersList({}); const result = await client.goose.GooseProvidersList({
// biome-ignore lint/suspicious/noExplicitAny: ACP SDK types don't expose providers field providerIds: [],
return (result as any).providers });
.filter(
(p: { id: string; label: string }) => !DEPRECATED_PROVIDER_IDS.has(p.id), const providers = result.entries
) .filter((entry) => !DEPRECATED_PROVIDER_IDS.has(entry.providerId))
.map((p: { id: string; label: string }) => ({ id: p.id, label: p.label })); .map((entry) => ({
id: entry.providerId,
label: entry.providerName,
}));
return [DEFAULT_PROVIDER, ...providers];
} }
export async function listSessions(): Promise<AcpSessionInfo[]> { export async function listSessions(): Promise<AcpSessionInfo[]> {
@@ -126,8 +126,6 @@ export function buildInitScript(options?: {
return jsonRpcResult(message.id, { stopReason: "end_turn" }); return jsonRpcResult(message.id, { stopReason: "end_turn" });
} }
case "_goose/providers/list": case "_goose/providers/list":
return jsonRpcResult(message.id, { providers: [] });
case "_goose/providers/inventory":
return jsonRpcResult(message.id, { entries: [] }); return jsonRpcResult(message.id, { entries: [] });
case "_goose/providers/inventory/refresh": case "_goose/providers/inventory/refresh":
return jsonRpcResult(message.id, { started: [], skipped: [] }); return jsonRpcResult(message.id, { started: [], skipped: [] });
-22
View File
@@ -34,10 +34,6 @@ import type {
ExportSourceResponse, ExportSourceResponse,
GetExtensionsRequest, GetExtensionsRequest,
GetExtensionsResponse, GetExtensionsResponse,
GetProviderDetailsRequest,
GetProviderDetailsResponse,
GetProviderInventoryRequest,
GetProviderInventoryResponse,
GetSessionExtensionsRequest, GetSessionExtensionsRequest,
GetSessionExtensionsResponse, GetSessionExtensionsResponse,
GetToolsRequest, GetToolsRequest,
@@ -76,8 +72,6 @@ import {
zExportSessionResponse, zExportSessionResponse,
zExportSourceResponse, zExportSourceResponse,
zGetExtensionsResponse, zGetExtensionsResponse,
zGetProviderDetailsResponse,
zGetProviderInventoryResponse,
zGetSessionExtensionsResponse, zGetSessionExtensionsResponse,
zGetToolsResponse, zGetToolsResponse,
zImportSessionResponse, zImportSessionResponse,
@@ -144,22 +138,6 @@ export class GooseExtClient {
return zListProvidersResponse.parse(raw) as ListProvidersResponse; return zListProvidersResponse.parse(raw) as ListProvidersResponse;
} }
async GooseProvidersDetails(
params: GetProviderDetailsRequest,
): Promise<GetProviderDetailsResponse> {
const raw = await this.conn.extMethod("_goose/providers/details", params);
return zGetProviderDetailsResponse.parse(raw) as GetProviderDetailsResponse;
}
async GooseProvidersInventory(
params: GetProviderInventoryRequest,
): Promise<GetProviderInventoryResponse> {
const raw = await this.conn.extMethod("_goose/providers/inventory", params);
return zGetProviderInventoryResponse.parse(
raw,
) as GetProviderInventoryResponse;
}
async GooseProvidersInventoryRefresh( async GooseProvidersInventoryRefresh(
params: RefreshProviderInventoryRequest, params: RefreshProviderInventoryRequest,
): Promise<RefreshProviderInventoryResponse> { ): Promise<RefreshProviderInventoryResponse> {
+1 -11
View File
@@ -1,6 +1,6 @@
// This file is auto-generated by @hey-api/openapi-ts // This file is auto-generated by @hey-api/openapi-ts
export type { AddExtensionRequest, ArchiveSessionRequest, CheckSecretRequest, CheckSecretResponse, CreateSourceRequest, CreateSourceResponse, DeleteSessionRequest, DeleteSourceRequest, DictationConfigRequest, DictationConfigResponse, DictationDownloadProgress, DictationLocalModelStatus, DictationModelCancelRequest, DictationModelDeleteRequest, DictationModelDownloadProgressRequest, DictationModelDownloadProgressResponse, DictationModelDownloadRequest, DictationModelOption, DictationModelSelectRequest, DictationModelsListRequest, DictationModelsListResponse, DictationProviderStatusEntry, DictationTranscribeRequest, DictationTranscribeResponse, EmptyResponse, ExportSessionRequest, ExportSessionResponse, ExportSourceRequest, ExportSourceResponse, ExtRequest, ExtResponse, GetExtensionsRequest, GetExtensionsResponse, GetProviderDetailsRequest, GetProviderDetailsResponse, GetProviderInventoryRequest, GetProviderInventoryResponse, GetSessionExtensionsRequest, GetSessionExtensionsResponse, GetToolsRequest, GetToolsResponse, ImportSessionRequest, ImportSessionResponse, ImportSourcesRequest, ImportSourcesResponse, ListProvidersRequest, ListProvidersResponse, ListSourcesRequest, ListSourcesResponse, ModelEntry, ProviderConfigKey, ProviderDetailEntry, ProviderInventoryEntryDto, ProviderInventoryModelDto, ProviderListEntry, ReadConfigRequest, ReadConfigResponse, ReadResourceRequest, ReadResourceResponse, RefreshProviderInventoryRequest, RefreshProviderInventoryResponse, RefreshProviderInventorySkipDto, RefreshProviderInventorySkipReasonDto, RemoveConfigRequest, RemoveExtensionRequest, RemoveSecretRequest, SourceEntry, SourceType, UnarchiveSessionRequest, UpdateSourceRequest, UpdateSourceResponse, UpdateWorkingDirRequest, UpsertConfigRequest, UpsertSecretRequest } from './types.gen.js'; export type { AddExtensionRequest, ArchiveSessionRequest, CheckSecretRequest, CheckSecretResponse, CreateSourceRequest, CreateSourceResponse, DeleteSessionRequest, DeleteSourceRequest, DictationConfigRequest, DictationConfigResponse, DictationDownloadProgress, DictationLocalModelStatus, DictationModelCancelRequest, DictationModelDeleteRequest, DictationModelDownloadProgressRequest, DictationModelDownloadProgressResponse, DictationModelDownloadRequest, DictationModelOption, DictationModelSelectRequest, DictationModelsListRequest, DictationModelsListResponse, DictationProviderStatusEntry, DictationTranscribeRequest, DictationTranscribeResponse, EmptyResponse, ExportSessionRequest, ExportSessionResponse, ExportSourceRequest, ExportSourceResponse, ExtRequest, ExtResponse, GetExtensionsRequest, GetExtensionsResponse, GetSessionExtensionsRequest, GetSessionExtensionsResponse, GetToolsRequest, GetToolsResponse, ImportSessionRequest, ImportSessionResponse, ImportSourcesRequest, ImportSourcesResponse, ListProvidersRequest, ListProvidersResponse, ListSourcesRequest, ListSourcesResponse, ProviderConfigKey, ProviderInventoryEntryDto, ProviderInventoryModelDto, ReadConfigRequest, ReadConfigResponse, ReadResourceRequest, ReadResourceResponse, RefreshProviderInventoryRequest, RefreshProviderInventoryResponse, RefreshProviderInventorySkipDto, RefreshProviderInventorySkipReasonDto, RemoveConfigRequest, RemoveExtensionRequest, RemoveSecretRequest, SourceEntry, SourceType, UnarchiveSessionRequest, UpdateSourceRequest, UpdateSourceResponse, UpdateWorkingDirRequest, UpsertConfigRequest, UpsertSecretRequest } from './types.gen.js';
export const GOOSE_EXT_METHODS = [ export const GOOSE_EXT_METHODS = [
{ {
@@ -48,16 +48,6 @@ export const GOOSE_EXT_METHODS = [
requestType: "ListProvidersRequest", requestType: "ListProvidersRequest",
responseType: "ListProvidersResponse", responseType: "ListProvidersResponse",
}, },
{
method: "_goose/providers/details",
requestType: "GetProviderDetailsRequest",
responseType: "GetProviderDetailsResponse",
},
{
method: "_goose/providers/inventory",
requestType: "GetProviderInventoryRequest",
responseType: "GetProviderInventoryResponse",
},
{ {
method: "_goose/providers/inventory/refresh", method: "_goose/providers/inventory/refresh",
requestType: "RefreshProviderInventoryRequest", requestType: "RefreshProviderInventoryRequest",
+35 -65
View File
@@ -105,69 +105,9 @@ export type GetSessionExtensionsResponse = {
}; };
/** /**
* List providers available through goose, including the config-default sentinel. * List providers with setup metadata and the current model inventory snapshot.
*/ */
export type ListProvidersRequest = { export type ListProvidersRequest = {
[key: string]: unknown;
};
/**
* Provider list response.
*/
export type ListProvidersResponse = {
providers: Array<ProviderListEntry>;
};
export type ProviderListEntry = {
id: string;
label: string;
};
/**
* List providers with full metadata (config keys, setup steps, etc.).
*/
export type GetProviderDetailsRequest = {
[key: string]: unknown;
};
/**
* Provider details response.
*/
export type GetProviderDetailsResponse = {
providers: Array<ProviderDetailEntry>;
};
export type ProviderDetailEntry = {
name: string;
displayName: string;
description: string;
defaultModel: string;
isConfigured: boolean;
providerType: string;
configKeys: Array<ProviderConfigKey>;
setupSteps?: Array<string>;
knownModels?: Array<ModelEntry>;
};
export type ProviderConfigKey = {
name: string;
required: boolean;
secret: boolean;
default?: string | null;
oauthFlow?: boolean;
deviceCodeFlow?: boolean;
primary?: boolean;
};
export type ModelEntry = {
name: string;
contextLimit: number;
};
/**
* Read per-provider inventory. Always returns immediately from stored state.
*/
export type GetProviderInventoryRequest = {
/** /**
* Only return entries for these providers. Empty means all. * Only return entries for these providers. Empty means all.
*/ */
@@ -175,9 +115,9 @@ export type GetProviderInventoryRequest = {
}; };
/** /**
* Provider inventory response. * Provider list response.
*/ */
export type GetProviderInventoryResponse = { export type ListProvidersResponse = {
entries: Array<ProviderInventoryEntryDto>; entries: Array<ProviderInventoryEntryDto>;
}; };
@@ -193,10 +133,30 @@ export type ProviderInventoryEntryDto = {
* Human-readable provider name. * Human-readable provider name.
*/ */
providerName: string; providerName: string;
/**
* Description of the provider's capabilities.
*/
description: string;
/**
* The default/recommended model for this provider.
*/
defaultModel: string;
/** /**
* Whether Goose has enough configuration to use this provider. * Whether Goose has enough configuration to use this provider.
*/ */
configured: boolean; configured: boolean;
/**
* Provider classification such as `Preferred`, `Builtin`, `Declarative`, or `Custom`.
*/
providerType: string;
/**
* Required configuration keys and setup metadata.
*/
configKeys: Array<ProviderConfigKey>;
/**
* Step-by-step setup instructions, when present.
*/
setupSteps: Array<string>;
/** /**
* Whether this provider supports background inventory refresh. * Whether this provider supports background inventory refresh.
*/ */
@@ -231,6 +191,16 @@ export type ProviderInventoryEntryDto = {
modelSelectionHint?: string | null; modelSelectionHint?: string | null;
}; };
export type ProviderConfigKey = {
name: string;
required: boolean;
secret: boolean;
default?: string | null;
oauthFlow?: boolean;
deviceCodeFlow?: boolean;
primary?: boolean;
};
/** /**
* A single model in provider inventory. * A single model in provider inventory.
*/ */
@@ -648,14 +618,14 @@ export type DictationModelSelectRequest = {
export type ExtRequest = { export type ExtRequest = {
id: string; id: string;
method: string; method: string;
params?: AddExtensionRequest | RemoveExtensionRequest | GetToolsRequest | ReadResourceRequest | UpdateWorkingDirRequest | DeleteSessionRequest | GetExtensionsRequest | GetSessionExtensionsRequest | ListProvidersRequest | GetProviderDetailsRequest | GetProviderInventoryRequest | RefreshProviderInventoryRequest | ReadConfigRequest | UpsertConfigRequest | RemoveConfigRequest | CheckSecretRequest | UpsertSecretRequest | RemoveSecretRequest | ExportSessionRequest | ImportSessionRequest | ArchiveSessionRequest | UnarchiveSessionRequest | CreateSourceRequest | ListSourcesRequest | UpdateSourceRequest | DeleteSourceRequest | ExportSourceRequest | ImportSourcesRequest | DictationTranscribeRequest | DictationConfigRequest | DictationModelsListRequest | DictationModelDownloadRequest | DictationModelDownloadProgressRequest | DictationModelCancelRequest | DictationModelDeleteRequest | DictationModelSelectRequest | { params?: AddExtensionRequest | RemoveExtensionRequest | GetToolsRequest | ReadResourceRequest | UpdateWorkingDirRequest | DeleteSessionRequest | GetExtensionsRequest | GetSessionExtensionsRequest | ListProvidersRequest | RefreshProviderInventoryRequest | ReadConfigRequest | UpsertConfigRequest | RemoveConfigRequest | CheckSecretRequest | UpsertSecretRequest | RemoveSecretRequest | ExportSessionRequest | ImportSessionRequest | ArchiveSessionRequest | UnarchiveSessionRequest | CreateSourceRequest | ListSourcesRequest | UpdateSourceRequest | DeleteSourceRequest | ExportSourceRequest | ImportSourcesRequest | DictationTranscribeRequest | DictationConfigRequest | DictationModelsListRequest | DictationModelDownloadRequest | DictationModelDownloadProgressRequest | DictationModelCancelRequest | DictationModelDeleteRequest | DictationModelSelectRequest | {
[key: string]: unknown; [key: string]: unknown;
} | null; } | null;
}; };
export type ExtResponse = { export type ExtResponse = {
id: string; id: string;
result?: EmptyResponse | GetToolsResponse | ReadResourceResponse | GetExtensionsResponse | GetSessionExtensionsResponse | ListProvidersResponse | GetProviderDetailsResponse | GetProviderInventoryResponse | RefreshProviderInventoryResponse | ReadConfigResponse | CheckSecretResponse | ExportSessionResponse | ImportSessionResponse | CreateSourceResponse | ListSourcesResponse | UpdateSourceResponse | ExportSourceResponse | ImportSourcesResponse | DictationTranscribeResponse | DictationConfigResponse | DictationModelsListResponse | DictationModelDownloadProgressResponse | unknown; result?: EmptyResponse | GetToolsResponse | ReadResourceResponse | GetExtensionsResponse | GetSessionExtensionsResponse | ListProvidersResponse | RefreshProviderInventoryResponse | ReadConfigResponse | CheckSecretResponse | ExportSessionResponse | ImportSessionResponse | CreateSourceResponse | ListSourcesResponse | UpdateSourceResponse | ExportSourceResponse | ImportSourcesResponse | DictationTranscribeResponse | DictationConfigResponse | DictationModelsListResponse | DictationModelDownloadProgressResponse | unknown;
} | { } | {
error: { error: {
code: number; code: number;
+10 -55
View File
@@ -90,27 +90,12 @@ export const zGetSessionExtensionsResponse = z.object({
}); });
/** /**
* List providers available through goose, including the config-default sentinel. * List providers with setup metadata and the current model inventory snapshot.
*/ */
export const zListProvidersRequest = z.record(z.unknown()); export const zListProvidersRequest = z.object({
providerIds: z.array(z.string()).optional().default([])
export const zProviderListEntry = z.object({
id: z.string(),
label: z.string()
}); });
/**
* Provider list response.
*/
export const zListProvidersResponse = z.object({
providers: z.array(zProviderListEntry)
});
/**
* List providers with full metadata (config keys, setup steps, etc.).
*/
export const zGetProviderDetailsRequest = z.record(z.unknown());
export const zProviderConfigKey = z.object({ export const zProviderConfigKey = z.object({
name: z.string(), name: z.string(),
required: z.boolean(), required: z.boolean(),
@@ -124,37 +109,6 @@ export const zProviderConfigKey = z.object({
primary: z.boolean().optional().default(false) primary: z.boolean().optional().default(false)
}); });
export const zModelEntry = z.object({
name: z.string(),
contextLimit: z.number().int().gte(0)
});
export const zProviderDetailEntry = z.object({
name: z.string(),
displayName: z.string(),
description: z.string(),
defaultModel: z.string(),
isConfigured: z.boolean(),
providerType: z.string(),
configKeys: z.array(zProviderConfigKey),
setupSteps: z.array(z.string()).optional().default([]),
knownModels: z.array(zModelEntry).optional().default([])
});
/**
* Provider details response.
*/
export const zGetProviderDetailsResponse = z.object({
providers: z.array(zProviderDetailEntry)
});
/**
* Read per-provider inventory. Always returns immediately from stored state.
*/
export const zGetProviderInventoryRequest = z.object({
providerIds: z.array(z.string()).optional().default([])
});
/** /**
* A single model in provider inventory. * A single model in provider inventory.
*/ */
@@ -182,7 +136,12 @@ export const zProviderInventoryModelDto = z.object({
export const zProviderInventoryEntryDto = z.object({ export const zProviderInventoryEntryDto = z.object({
providerId: z.string(), providerId: z.string(),
providerName: z.string(), providerName: z.string(),
description: z.string(),
defaultModel: z.string(),
configured: z.boolean(), configured: z.boolean(),
providerType: z.string(),
configKeys: z.array(zProviderConfigKey),
setupSteps: z.array(z.string()),
supportsRefresh: z.boolean(), supportsRefresh: z.boolean(),
refreshing: z.boolean(), refreshing: z.boolean(),
models: z.array(zProviderInventoryModelDto), models: z.array(zProviderInventoryModelDto),
@@ -206,9 +165,9 @@ export const zProviderInventoryEntryDto = z.object({
}); });
/** /**
* Provider inventory response. * Provider list response.
*/ */
export const zGetProviderInventoryResponse = z.object({ export const zListProvidersResponse = z.object({
entries: z.array(zProviderInventoryEntryDto) entries: z.array(zProviderInventoryEntryDto)
}); });
@@ -626,8 +585,6 @@ export const zExtRequest = z.object({
zGetExtensionsRequest, zGetExtensionsRequest,
zGetSessionExtensionsRequest, zGetSessionExtensionsRequest,
zListProvidersRequest, zListProvidersRequest,
zGetProviderDetailsRequest,
zGetProviderInventoryRequest,
zRefreshProviderInventoryRequest, zRefreshProviderInventoryRequest,
zReadConfigRequest, zReadConfigRequest,
zUpsertConfigRequest, zUpsertConfigRequest,
@@ -672,8 +629,6 @@ export const zExtResponse = z.union([
zGetExtensionsResponse, zGetExtensionsResponse,
zGetSessionExtensionsResponse, zGetSessionExtensionsResponse,
zListProvidersResponse, zListProvidersResponse,
zGetProviderDetailsResponse,
zGetProviderInventoryResponse,
zRefreshProviderInventoryResponse, zRefreshProviderInventoryResponse,
zReadConfigResponse, zReadConfigResponse,
zCheckSecretResponse, zCheckSecretResponse,
+25 -18
View File
@@ -1,6 +1,6 @@
import React, { useState, useEffect, useCallback } from "react"; import React, { useState, useEffect, useCallback } from "react";
import { Box, Text, useInput, useStdout } from "ink"; import { Box, Text, useInput, useStdout } from "ink";
import type { GooseClient, ProviderDetailEntry } from "@aaif/goose-sdk"; import type { GooseClient, ProviderInventoryEntryDto } from "@aaif/goose-sdk";
import { import {
CRANBERRY, CRANBERRY,
TEAL, TEAL,
@@ -38,7 +38,7 @@ interface ConfigureProps {
interface ModelSelectorProps { interface ModelSelectorProps {
client: GooseClient; client: GooseClient;
provider: ProviderDetailEntry; provider: ProviderInventoryEntryDto;
height: number; height: number;
onSelect: (model: string) => void; onSelect: (model: string) => void;
onBack: () => void; onBack: () => void;
@@ -74,7 +74,7 @@ const ModelSelector = React.memo(function ModelSelector({
setLoading(true); setLoading(true);
setError(null); setError(null);
const resp = await client.goose.GooseProvidersModels({ const resp = await client.goose.GooseProvidersModels({
providerName: provider.name, providerName: provider.providerId,
}); });
if (!cancelled) { if (!cancelled) {
setModels(resp.models); setModels(resp.models);
@@ -96,7 +96,7 @@ const ModelSelector = React.memo(function ModelSelector({
cancelled = true; cancelled = true;
clearTimeout(timeoutId); clearTimeout(timeoutId);
}; };
}, [client, provider.name, provider.defaultModel]); }, [client, provider.providerId, provider.defaultModel]);
const filtered = (() => { const filtered = (() => {
if (!searchQuery) return models; if (!searchQuery) return models;
@@ -190,7 +190,7 @@ const ModelSelector = React.memo(function ModelSelector({
<Text color={TEXT_PRIMARY} bold> Select model </Text> <Text color={TEXT_PRIMARY} bold> Select model </Text>
</Box> </Box>
<Box justifyContent="center" marginBottom={2}> <Box justifyContent="center" marginBottom={2}>
<Text color={TEXT_DIM}>Loading models for {provider.displayName}</Text> <Text color={TEXT_DIM}>Loading models for {provider.providerName}</Text>
</Box> </Box>
<Box justifyContent="center" flexGrow={1} alignItems="center"> <Box justifyContent="center" flexGrow={1} alignItems="center">
<Spinner idx={0} /> <Spinner idx={0} />
@@ -235,7 +235,7 @@ const ModelSelector = React.memo(function ModelSelector({
<Text color={TEXT_PRIMARY} bold> Enter model name </Text> <Text color={TEXT_PRIMARY} bold> Enter model name </Text>
</Box> </Box>
<Box justifyContent="center" marginBottom={2}> <Box justifyContent="center" marginBottom={2}>
<Text color={TEXT_DIM}>Type a model identifier for {provider.displayName}</Text> <Text color={TEXT_DIM}>Type a model identifier for {provider.providerName}</Text>
</Box> </Box>
<Box justifyContent="center"> <Box justifyContent="center">
@@ -270,7 +270,7 @@ const ModelSelector = React.memo(function ModelSelector({
<Text color={TEXT_PRIMARY} bold> Select model </Text> <Text color={TEXT_PRIMARY} bold> Select model </Text>
</Box> </Box>
<Box justifyContent="center" marginBottom={2}> <Box justifyContent="center" marginBottom={2}>
<Text color={TEXT_DIM}>Choose a model for {provider.displayName}</Text> <Text color={TEXT_DIM}>Choose a model for {provider.providerName}</Text>
</Box> </Box>
{/* Search Bar */} {/* Search Bar */}
@@ -359,8 +359,8 @@ export default function ConfigureScreen({
initialIntent, initialIntent,
}: ConfigureProps) { }: ConfigureProps) {
const [phase, setPhase] = useState<Phase>("loading"); const [phase, setPhase] = useState<Phase>("loading");
const [providers, setProviders] = useState<ProviderDetailEntry[]>([]); const [providers, setProviders] = useState<ProviderInventoryEntryDto[]>([]);
const [selectedProvider, setSelectedProvider] = useState<ProviderDetailEntry | null>(null); const [selectedProvider, setSelectedProvider] = useState<ProviderInventoryEntryDto | null>(null);
const [errorMsg, setErrorMsg] = useState(""); const [errorMsg, setErrorMsg] = useState("");
const [spinIdx, setSpinIdx] = useState(0); const [spinIdx, setSpinIdx] = useState(0);
const [fetchKey, setFetchKey] = useState(0); const [fetchKey, setFetchKey] = useState(0);
@@ -378,13 +378,13 @@ export default function ConfigureScreen({
(async () => { (async () => {
try { try {
const resp = await client.goose.GooseProvidersDetails({}); const resp = await client.goose.GooseProvidersList({ providerIds: [] });
if (cancelled) return; if (cancelled) return;
const sorted = [...resp.providers].sort((a, b) => { const sorted = [...resp.entries].sort((a, b) => {
const aP = a.providerType === "Preferred" ? 0 : 1; const aP = a.providerType === "Preferred" ? 0 : 1;
const bP = b.providerType === "Preferred" ? 0 : 1; const bP = b.providerType === "Preferred" ? 0 : 1;
if (aP !== bP) return aP - bP; if (aP !== bP) return aP - bP;
return a.displayName.localeCompare(b.displayName); return a.providerName.localeCompare(b.providerName);
}); });
setProviders(sorted); setProviders(sorted);
@@ -392,7 +392,7 @@ export default function ConfigureScreen({
try { try {
const cfg = await client.goose.GooseConfigRead({ key: "GOOSE_PROVIDER" }); const cfg = await client.goose.GooseConfigRead({ key: "GOOSE_PROVIDER" });
if (cancelled) return; if (cancelled) return;
const current = sorted.find((p) => p.name === cfg.value); const current = sorted.find((p) => p.providerId === cfg.value);
if (current) { if (current) {
setSelectedProvider(current); setSelectedProvider(current);
setPendingConfigValues({}); setPendingConfigValues({});
@@ -419,7 +419,11 @@ export default function ConfigureScreen({
}, [client, fetchKey, initialIntent]); }, [client, fetchKey, initialIntent]);
const applyProviderModel = useCallback( const applyProviderModel = useCallback(
async (provider: ProviderDetailEntry, model: string, configValues: Record<string, string>) => { async (
provider: ProviderInventoryEntryDto,
model: string,
configValues: Record<string, string>,
) => {
setPhase("saving"); setPhase("saving");
try { try {
for (const [key, value] of Object.entries(configValues)) { for (const [key, value] of Object.entries(configValues)) {
@@ -430,11 +434,14 @@ export default function ConfigureScreen({
await client.goose.GooseConfigUpsert({ key, value }); await client.goose.GooseConfigUpsert({ key, value });
} }
} }
await client.goose.GooseConfigUpsert({ key: "GOOSE_PROVIDER", value: provider.name }); await client.goose.GooseConfigUpsert({
key: "GOOSE_PROVIDER",
value: provider.providerId,
});
await client.goose.GooseConfigUpsert({ key: "GOOSE_MODEL", value: model }); await client.goose.GooseConfigUpsert({ key: "GOOSE_MODEL", value: model });
await client.goose.GooseSessionProviderUpdate({ await client.goose.GooseSessionProviderUpdate({
sessionId, sessionId,
provider: provider.name, provider: provider.providerId,
model, model,
}); });
onComplete(); onComplete();
@@ -449,12 +456,12 @@ export default function ConfigureScreen({
const [pendingConfigValues, setPendingConfigValues] = useState<Record<string, string>>({}); const [pendingConfigValues, setPendingConfigValues] = useState<Record<string, string>>({});
const handleProviderSelected = useCallback( const handleProviderSelected = useCallback(
(provider: ProviderDetailEntry) => { (provider: ProviderInventoryEntryDto) => {
const keys = provider.configKeys.filter( const keys = provider.configKeys.filter(
(k) => k.required && !k.oauthFlow && !k.deviceCodeFlow, (k) => k.required && !k.oauthFlow && !k.deviceCodeFlow,
); );
setSelectedProvider(provider); setSelectedProvider(provider);
if (keys.length > 0 && !provider.isConfigured) { if (keys.length > 0 && !provider.configured) {
setPhase("configure"); setPhase("configure");
} else { } else {
setPendingConfigValues({}); setPendingConfigValues({});
+26 -22
View File
@@ -1,7 +1,7 @@
import React, { useState, useEffect, useCallback } from "react"; import React, { useState, useEffect, useCallback } from "react";
import { Box, Text, useInput, useStdout } from "ink"; import { Box, Text, useInput, useStdout } from "ink";
import { TextInput, PasswordInput } from '@inkjs/ui'; import { TextInput, PasswordInput } from '@inkjs/ui';
import type { GooseClient, ProviderDetailEntry } from "@aaif/goose-sdk"; import type { GooseClient, ProviderInventoryEntryDto } from "@aaif/goose-sdk";
import { import {
CRANBERRY, CRANBERRY,
TEAL, TEAL,
@@ -30,9 +30,9 @@ interface OnboardingProps {
} }
export interface ProviderSelectorProps { export interface ProviderSelectorProps {
providers: ProviderDetailEntry[]; providers: ProviderInventoryEntryDto[];
height: number; height: number;
onSelect: (provider: ProviderDetailEntry) => void; onSelect: (provider: ProviderInventoryEntryDto) => void;
title?: string; title?: string;
subtitle?: string; subtitle?: string;
onBack?: () => void; onBack?: () => void;
@@ -49,8 +49,8 @@ export const ProviderSelector = React.memo(function ProviderSelector({ providers
const q = searchQuery.toLowerCase(); const q = searchQuery.toLowerCase();
return providers.filter( return providers.filter(
(p) => (p) =>
p.displayName.toLowerCase().includes(q) || p.providerName.toLowerCase().includes(q) ||
p.name.toLowerCase().includes(q), p.providerId.toLowerCase().includes(q),
); );
})(); })();
@@ -151,7 +151,11 @@ export const ProviderSelector = React.memo(function ProviderSelector({ providers
}); });
// Create grid of provider cards // Create grid of provider cards
const renderProviderCard = (provider: ProviderDetailEntry, _index: number, isSelected: boolean) => { const renderProviderCard = (
provider: ProviderInventoryEntryDto,
_index: number,
isSelected: boolean,
) => {
const cardBorder = isSelected ? "double" : "single"; const cardBorder = isSelected ? "double" : "single";
const cardBorderColor = isSelected ? GOLD : RULE_COLOR; const cardBorderColor = isSelected ? GOLD : RULE_COLOR;
const textColor = isSelected ? TEXT_PRIMARY : TEXT_SECONDARY; const textColor = isSelected ? TEXT_PRIMARY : TEXT_SECONDARY;
@@ -166,7 +170,7 @@ export const ProviderSelector = React.memo(function ProviderSelector({ providers
return ( return (
<Box <Box
key={provider.name} key={provider.providerId}
width={cardWidth} width={cardWidth}
height={cardHeight} height={cardHeight}
borderStyle={cardBorder} borderStyle={cardBorder}
@@ -178,14 +182,14 @@ export const ProviderSelector = React.memo(function ProviderSelector({ providers
<Box justifyContent="space-between" alignItems="center"> <Box justifyContent="space-between" alignItems="center">
<Box width={titleWidth} flexShrink={1}> <Box width={titleWidth} flexShrink={1}>
<Text color={textColor} bold={isSelected} wrap="truncate"> <Text color={textColor} bold={isSelected} wrap="truncate">
{provider.displayName} {provider.providerName}
</Text> </Text>
</Box> </Box>
<Box flexShrink={0}> <Box flexShrink={0}>
{provider.providerType === "Preferred" && ( {provider.providerType === "Preferred" && (
<Text color={TEAL}></Text> <Text color={TEAL}></Text>
)} )}
{provider.isConfigured && ( {provider.configured && (
<Text color={TEAL}></Text> <Text color={TEAL}></Text>
)} )}
</Box> </Box>
@@ -194,7 +198,7 @@ export const ProviderSelector = React.memo(function ProviderSelector({ providers
<Box marginTop={1} flexDirection="column" flexGrow={1}> <Box marginTop={1} flexDirection="column" flexGrow={1}>
<Box width={contentWidth}> <Box width={contentWidth}>
<Text color={TEXT_DIM} wrap="truncate"> <Text color={TEXT_DIM} wrap="truncate">
{provider.name} {provider.providerId}
</Text> </Text>
</Box> </Box>
{provider.description && ( {provider.description && (
@@ -306,7 +310,7 @@ export const ProviderSelector = React.memo(function ProviderSelector({ providers
}); });
export interface ProviderConfiguratorProps { export interface ProviderConfiguratorProps {
provider: ProviderDetailEntry; provider: ProviderInventoryEntryDto;
height: number; height: number;
onComplete: (values: Record<string, string>) => void; onComplete: (values: Record<string, string>) => void;
onBack: () => void; onBack: () => void;
@@ -383,7 +387,7 @@ export const ProviderConfigurator = React.memo(function ProviderConfigurator({ p
{/* Header */} {/* Header */}
<Box justifyContent="center" marginBottom={1}> <Box justifyContent="center" marginBottom={1}>
<Text color={TEXT_PRIMARY} bold> <Text color={TEXT_PRIMARY} bold>
Configure {provider.displayName} Configure {provider.providerName}
</Text> </Text>
</Box> </Box>
{provider.description && ( {provider.description && (
@@ -473,7 +477,7 @@ export const ProviderConfigurator = React.memo(function ProviderConfigurator({ p
}); });
interface SuccessScreenProps { interface SuccessScreenProps {
provider: ProviderDetailEntry | null; provider: ProviderInventoryEntryDto | null;
height: number; height: number;
} }
@@ -501,7 +505,7 @@ const SuccessScreen = React.memo(function SuccessScreen({ provider, height }: Su
{provider && ( {provider && (
<Box marginTop={1}> <Box marginTop={1}>
<Text color={TEXT_SECONDARY}> <Text color={TEXT_SECONDARY}>
Connected to {provider.displayName} Connected to {provider.providerName}
</Text> </Text>
</Box> </Box>
)} )}
@@ -517,9 +521,9 @@ export default function Onboarding({
onComplete, onComplete,
}: OnboardingProps) { }: OnboardingProps) {
const [phase, setPhase] = useState<Phase>("loading"); const [phase, setPhase] = useState<Phase>("loading");
const [providers, setProviders] = useState<ProviderDetailEntry[]>([]); const [providers, setProviders] = useState<ProviderInventoryEntryDto[]>([]);
const [selectedProvider, setSelectedProvider] = const [selectedProvider, setSelectedProvider] =
useState<ProviderDetailEntry | null>(null); useState<ProviderInventoryEntryDto | null>(null);
const [errorMsg, setErrorMsg] = useState(""); const [errorMsg, setErrorMsg] = useState("");
const [spinIdx, setSpinIdx] = useState(0); const [spinIdx, setSpinIdx] = useState(0);
const [fetchKey, setFetchKey] = useState(0); const [fetchKey, setFetchKey] = useState(0);
@@ -535,12 +539,12 @@ export default function Onboarding({
useEffect(() => { useEffect(() => {
(async () => { (async () => {
try { try {
const resp = await client.goose.GooseProvidersDetails({}); const resp = await client.goose.GooseProvidersList({ providerIds: [] });
const sorted = [...resp.providers].sort((a, b) => { const sorted = [...resp.entries].sort((a, b) => {
const aP = a.providerType === "Preferred" ? 0 : 1; const aP = a.providerType === "Preferred" ? 0 : 1;
const bP = b.providerType === "Preferred" ? 0 : 1; const bP = b.providerType === "Preferred" ? 0 : 1;
if (aP !== bP) return aP - bP; if (aP !== bP) return aP - bP;
return a.displayName.localeCompare(b.displayName); return a.providerName.localeCompare(b.providerName);
}); });
setProviders(sorted); setProviders(sorted);
setPhase("select_provider"); setPhase("select_provider");
@@ -552,7 +556,7 @@ export default function Onboarding({
}, [client, fetchKey]); }, [client, fetchKey]);
const saveProvider = useCallback( const saveProvider = useCallback(
async (provider: ProviderDetailEntry, values: Record<string, string>) => { async (provider: ProviderInventoryEntryDto, values: Record<string, string>) => {
setPhase("saving"); setPhase("saving");
try { try {
for (const [key, value] of Object.entries(values)) { for (const [key, value] of Object.entries(values)) {
@@ -565,7 +569,7 @@ export default function Onboarding({
} }
await client.goose.GooseConfigUpsert({ await client.goose.GooseConfigUpsert({
key: "GOOSE_PROVIDER", key: "GOOSE_PROVIDER",
value: provider.name, value: provider.providerId,
}); });
await client.goose.GooseConfigUpsert({ await client.goose.GooseConfigUpsert({
key: "GOOSE_MODEL", key: "GOOSE_MODEL",
@@ -582,7 +586,7 @@ export default function Onboarding({
); );
const confirmProvider = useCallback( const confirmProvider = useCallback(
(provider: ProviderDetailEntry) => { (provider: ProviderInventoryEntryDto) => {
const keys = provider.configKeys.filter( const keys = provider.configKeys.filter(
(k) => k.required && !k.oauthFlow && !k.deviceCodeFlow, (k) => k.required && !k.oauthFlow && !k.deviceCodeFlow,
); );