chore: show important keys for provider configuration (#7265)

This commit is contained in:
Lifei Zhou
2026-02-18 18:10:53 +11:00
committed by GitHub
parent cc51ec0941
commit 5dacfde2f3
29 changed files with 283 additions and 158 deletions
+147 -99
View File
@@ -21,6 +21,7 @@ use goose::config::{
}; };
use goose::model::ModelConfig; use goose::model::ModelConfig;
use goose::posthog::{get_telemetry_choice, TELEMETRY_ENABLED_KEY}; use goose::posthog::{get_telemetry_choice, TELEMETRY_ENABLED_KEY};
use goose::providers::base::ConfigKey;
use goose::providers::provider_test::test_provider_configuration; use goose::providers::provider_test::test_provider_configuration;
use goose::providers::{create, providers, retry_operation, RetryConfig}; use goose::providers::{create, providers, retry_operation, RetryConfig};
use goose::session::SessionType; use goose::session::SessionType;
@@ -541,6 +542,129 @@ fn try_store_secret(config: &Config, key_name: &str, value: String) -> anyhow::R
} }
} }
async fn configure_single_key(
config: &Config,
provider_name: &str,
display_name: &str,
key: &ConfigKey,
) -> anyhow::Result<bool> {
let from_env = std::env::var(&key.name).ok();
match from_env {
Some(env_value) => {
let _ = cliclack::log::info(format!("{} is set via environment variable", key.name));
if cliclack::confirm("Would you like to save this value to your keyring?")
.initial_value(true)
.interact()?
{
if key.secret {
if !try_store_secret(config, &key.name, env_value)? {
return Ok(false);
}
} else {
config.set_param(&key.name, &env_value)?;
}
let _ = cliclack::log::info(format!("Saved {} to {}", key.name, config.path()));
}
}
None => {
let existing: Result<String, _> = if key.secret {
config.get_secret(&key.name)
} else {
config.get_param(&key.name)
};
match existing {
Ok(_) => {
let _ = cliclack::log::info(format!("{} is already configured", key.name));
if cliclack::confirm("Would you like to update this value?").interact()? {
if key.oauth_flow {
handle_oauth_configuration(provider_name, &key.name).await?;
} else {
let value: String = if key.secret {
cliclack::password(format!("Enter new value for {}", key.name))
.mask('▪')
.interact()?
} else {
let mut input =
cliclack::input(format!("Enter new value for {}", key.name));
if key.default.is_some() {
input = input.default_input(&key.default.clone().unwrap());
}
input.interact()?
};
if key.secret {
if !try_store_secret(config, &key.name, value)? {
return Ok(false);
}
} else {
config.set_param(&key.name, &value)?;
}
}
}
}
Err(_) => {
if key.oauth_flow {
handle_oauth_configuration(provider_name, &key.name).await?;
} else if !key.required && key.secret {
if cliclack::confirm(format!(
"Would you like to set {}? (optional)",
key.name
))
.initial_value(true)
.interact()?
{
let value: String =
cliclack::password(format!("Enter value for {}", key.name))
.mask('▪')
.interact()?;
if !try_store_secret(config, &key.name, value)? {
return Ok(false);
}
}
} else {
let prompt = if key.required {
format!(
"Provider {} requires {}, please enter a value",
display_name, key.name
)
} else {
format!("Enter {} (optional, press Enter to skip)", key.name)
};
let value: String = if key.secret {
cliclack::password(&prompt).mask('▪').interact()?
} else {
let mut input = cliclack::input(&prompt);
if key.default.is_some() {
input = input.default_input(&key.default.clone().unwrap());
}
if !key.required {
input = input.required(false);
}
input.interact()?
};
if value.is_empty() {
return Ok(true);
}
if key.secret {
if !try_store_secret(config, &key.name, value)? {
return Ok(false);
}
} else {
config.set_param(&key.name, &value)?;
}
}
}
}
}
}
Ok(true)
}
pub async fn configure_provider_dialog() -> anyhow::Result<bool> { pub async fn configure_provider_dialog() -> anyhow::Result<bool> {
// Get global config instance // Get global config instance
let config = Config::global(); let config = Config::global();
@@ -574,107 +698,31 @@ pub async fn configure_provider_dialog() -> anyhow::Result<bool> {
.find(|(p, _)| &p.name == provider_name) .find(|(p, _)| &p.name == provider_name)
.expect("Selected provider must exist in metadata"); .expect("Selected provider must exist in metadata");
// Configure required provider keys for key in provider_meta
for key in &provider_meta.config_keys { .config_keys
if !key.required { .iter()
continue; .filter(|k| k.primary || k.oauth_flow)
{
if !configure_single_key(config, provider_name, &provider_meta.display_name, key).await? {
return Ok(false);
} }
}
// First check if the value is set via environment variable let non_primary_keys: Vec<_> = provider_meta
let from_env = std::env::var(&key.name).ok(); .config_keys
.iter()
match from_env { .filter(|k| !k.primary && !k.oauth_flow)
Some(env_value) => { .collect();
let _ = if !non_primary_keys.is_empty()
cliclack::log::info(format!("{} is set via environment variable", key.name)); && cliclack::confirm("Would you like to configure advanced settings?")
if cliclack::confirm("Would you like to save this value to your keyring?") .initial_value(false)
.initial_value(true) .interact()?
.interact()? {
{ for key in non_primary_keys {
if key.secret { if !configure_single_key(config, provider_name, &provider_meta.display_name, key)
if !try_store_secret(config, &key.name, env_value)? { .await?
return Ok(false); {
} return Ok(false);
} else {
config.set_param(&key.name, &env_value)?;
}
let _ = cliclack::log::info(format!("Saved {} to {}", key.name, config.path()));
}
}
None => {
let existing: Result<String, _> = if key.secret {
config.get_secret(&key.name)
} else {
config.get_param(&key.name)
};
match existing {
Ok(_) => {
let _ = cliclack::log::info(format!("{} is already configured", key.name));
if cliclack::confirm("Would you like to update this value?").interact()? {
// Check if this key uses OAuth flow
if key.oauth_flow {
handle_oauth_configuration(provider_name, &key.name).await?;
} else {
// Non-OAuth key, use manual entry
let value: String = if key.secret {
cliclack::password(format!("Enter new value for {}", key.name))
.mask('▪')
.interact()?
} else {
let mut input = cliclack::input(format!(
"Enter new value for {}",
key.name
));
if key.default.is_some() {
input = input.default_input(&key.default.clone().unwrap());
}
input.interact()?
};
if key.secret {
if !try_store_secret(config, &key.name, value)? {
return Ok(false);
}
} else {
config.set_param(&key.name, &value)?;
}
}
}
}
Err(_) => {
if key.oauth_flow {
handle_oauth_configuration(provider_name, &key.name).await?;
} else {
// Non-OAuth key, use manual entry
let value: String = if key.secret {
cliclack::password(format!(
"Provider {} requires {}, please enter a value",
provider_meta.display_name, key.name
))
.mask('▪')
.interact()?
} else {
let mut input = cliclack::input(format!(
"Provider {} requires {}, please enter a value",
provider_meta.display_name, key.name
));
if key.default.is_some() {
input = input.default_input(&key.default.clone().unwrap());
}
input.interact()?
};
if key.secret {
if !try_store_secret(config, &key.name, value)? {
return Ok(false);
}
} else {
config.set_param(&key.name, &value)?;
}
}
}
}
} }
} }
} }
+2 -1
View File
@@ -153,12 +153,13 @@ impl ProviderDef for AnthropicProvider {
models, models,
ANTHROPIC_DOC_URL, ANTHROPIC_DOC_URL,
vec![ vec![
ConfigKey::new("ANTHROPIC_API_KEY", true, true, None), ConfigKey::new("ANTHROPIC_API_KEY", true, true, None, true),
ConfigKey::new( ConfigKey::new(
"ANTHROPIC_HOST", "ANTHROPIC_HOST",
true, true,
false, false,
Some("https://api.anthropic.com"), Some("https://api.anthropic.com"),
false,
), ),
], ],
) )
+10 -4
View File
@@ -55,10 +55,16 @@ impl ProviderDef for AzureProvider {
AZURE_OPENAI_KNOWN_MODELS.to_vec(), AZURE_OPENAI_KNOWN_MODELS.to_vec(),
AZURE_DOC_URL, AZURE_DOC_URL,
vec![ vec![
ConfigKey::new("AZURE_OPENAI_ENDPOINT", true, false, None), ConfigKey::new("AZURE_OPENAI_ENDPOINT", true, false, None, true),
ConfigKey::new("AZURE_OPENAI_DEPLOYMENT_NAME", true, false, None), ConfigKey::new("AZURE_OPENAI_DEPLOYMENT_NAME", true, false, None, true),
ConfigKey::new("AZURE_OPENAI_API_VERSION", true, false, Some("2024-10-21")), ConfigKey::new(
ConfigKey::new("AZURE_OPENAI_API_KEY", false, true, Some("")), "AZURE_OPENAI_API_VERSION",
true,
false,
Some("2024-10-21"),
false,
),
ConfigKey::new("AZURE_OPENAI_API_KEY", false, true, Some(""), true),
], ],
) )
} }
+22 -3
View File
@@ -205,27 +205,39 @@ pub struct ConfigKey {
/// Whether this key should be configured using OAuth device code flow /// Whether this key should be configured using OAuth device code flow
/// When true, the provider's configure_oauth() method will be called instead of prompting for manual input /// When true, the provider's configure_oauth() method will be called instead of prompting for manual input
pub oauth_flow: bool, pub oauth_flow: bool,
/// Whether this key should be shown prominently during provider setup
/// (onboarding, settings modal, CLI configure)
#[serde(default)]
pub primary: bool,
} }
impl ConfigKey { impl ConfigKey {
/// Create a new ConfigKey /// Create a new ConfigKey
pub fn new(name: &str, required: bool, secret: bool, default: Option<&str>) -> Self { pub fn new(
name: &str,
required: bool,
secret: bool,
default: Option<&str>,
primary: bool,
) -> Self {
Self { Self {
name: name.to_string(), name: name.to_string(),
required, required,
secret, secret,
default: default.map(|s| s.to_string()), default: default.map(|s| s.to_string()),
oauth_flow: false, oauth_flow: false,
primary,
} }
} }
pub fn from_value_type<T: ConfigValue>(required: bool, secret: bool) -> Self { pub fn from_value_type<T: ConfigValue>(required: bool, secret: bool, primary: bool) -> Self {
Self { Self {
name: T::KEY.to_string(), name: T::KEY.to_string(),
required, required,
secret, secret,
default: Some(T::DEFAULT.to_string()), default: Some(T::DEFAULT.to_string()),
oauth_flow: false, oauth_flow: false,
primary,
} }
} }
@@ -233,13 +245,20 @@ impl ConfigKey {
/// ///
/// This is used for providers that support OAuth authentication instead of manual API key entry. /// This is used for providers that support OAuth authentication instead of manual API key entry.
/// When oauth_flow is true, the configuration system will call the provider's configure_oauth() method. /// When oauth_flow is true, the configuration system will call the provider's configure_oauth() method.
pub fn new_oauth(name: &str, required: bool, secret: bool, default: Option<&str>) -> Self { pub fn new_oauth(
name: &str,
required: bool,
secret: bool,
default: Option<&str>,
primary: bool,
) -> Self {
Self { Self {
name: name.to_string(), name: name.to_string(),
required, required,
secret, secret,
default: default.map(|s| s.to_string()), default: default.map(|s| s.to_string()),
oauth_flow: true, oauth_flow: true,
primary,
} }
} }
} }
+3 -3
View File
@@ -277,9 +277,9 @@ impl ProviderDef for BedrockProvider {
BEDROCK_KNOWN_MODELS.to_vec(), BEDROCK_KNOWN_MODELS.to_vec(),
BEDROCK_DOC_LINK, BEDROCK_DOC_LINK,
vec![ vec![
ConfigKey::new("AWS_PROFILE", false, false, Some("default")), ConfigKey::new("AWS_PROFILE", false, false, Some("default"), true),
ConfigKey::new("AWS_REGION", false, false, None), ConfigKey::new("AWS_REGION", false, false, None, true),
ConfigKey::new("AWS_BEARER_TOKEN_BEDROCK", false, true, None), ConfigKey::new("AWS_BEARER_TOKEN_BEDROCK", false, true, None, true),
], ],
) )
} }
@@ -861,6 +861,7 @@ impl ProviderDef for ChatGptCodexProvider {
true, true,
true, true,
None, None,
false,
)], )],
) )
.with_unlisted_models() .with_unlisted_models()
+3 -1
View File
@@ -483,7 +483,9 @@ impl ProviderDef for ClaudeCodeProvider {
// Only a few agentic choices; fetched dynamically via fetch_supported_models. // Only a few agentic choices; fetched dynamically via fetch_supported_models.
vec![], vec![],
CLAUDE_CODE_DOC_URL, CLAUDE_CODE_DOC_URL,
vec![ConfigKey::from_value_type::<ClaudeCodeCommand>(true, false)], vec![ConfigKey::from_value_type::<ClaudeCodeCommand>(
true, false, true,
)],
) )
// The model list only returns aliases the `claude` CLI uses, such as "default" // The model list only returns aliases the `claude` CLI uses, such as "default"
// and "haiku". There is no listing that includes full names like // and "haiku". There is no listing that includes full names like
+3 -3
View File
@@ -600,9 +600,9 @@ impl ProviderDef for CodexProvider {
CODEX_KNOWN_MODELS.to_vec(), CODEX_KNOWN_MODELS.to_vec(),
CODEX_DOC_URL, CODEX_DOC_URL,
vec![ vec![
ConfigKey::from_value_type::<CodexCommand>(true, false), ConfigKey::from_value_type::<CodexCommand>(true, false, true),
ConfigKey::from_value_type::<CodexReasoningEffort>(false, false), ConfigKey::from_value_type::<CodexReasoningEffort>(false, false, true),
ConfigKey::from_value_type::<CodexSkipGitCheck>(false, false), ConfigKey::from_value_type::<CodexSkipGitCheck>(false, false, true),
], ],
) )
.with_unlisted_models() .with_unlisted_models()
+1 -1
View File
@@ -291,7 +291,7 @@ impl ProviderDef for CursorAgentProvider {
CURSOR_AGENT_KNOWN_MODELS.to_vec(), CURSOR_AGENT_KNOWN_MODELS.to_vec(),
CURSOR_AGENT_DOC_URL, CURSOR_AGENT_DOC_URL,
vec![ConfigKey::from_value_type::<CursorAgentCommand>( vec![ConfigKey::from_value_type::<CursorAgentCommand>(
true, false, true, false, true,
)], )],
) )
.with_unlisted_models() .with_unlisted_models()
+2 -2
View File
@@ -246,8 +246,8 @@ impl ProviderDef for DatabricksProvider {
DATABRICKS_KNOWN_MODELS.to_vec(), DATABRICKS_KNOWN_MODELS.to_vec(),
DATABRICKS_DOC_URL, DATABRICKS_DOC_URL,
vec![ vec![
ConfigKey::new("DATABRICKS_HOST", true, false, None), ConfigKey::new("DATABRICKS_HOST", true, false, None, true),
ConfigKey::new("DATABRICKS_TOKEN", false, true, None), ConfigKey::new("DATABRICKS_TOKEN", false, true, None, true),
], ],
) )
} }
+6 -1
View File
@@ -503,36 +503,41 @@ impl ProviderDef for GcpVertexAIProvider {
KNOWN_MODELS.to_vec(), KNOWN_MODELS.to_vec(),
GCP_VERTEX_AI_DOC_URL, GCP_VERTEX_AI_DOC_URL,
vec![ vec![
ConfigKey::new("GCP_PROJECT_ID", true, false, None), ConfigKey::new("GCP_PROJECT_ID", true, false, None, true),
ConfigKey::new( ConfigKey::new(
"GCP_LOCATION", "GCP_LOCATION",
true, true,
false, false,
Some(&GcpLocation::Iowa.to_string()), Some(&GcpLocation::Iowa.to_string()),
true,
), ),
ConfigKey::new( ConfigKey::new(
"GCP_MAX_RETRIES", "GCP_MAX_RETRIES",
false, false,
false, false,
Some(&DEFAULT_MAX_RETRIES.to_string()), Some(&DEFAULT_MAX_RETRIES.to_string()),
false,
), ),
ConfigKey::new( ConfigKey::new(
"GCP_INITIAL_RETRY_INTERVAL_MS", "GCP_INITIAL_RETRY_INTERVAL_MS",
false, false,
false, false,
Some(&DEFAULT_INITIAL_RETRY_INTERVAL_MS.to_string()), Some(&DEFAULT_INITIAL_RETRY_INTERVAL_MS.to_string()),
false,
), ),
ConfigKey::new( ConfigKey::new(
"GCP_BACKOFF_MULTIPLIER", "GCP_BACKOFF_MULTIPLIER",
false, false,
false, false,
Some(&DEFAULT_BACKOFF_MULTIPLIER.to_string()), Some(&DEFAULT_BACKOFF_MULTIPLIER.to_string()),
false,
), ),
ConfigKey::new( ConfigKey::new(
"GCP_MAX_RETRY_INTERVAL_MS", "GCP_MAX_RETRY_INTERVAL_MS",
false, false,
false, false,
Some(&DEFAULT_MAX_RETRY_INTERVAL_MS.to_string()), Some(&DEFAULT_MAX_RETRY_INTERVAL_MS.to_string()),
false,
), ),
], ],
) )
+3 -1
View File
@@ -306,7 +306,9 @@ impl ProviderDef for GeminiCliProvider {
GEMINI_CLI_DEFAULT_MODEL, GEMINI_CLI_DEFAULT_MODEL,
GEMINI_CLI_KNOWN_MODELS.to_vec(), GEMINI_CLI_KNOWN_MODELS.to_vec(),
GEMINI_CLI_DOC_URL, GEMINI_CLI_DOC_URL,
vec![ConfigKey::from_value_type::<GeminiCliCommand>(true, false)], vec![ConfigKey::from_value_type::<GeminiCliCommand>(
true, false, true,
)],
) )
.with_unlisted_models() .with_unlisted_models()
} }
@@ -395,6 +395,7 @@ impl ProviderDef for GithubCopilotProvider {
true, true,
true, true,
None, None,
false,
)], )],
) )
} }
+2 -2
View File
@@ -117,8 +117,8 @@ impl ProviderDef for GoogleProvider {
GOOGLE_KNOWN_MODELS.to_vec(), GOOGLE_KNOWN_MODELS.to_vec(),
GOOGLE_DOC_URL, GOOGLE_DOC_URL,
vec![ vec![
ConfigKey::new("GOOGLE_API_KEY", true, true, None), ConfigKey::new("GOOGLE_API_KEY", true, true, None, true),
ConfigKey::new("GOOGLE_HOST", false, false, Some(GOOGLE_API_HOST)), ConfigKey::new("GOOGLE_HOST", false, false, Some(GOOGLE_API_HOST), false),
], ],
) )
} }
+11 -4
View File
@@ -145,16 +145,23 @@ impl ProviderDef for LiteLLMProvider {
vec![], vec![],
LITELLM_DOC_URL, LITELLM_DOC_URL,
vec![ vec![
ConfigKey::new("LITELLM_API_KEY", true, true, None), ConfigKey::new("LITELLM_API_KEY", true, true, None, true),
ConfigKey::new("LITELLM_HOST", true, false, Some("http://localhost:4000")), ConfigKey::new(
"LITELLM_HOST",
true,
false,
Some("http://localhost:4000"),
true,
),
ConfigKey::new( ConfigKey::new(
"LITELLM_BASE_PATH", "LITELLM_BASE_PATH",
true, true,
false, false,
Some("v1/chat/completions"), Some("v1/chat/completions"),
false,
), ),
ConfigKey::new("LITELLM_CUSTOM_HEADERS", false, true, None), ConfigKey::new("LITELLM_CUSTOM_HEADERS", false, true, None, false),
ConfigKey::new("LITELLM_TIMEOUT", false, false, Some("600")), ConfigKey::new("LITELLM_TIMEOUT", false, false, Some("600"), false),
], ],
) )
} }
+2 -1
View File
@@ -158,12 +158,13 @@ impl ProviderDef for OllamaProvider {
OLLAMA_KNOWN_MODELS.to_vec(), OLLAMA_KNOWN_MODELS.to_vec(),
OLLAMA_DOC_URL, OLLAMA_DOC_URL,
vec![ vec![
ConfigKey::new("OLLAMA_HOST", true, false, Some(OLLAMA_HOST)), ConfigKey::new("OLLAMA_HOST", true, false, Some(OLLAMA_HOST), true),
ConfigKey::new( ConfigKey::new(
"OLLAMA_TIMEOUT", "OLLAMA_TIMEOUT",
false, false,
false, false,
Some(&(OLLAMA_TIMEOUT.to_string())), Some(&(OLLAMA_TIMEOUT.to_string())),
false,
), ),
], ],
) )
+19 -7
View File
@@ -285,13 +285,25 @@ impl ProviderDef for OpenAiProvider {
models, models,
OPEN_AI_DOC_URL, OPEN_AI_DOC_URL,
vec![ vec![
ConfigKey::new("OPENAI_API_KEY", false, true, None), ConfigKey::new("OPENAI_API_KEY", false, true, None, true),
ConfigKey::new("OPENAI_HOST", true, false, Some("https://api.openai.com")), ConfigKey::new(
ConfigKey::new("OPENAI_BASE_PATH", true, false, Some("v1/chat/completions")), "OPENAI_HOST",
ConfigKey::new("OPENAI_ORGANIZATION", false, false, None), true,
ConfigKey::new("OPENAI_PROJECT", false, false, None), false,
ConfigKey::new("OPENAI_CUSTOM_HEADERS", false, true, None), Some("https://api.openai.com"),
ConfigKey::new("OPENAI_TIMEOUT", false, false, Some("600")), false,
),
ConfigKey::new(
"OPENAI_BASE_PATH",
true,
false,
Some("v1/chat/completions"),
false,
),
ConfigKey::new("OPENAI_ORGANIZATION", false, false, None, false),
ConfigKey::new("OPENAI_PROJECT", false, false, None, false),
ConfigKey::new("OPENAI_CUSTOM_HEADERS", false, true, None, false),
ConfigKey::new("OPENAI_TIMEOUT", false, false, Some("600"), false),
], ],
) )
} }
+2 -1
View File
@@ -158,12 +158,13 @@ impl ProviderDef for OpenRouterProvider {
OPENROUTER_KNOWN_MODELS.to_vec(), OPENROUTER_KNOWN_MODELS.to_vec(),
OPENROUTER_DOC_URL, OPENROUTER_DOC_URL,
vec![ vec![
ConfigKey::new("OPENROUTER_API_KEY", true, true, None), ConfigKey::new("OPENROUTER_API_KEY", true, true, None, true),
ConfigKey::new( ConfigKey::new(
"OPENROUTER_HOST", "OPENROUTER_HOST",
false, false,
false, false,
Some("https://openrouter.ai"), Some("https://openrouter.ai"),
false,
), ),
], ],
) )
@@ -109,8 +109,13 @@ impl ProviderRegistry {
config_keys.remove(api_key_index); config_keys.remove(api_key_index);
} else if !config.api_key_env.is_empty() { } else if !config.api_key_env.is_empty() {
let api_key_required = provider_type == ProviderType::Declarative; let api_key_required = provider_type == ProviderType::Declarative;
config_keys[api_key_index] = config_keys[api_key_index] = super::base::ConfigKey::new(
super::base::ConfigKey::new(&config.api_key_env, api_key_required, true, None); &config.api_key_env,
api_key_required,
true,
None,
true,
);
} }
} }
+3 -3
View File
@@ -284,9 +284,9 @@ impl ProviderDef for SageMakerTgiProvider {
vec![SAGEMAKER_TGI_DEFAULT_MODEL], vec![SAGEMAKER_TGI_DEFAULT_MODEL],
SAGEMAKER_TGI_DOC_LINK, SAGEMAKER_TGI_DOC_LINK,
vec![ vec![
ConfigKey::new("SAGEMAKER_ENDPOINT_NAME", false, false, None), ConfigKey::new("SAGEMAKER_ENDPOINT_NAME", true, false, None, true),
ConfigKey::new("AWS_REGION", true, false, Some("us-east-1")), ConfigKey::new("AWS_REGION", true, false, Some("us-east-1"), true),
ConfigKey::new("AWS_PROFILE", true, false, Some("default")), ConfigKey::new("AWS_PROFILE", true, false, Some("default"), true),
], ],
) )
} }
+2 -2
View File
@@ -309,8 +309,8 @@ impl ProviderDef for SnowflakeProvider {
SNOWFLAKE_KNOWN_MODELS.to_vec(), SNOWFLAKE_KNOWN_MODELS.to_vec(),
SNOWFLAKE_DOC_URL, SNOWFLAKE_DOC_URL,
vec![ vec![
ConfigKey::new("SNOWFLAKE_HOST", true, false, None), ConfigKey::new("SNOWFLAKE_HOST", true, false, None, true),
ConfigKey::new("SNOWFLAKE_TOKEN", true, true, None), ConfigKey::new("SNOWFLAKE_TOKEN", true, true, None, true),
], ],
) )
} }
+2 -1
View File
@@ -75,12 +75,13 @@ impl ProviderDef for TetrateProvider {
TETRATE_KNOWN_MODELS.to_vec(), TETRATE_KNOWN_MODELS.to_vec(),
TETRATE_DOC_URL, TETRATE_DOC_URL,
vec![ vec![
ConfigKey::new("TETRATE_API_KEY", true, true, None), ConfigKey::new("TETRATE_API_KEY", true, true, None, true),
ConfigKey::new( ConfigKey::new(
"TETRATE_HOST", "TETRATE_HOST",
false, false,
false, false,
Some("https://api.router.tetrate.ai"), Some("https://api.router.tetrate.ai"),
false,
), ),
], ],
) )
+4 -2
View File
@@ -205,19 +205,21 @@ impl ProviderDef for VeniceProvider {
FALLBACK_MODELS.to_vec(), FALLBACK_MODELS.to_vec(),
VENICE_DOC_URL, VENICE_DOC_URL,
vec![ vec![
ConfigKey::new("VENICE_API_KEY", true, true, None), ConfigKey::new("VENICE_API_KEY", true, true, None, true),
ConfigKey::new("VENICE_HOST", true, false, Some(VENICE_DEFAULT_HOST)), ConfigKey::new("VENICE_HOST", true, false, Some(VENICE_DEFAULT_HOST), false),
ConfigKey::new( ConfigKey::new(
"VENICE_BASE_PATH", "VENICE_BASE_PATH",
true, true,
false, false,
Some(VENICE_DEFAULT_BASE_PATH), Some(VENICE_DEFAULT_BASE_PATH),
false,
), ),
ConfigKey::new( ConfigKey::new(
"VENICE_MODELS_PATH", "VENICE_MODELS_PATH",
true, true,
false, false,
Some(VENICE_DEFAULT_MODELS_PATH), Some(VENICE_DEFAULT_MODELS_PATH),
false,
), ),
], ],
) )
+2 -2
View File
@@ -45,8 +45,8 @@ impl ProviderDef for XaiProvider {
XAI_KNOWN_MODELS.to_vec(), XAI_KNOWN_MODELS.to_vec(),
XAI_DOC_URL, XAI_DOC_URL,
vec![ vec![
ConfigKey::new("XAI_API_KEY", true, true, None), ConfigKey::new("XAI_API_KEY", true, true, None, true),
ConfigKey::new("XAI_HOST", false, false, Some(XAI_API_HOST)), ConfigKey::new("XAI_HOST", false, false, Some(XAI_API_HOST), false),
], ],
) )
} }
+4
View File
@@ -3588,6 +3588,10 @@
"type": "boolean", "type": "boolean",
"description": "Whether this key should be configured using OAuth device code flow\nWhen true, the provider's configure_oauth() method will be called instead of prompting for manual input" "description": "Whether this key should be configured using OAuth device code flow\nWhen true, the provider's configure_oauth() method will be called instead of prompting for manual input"
}, },
"primary": {
"type": "boolean",
"description": "Whether this key should be shown prominently during provider setup\n(onboarding, settings modal, CLI configure)"
},
"required": { "required": {
"type": "boolean", "type": "boolean",
"description": "Whether this key is required for the provider to function" "description": "Whether this key is required for the provider to function"
+5
View File
@@ -90,6 +90,11 @@ export type ConfigKey = {
* When true, the provider's configure_oauth() method will be called instead of prompting for manual input * When true, the provider's configure_oauth() method will be called instead of prompting for manual input
*/ */
oauth_flow: boolean; oauth_flow: boolean;
/**
* Whether this key should be shown prominently during provider setup
* (onboarding, settings modal, CLI configure)
*/
primary?: boolean;
/** /**
* Whether this key is required for the provider to function * Whether this key is required for the provider to function
*/ */
@@ -41,9 +41,10 @@ export default function ProviderConfigurationModal({
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [isOAuthLoading, setIsOAuthLoading] = useState(false); const [isOAuthLoading, setIsOAuthLoading] = useState(false);
const requiredParameters = provider.metadata.config_keys.filter( let primaryParameters = provider.metadata.config_keys.filter((param) => param.primary);
(param) => param.required === true if (primaryParameters.length === 0) {
); primaryParameters = provider.metadata.config_keys;
}
// Check if this provider uses OAuth for configuration // Check if this provider uses OAuth for configuration
const isOAuthProvider = provider.metadata.config_keys.some((key) => key.oauth_flow); const isOAuthProvider = provider.metadata.config_keys.some((key) => key.oauth_flow);
@@ -238,7 +239,7 @@ export default function ProviderConfigurationModal({
validationErrors={validationErrors} validationErrors={validationErrors}
/> />
{requiredParameters.length > 0 && {primaryParameters.length > 0 &&
provider.metadata.config_keys && provider.metadata.config_keys &&
provider.metadata.config_keys.length > 0 && <SecureStorageNotice />} provider.metadata.config_keys.length > 0 && <SecureStorageNotice />}
</> </>
@@ -260,7 +261,7 @@ export default function ProviderConfigurationModal({
</div> </div>
) : ( ) : (
<ProviderSetupActions <ProviderSetupActions
requiredParameters={requiredParameters} primaryParameters={primaryParameters}
onCancel={handleCancel} onCancel={handleCancel}
onSubmit={handleSubmitForm} onSubmit={handleSubmitForm}
onDelete={handleDelete} onDelete={handleDelete}
@@ -12,7 +12,7 @@ interface ProviderSetupActionsProps {
onCancelDelete?: () => void; onCancelDelete?: () => void;
canDelete?: boolean; canDelete?: boolean;
providerName?: string; providerName?: string;
requiredParameters?: ConfigKey[]; primaryParameters?: ConfigKey[];
isActiveProvider?: boolean; // Made optional with default false isActiveProvider?: boolean; // Made optional with default false
} }
@@ -29,7 +29,7 @@ export default function ProviderSetupActions({
onCancelDelete, onCancelDelete,
canDelete, canDelete,
providerName, providerName,
requiredParameters, primaryParameters,
isActiveProvider = false, // Default value provided isActiveProvider = false, // Default value provided
}: ProviderSetupActionsProps) { }: ProviderSetupActionsProps) {
// If we're showing delete confirmation, render the delete confirmation buttons // If we're showing delete confirmation, render the delete confirmation buttons
@@ -96,7 +96,7 @@ export default function ProviderSetupActions({
<Trash2 className="h-4 w-4 mr-2" /> Delete Provider <Trash2 className="h-4 w-4 mr-2" /> Delete Provider
</Button> </Button>
)} )}
{requiredParameters && requiredParameters.length > 0 ? ( {primaryParameters && primaryParameters.length > 0 ? (
<> <>
<Button <Button
type="submit" type="submit"
@@ -168,10 +168,11 @@ export default function DefaultProviderSetupForm({
)); ));
}; };
let aboveFoldParameters = parameters.filter((p) => p.required); let aboveFoldParameters = parameters.filter((p) => p.primary);
let belowFoldParameters = parameters.filter((p) => !p.required); let belowFoldParameters = parameters.filter((p) => !p.primary);
if (aboveFoldParameters.length === 0) {
aboveFoldParameters = belowFoldParameters; if (aboveFoldParameters.length === 0 && parameters.length > 0) {
aboveFoldParameters = parameters;
belowFoldParameters = []; belowFoldParameters = [];
} }