mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
feat: add requires_auth flag for custom providers without authentication (#6705)
Signed-off-by: rabi <ramishra@redhat.com>
This commit is contained in:
@@ -5,7 +5,9 @@ use goose::agents::extension::ToolInfo;
|
||||
use goose::agents::extension_manager::get_parameter_names;
|
||||
use goose::agents::Agent;
|
||||
use goose::agents::{extension::Envs, ExtensionConfig};
|
||||
use goose::config::declarative_providers::{create_custom_provider, remove_custom_provider};
|
||||
use goose::config::declarative_providers::{
|
||||
create_custom_provider, remove_custom_provider, CreateCustomProviderParams,
|
||||
};
|
||||
use goose::config::extensions::{
|
||||
get_all_extension_names, get_all_extensions, get_enabled_extensions, get_extension_by_name,
|
||||
name_to_key, remove_extension, set_extension, set_extension_enabled,
|
||||
@@ -1864,11 +1866,16 @@ fn add_provider() -> anyhow::Result<()> {
|
||||
})
|
||||
.interact()?;
|
||||
|
||||
let api_key: String = cliclack::password("API key:")
|
||||
.allow_empty()
|
||||
.mask('▪')
|
||||
let requires_auth = cliclack::confirm("Does this provider require authentication?")
|
||||
.initial_value(true)
|
||||
.interact()?;
|
||||
|
||||
let api_key: String = if requires_auth {
|
||||
cliclack::password("API key:").mask('▪').interact()?
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let models_input: String = cliclack::input("Available models (separate with commas):")
|
||||
.placeholder("model-a, model-b, model-c")
|
||||
.validate(|input: &String| {
|
||||
@@ -1897,15 +1904,16 @@ fn add_provider() -> anyhow::Result<()> {
|
||||
None
|
||||
};
|
||||
|
||||
create_custom_provider(
|
||||
provider_type,
|
||||
display_name.clone(),
|
||||
create_custom_provider(CreateCustomProviderParams {
|
||||
engine: provider_type.to_string(),
|
||||
display_name: display_name.clone(),
|
||||
api_url,
|
||||
api_key,
|
||||
models,
|
||||
Some(supports_streaming),
|
||||
supports_streaming: Some(supports_streaming),
|
||||
headers,
|
||||
)?;
|
||||
requires_auth,
|
||||
})?;
|
||||
|
||||
cliclack::outro(format!("Custom provider added: {}", display_name))?;
|
||||
Ok(())
|
||||
|
||||
@@ -93,6 +93,12 @@ pub struct UpdateCustomProviderRequest {
|
||||
pub models: Vec<String>,
|
||||
pub supports_streaming: Option<bool>,
|
||||
pub headers: Option<std::collections::HashMap<String, String>>,
|
||||
#[serde(default = "default_requires_auth")]
|
||||
pub requires_auth: bool,
|
||||
}
|
||||
|
||||
fn default_requires_auth() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
@@ -699,13 +705,16 @@ pub async fn create_custom_provider(
|
||||
Json(request): Json<UpdateCustomProviderRequest>,
|
||||
) -> Result<Json<String>, StatusCode> {
|
||||
let config = goose::config::declarative_providers::create_custom_provider(
|
||||
&request.engine,
|
||||
request.display_name,
|
||||
request.api_url,
|
||||
request.api_key,
|
||||
request.models,
|
||||
request.supports_streaming,
|
||||
request.headers,
|
||||
goose::config::declarative_providers::CreateCustomProviderParams {
|
||||
engine: request.engine,
|
||||
display_name: request.display_name,
|
||||
api_url: request.api_url,
|
||||
api_key: request.api_key,
|
||||
models: request.models,
|
||||
supports_streaming: request.supports_streaming,
|
||||
headers: request.headers,
|
||||
requires_auth: request.requires_auth,
|
||||
},
|
||||
)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
@@ -769,13 +778,17 @@ pub async fn update_custom_provider(
|
||||
Json(request): Json<UpdateCustomProviderRequest>,
|
||||
) -> Result<Json<String>, StatusCode> {
|
||||
goose::config::declarative_providers::update_custom_provider(
|
||||
&id,
|
||||
&request.engine,
|
||||
request.display_name,
|
||||
request.api_url,
|
||||
request.api_key,
|
||||
request.models,
|
||||
request.supports_streaming,
|
||||
goose::config::declarative_providers::UpdateCustomProviderParams {
|
||||
id: id.clone(),
|
||||
engine: request.engine,
|
||||
display_name: request.display_name,
|
||||
api_url: request.api_url,
|
||||
api_key: request.api_key,
|
||||
models: request.models,
|
||||
supports_streaming: request.supports_streaming,
|
||||
headers: request.headers,
|
||||
requires_auth: request.requires_auth,
|
||||
},
|
||||
)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
|
||||
@@ -96,9 +96,20 @@ pub fn check_provider_configured(metadata: &ProviderMetadata, provider_type: Pro
|
||||
|
||||
if provider_type == ProviderType::Custom || provider_type == ProviderType::Declarative {
|
||||
if let Ok(loaded_provider) = load_provider(metadata.name.as_str()) {
|
||||
return config
|
||||
.get_secret::<String>(&loaded_provider.config.api_key_env)
|
||||
.is_ok();
|
||||
if !loaded_provider.config.requires_auth {
|
||||
return true;
|
||||
}
|
||||
|
||||
if !loaded_provider.config.api_key_env.is_empty() {
|
||||
let api_key_result =
|
||||
config.get_secret::<String>(&loaded_provider.config.api_key_env);
|
||||
if api_key_result.is_ok() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Custom providers with config files are intentionally created
|
||||
return provider_type == ProviderType::Custom;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,12 +33,19 @@ pub struct DeclarativeProviderConfig {
|
||||
pub engine: ProviderEngine,
|
||||
pub display_name: String,
|
||||
pub description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub api_key_env: String,
|
||||
pub base_url: String,
|
||||
pub models: Vec<ModelInfo>,
|
||||
pub headers: Option<HashMap<String, String>>,
|
||||
pub timeout_seconds: Option<u64>,
|
||||
pub supports_streaming: Option<bool>,
|
||||
#[serde(default = "default_requires_auth")]
|
||||
pub requires_auth: bool,
|
||||
}
|
||||
|
||||
fn default_requires_auth() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl DeclarativeProviderConfig {
|
||||
@@ -85,42 +92,68 @@ pub fn generate_api_key_name(id: &str) -> String {
|
||||
format!("{}_API_KEY", id.to_uppercase())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreateCustomProviderParams {
|
||||
pub engine: String,
|
||||
pub display_name: String,
|
||||
pub api_url: String,
|
||||
pub api_key: String,
|
||||
pub models: Vec<String>,
|
||||
pub supports_streaming: Option<bool>,
|
||||
pub headers: Option<HashMap<String, String>>,
|
||||
pub requires_auth: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UpdateCustomProviderParams {
|
||||
pub id: String,
|
||||
pub engine: String,
|
||||
pub display_name: String,
|
||||
pub api_url: String,
|
||||
pub api_key: String,
|
||||
pub models: Vec<String>,
|
||||
pub supports_streaming: Option<bool>,
|
||||
pub headers: Option<HashMap<String, String>>,
|
||||
pub requires_auth: bool,
|
||||
}
|
||||
|
||||
pub fn create_custom_provider(
|
||||
engine: &str,
|
||||
display_name: String,
|
||||
api_url: String,
|
||||
api_key: String,
|
||||
models: Vec<String>,
|
||||
supports_streaming: Option<bool>,
|
||||
headers: Option<HashMap<String, String>>,
|
||||
params: CreateCustomProviderParams,
|
||||
) -> Result<DeclarativeProviderConfig> {
|
||||
let id = generate_id(&display_name);
|
||||
let api_key_name = generate_api_key_name(&id);
|
||||
let id = generate_id(¶ms.display_name);
|
||||
|
||||
let config = Config::global();
|
||||
config.set_secret(&api_key_name, &api_key)?;
|
||||
let api_key_env = if params.requires_auth {
|
||||
let api_key_name = generate_api_key_name(&id);
|
||||
let config = Config::global();
|
||||
config.set_secret(&api_key_name, ¶ms.api_key)?;
|
||||
api_key_name
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let model_infos: Vec<ModelInfo> = models
|
||||
let model_infos: Vec<ModelInfo> = params
|
||||
.models
|
||||
.into_iter()
|
||||
.map(|name| ModelInfo::new(name, 128000))
|
||||
.collect();
|
||||
|
||||
let provider_config = DeclarativeProviderConfig {
|
||||
name: id.clone(),
|
||||
engine: match engine {
|
||||
engine: match params.engine.as_str() {
|
||||
"openai_compatible" => ProviderEngine::OpenAI,
|
||||
"anthropic_compatible" => ProviderEngine::Anthropic,
|
||||
"ollama_compatible" => ProviderEngine::Ollama,
|
||||
_ => return Err(anyhow::anyhow!("Invalid provider type: {}", engine)),
|
||||
_ => return Err(anyhow::anyhow!("Invalid provider type: {}", params.engine)),
|
||||
},
|
||||
display_name: display_name.clone(),
|
||||
description: Some(format!("Custom {} provider", display_name)),
|
||||
api_key_env: api_key_name,
|
||||
base_url: api_url,
|
||||
display_name: params.display_name.clone(),
|
||||
description: Some(format!("Custom {} provider", params.display_name)),
|
||||
api_key_env,
|
||||
base_url: params.api_url,
|
||||
models: model_infos,
|
||||
headers,
|
||||
headers: params.headers,
|
||||
timeout_seconds: None,
|
||||
supports_streaming,
|
||||
supports_streaming: params.supports_streaming,
|
||||
requires_auth: params.requires_auth,
|
||||
};
|
||||
|
||||
let custom_providers_dir = custom_providers_dir();
|
||||
@@ -133,49 +166,54 @@ pub fn create_custom_provider(
|
||||
Ok(provider_config)
|
||||
}
|
||||
|
||||
pub fn update_custom_provider(
|
||||
id: &str,
|
||||
provider_type: &str,
|
||||
display_name: String,
|
||||
api_url: String,
|
||||
api_key: String,
|
||||
models: Vec<String>,
|
||||
supports_streaming: Option<bool>,
|
||||
) -> Result<()> {
|
||||
let loaded_provider = load_provider(id)?;
|
||||
pub fn update_custom_provider(params: UpdateCustomProviderParams) -> Result<()> {
|
||||
let loaded_provider = load_provider(¶ms.id)?;
|
||||
let existing_config = loaded_provider.config;
|
||||
let editable = loaded_provider.is_editable;
|
||||
|
||||
let config = Config::global();
|
||||
if !api_key.is_empty() {
|
||||
config.set_secret(&existing_config.api_key_env, &api_key)?;
|
||||
}
|
||||
|
||||
let api_key_env = if params.requires_auth {
|
||||
let api_key_name = if existing_config.api_key_env.is_empty() {
|
||||
generate_api_key_name(¶ms.id)
|
||||
} else {
|
||||
existing_config.api_key_env.clone()
|
||||
};
|
||||
if !params.api_key.is_empty() {
|
||||
config.set_secret(&api_key_name, ¶ms.api_key)?;
|
||||
}
|
||||
api_key_name
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
if editable {
|
||||
let model_infos: Vec<ModelInfo> = models
|
||||
let model_infos: Vec<ModelInfo> = params
|
||||
.models
|
||||
.into_iter()
|
||||
.map(|name| ModelInfo::new(name, 128000))
|
||||
.collect();
|
||||
|
||||
let updated_config = DeclarativeProviderConfig {
|
||||
name: id.to_string(),
|
||||
engine: match provider_type {
|
||||
name: params.id.clone(),
|
||||
engine: match params.engine.as_str() {
|
||||
"openai_compatible" => ProviderEngine::OpenAI,
|
||||
"anthropic_compatible" => ProviderEngine::Anthropic,
|
||||
"ollama_compatible" => ProviderEngine::Ollama,
|
||||
_ => return Err(anyhow::anyhow!("Invalid provider type: {}", provider_type)),
|
||||
_ => return Err(anyhow::anyhow!("Invalid provider type: {}", params.engine)),
|
||||
},
|
||||
display_name,
|
||||
display_name: params.display_name,
|
||||
description: existing_config.description,
|
||||
api_key_env: existing_config.api_key_env,
|
||||
base_url: api_url,
|
||||
api_key_env,
|
||||
base_url: params.api_url,
|
||||
models: model_infos,
|
||||
headers: existing_config.headers,
|
||||
headers: params.headers.or(existing_config.headers),
|
||||
timeout_seconds: existing_config.timeout_seconds,
|
||||
supports_streaming,
|
||||
supports_streaming: params.supports_streaming,
|
||||
requires_auth: params.requires_auth,
|
||||
};
|
||||
|
||||
let file_path = custom_providers_dir().join(format!("{}.json", id));
|
||||
let file_path = custom_providers_dir().join(format!("{}.json", updated_config.name));
|
||||
let json_content = serde_json::to_string_pretty(&updated_config)?;
|
||||
std::fs::write(file_path, json_content)?;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ pub struct ApiClient {
|
||||
}
|
||||
|
||||
pub enum AuthMethod {
|
||||
NoAuth,
|
||||
BearerToken(String),
|
||||
ApiKey {
|
||||
header_name: String,
|
||||
@@ -172,6 +173,7 @@ pub struct ApiResponse {
|
||||
impl fmt::Debug for AuthMethod {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
AuthMethod::NoAuth => f.debug_tuple("NoAuth").finish(),
|
||||
AuthMethod::BearerToken(_) => f.debug_tuple("BearerToken").field(&"[hidden]").finish(),
|
||||
AuthMethod::ApiKey { header_name, .. } => f
|
||||
.debug_struct("ApiKey")
|
||||
@@ -390,6 +392,7 @@ impl<'a> ApiRequestBuilder<'a> {
|
||||
request = request.headers(headers);
|
||||
|
||||
request = match &self.client.auth {
|
||||
AuthMethod::NoAuth => request,
|
||||
AuthMethod::BearerToken(token) => {
|
||||
request.header("Authorization", format!("Bearer {}", token))
|
||||
}
|
||||
|
||||
@@ -316,13 +316,12 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_openai_compatible_providers_config_keys() {
|
||||
let providers_list = providers().await;
|
||||
let cases = vec![
|
||||
("openai", "OPENAI_API_KEY"),
|
||||
let required_api_key_cases = vec![
|
||||
("groq", "GROQ_API_KEY"),
|
||||
("mistral", "MISTRAL_API_KEY"),
|
||||
("custom_deepseek", "DEEPSEEK_API_KEY"),
|
||||
];
|
||||
for (name, expected_key) in cases {
|
||||
for (name, expected_key) in required_api_key_cases {
|
||||
if let Some((meta, _)) = providers_list.iter().find(|(m, _)| m.name == name) {
|
||||
assert!(
|
||||
!meta.config_keys.is_empty(),
|
||||
@@ -346,5 +345,24 @@ mod tests {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((meta, _)) = providers_list.iter().find(|(m, _)| m.name == "openai") {
|
||||
assert!(
|
||||
!meta.config_keys.is_empty(),
|
||||
"openai provider should have config keys"
|
||||
);
|
||||
assert_eq!(
|
||||
meta.config_keys[0].name, "OPENAI_API_KEY",
|
||||
"First config key for openai should be OPENAI_API_KEY"
|
||||
);
|
||||
assert!(
|
||||
!meta.config_keys[0].required,
|
||||
"OPENAI_API_KEY should be optional for local server support"
|
||||
);
|
||||
assert!(
|
||||
meta.config_keys[0].secret,
|
||||
"OPENAI_API_KEY should be secret"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ impl LiteLLMProvider {
|
||||
let timeout_secs: u64 = config.get_param("LITELLM_TIMEOUT").unwrap_or(600);
|
||||
|
||||
let auth = if api_key.is_empty() {
|
||||
AuthMethod::Custom(Box::new(NoAuth))
|
||||
AuthMethod::NoAuth
|
||||
} else {
|
||||
AuthMethod::BearerToken(api_key)
|
||||
};
|
||||
@@ -129,17 +129,6 @@ impl LiteLLMProvider {
|
||||
}
|
||||
}
|
||||
|
||||
// No authentication provider for LiteLLM when API key is not provided
|
||||
struct NoAuth;
|
||||
|
||||
#[async_trait]
|
||||
impl super::api_client::AuthProvider for NoAuth {
|
||||
async fn get_auth_header(&self) -> Result<(String, String)> {
|
||||
// Return a dummy header that won't be used
|
||||
Ok(("X-No-Auth".to_string(), "true".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Provider for LiteLLMProvider {
|
||||
fn metadata() -> ProviderMetadata {
|
||||
|
||||
@@ -71,8 +71,8 @@ impl OllamaProvider {
|
||||
.map_err(|_| anyhow::anyhow!("Failed to set default port"))?;
|
||||
}
|
||||
|
||||
let auth = AuthMethod::Custom(Box::new(NoAuth));
|
||||
let api_client = ApiClient::with_timeout(base_url.to_string(), auth, timeout)?;
|
||||
let api_client =
|
||||
ApiClient::with_timeout(base_url.to_string(), AuthMethod::NoAuth, timeout)?;
|
||||
|
||||
Ok(Self {
|
||||
api_client,
|
||||
@@ -108,8 +108,8 @@ impl OllamaProvider {
|
||||
.map_err(|_| anyhow::anyhow!("Failed to set default port"))?;
|
||||
}
|
||||
|
||||
let auth = AuthMethod::Custom(Box::new(NoAuth));
|
||||
let api_client = ApiClient::with_timeout(base_url.to_string(), auth, timeout)?;
|
||||
let api_client =
|
||||
ApiClient::with_timeout(base_url.to_string(), AuthMethod::NoAuth, timeout)?;
|
||||
|
||||
Ok(Self {
|
||||
api_client,
|
||||
@@ -132,15 +132,6 @@ impl OllamaProvider {
|
||||
}
|
||||
}
|
||||
|
||||
struct NoAuth;
|
||||
|
||||
#[async_trait]
|
||||
impl super::api_client::AuthProvider for NoAuth {
|
||||
async fn get_auth_header(&self) -> Result<(String, String)> {
|
||||
Ok(("X-No-Auth".to_string(), "true".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Provider for OllamaProvider {
|
||||
fn metadata() -> ProviderMetadata {
|
||||
|
||||
@@ -67,23 +67,27 @@ impl OpenAiProvider {
|
||||
let model = model.with_fast(OPEN_AI_DEFAULT_FAST_MODEL.to_string());
|
||||
|
||||
let config = crate::config::Config::global();
|
||||
let secrets = config.get_secrets("OPENAI_API_KEY", &["OPENAI_CUSTOM_HEADERS"])?;
|
||||
let api_key = secrets.get("OPENAI_API_KEY").unwrap().clone();
|
||||
let host: String = config
|
||||
.get_param("OPENAI_HOST")
|
||||
.unwrap_or_else(|_| "https://api.openai.com".to_string());
|
||||
|
||||
let api_key: Option<String> = config.get_secret("OPENAI_API_KEY").ok();
|
||||
let custom_headers: Option<HashMap<String, String>> = config
|
||||
.get_secret::<String>("OPENAI_CUSTOM_HEADERS")
|
||||
.ok()
|
||||
.map(parse_custom_headers);
|
||||
|
||||
let base_path: String = config
|
||||
.get_param("OPENAI_BASE_PATH")
|
||||
.unwrap_or_else(|_| "v1/chat/completions".to_string());
|
||||
let organization: Option<String> = config.get_param("OPENAI_ORGANIZATION").ok();
|
||||
let project: Option<String> = config.get_param("OPENAI_PROJECT").ok();
|
||||
let custom_headers: Option<HashMap<String, String>> = secrets
|
||||
.get("OPENAI_CUSTOM_HEADERS")
|
||||
.cloned()
|
||||
.map(parse_custom_headers);
|
||||
let timeout_secs: u64 = config.get_param("OPENAI_TIMEOUT").unwrap_or(600);
|
||||
|
||||
let auth = AuthMethod::BearerToken(api_key);
|
||||
let auth = match api_key {
|
||||
Some(key) if !key.is_empty() => AuthMethod::BearerToken(key),
|
||||
_ => AuthMethod::NoAuth,
|
||||
};
|
||||
let mut api_client =
|
||||
ApiClient::with_timeout(host, auth, std::time::Duration::from_secs(timeout_secs))?;
|
||||
|
||||
@@ -136,9 +140,12 @@ impl OpenAiProvider {
|
||||
config: DeclarativeProviderConfig,
|
||||
) -> Result<Self> {
|
||||
let global_config = crate::config::Config::global();
|
||||
let api_key: String = global_config
|
||||
.get_secret(&config.api_key_env)
|
||||
.map_err(|_e| anyhow::anyhow!("Missing API key: {}", config.api_key_env))?;
|
||||
|
||||
let api_key: Option<String> = if config.requires_auth && !config.api_key_env.is_empty() {
|
||||
global_config.get_secret(&config.api_key_env).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let url = url::Url::parse(&config.base_url)
|
||||
.map_err(|e| anyhow::anyhow!("Invalid base URL '{}': {}", config.base_url, e))?;
|
||||
@@ -161,7 +168,11 @@ impl OpenAiProvider {
|
||||
};
|
||||
|
||||
let timeout_secs = config.timeout_seconds.unwrap_or(600);
|
||||
let auth = AuthMethod::BearerToken(api_key);
|
||||
|
||||
let auth = match api_key {
|
||||
Some(key) if !key.is_empty() => AuthMethod::BearerToken(key),
|
||||
_ => AuthMethod::NoAuth,
|
||||
};
|
||||
let mut api_client =
|
||||
ApiClient::with_timeout(host, auth, std::time::Duration::from_secs(timeout_secs))?;
|
||||
|
||||
@@ -232,7 +243,7 @@ impl Provider for OpenAiProvider {
|
||||
models,
|
||||
OPEN_AI_DOC_URL,
|
||||
vec![
|
||||
ConfigKey::new("OPENAI_API_KEY", true, true, None),
|
||||
ConfigKey::new("OPENAI_API_KEY", false, true, None),
|
||||
ConfigKey::new("OPENAI_HOST", true, false, Some("https://api.openai.com")),
|
||||
ConfigKey::new("OPENAI_BASE_PATH", true, false, Some("v1/chat/completions")),
|
||||
ConfigKey::new("OPENAI_ORGANIZATION", false, false, None),
|
||||
|
||||
@@ -98,12 +98,14 @@ impl ProviderRegistry {
|
||||
|
||||
let mut config_keys = base_metadata.config_keys.clone();
|
||||
|
||||
if let Some(api_key_index) = config_keys
|
||||
.iter()
|
||||
.position(|key| key.required && key.secret)
|
||||
{
|
||||
config_keys[api_key_index] =
|
||||
super::base::ConfigKey::new(&config.api_key_env, true, true, None);
|
||||
if let Some(api_key_index) = config_keys.iter().position(|key| key.secret) {
|
||||
if !config.requires_auth {
|
||||
config_keys.remove(api_key_index);
|
||||
} else if !config.api_key_env.is_empty() {
|
||||
let api_key_required = provider_type == ProviderType::Declarative;
|
||||
config_keys[api_key_index] =
|
||||
super::base::ConfigKey::new(&config.api_key_env, api_key_required, true, None);
|
||||
}
|
||||
}
|
||||
|
||||
let custom_metadata = ProviderMetadata {
|
||||
|
||||
@@ -3480,7 +3480,6 @@
|
||||
"name",
|
||||
"engine",
|
||||
"display_name",
|
||||
"api_key_env",
|
||||
"base_url",
|
||||
"models"
|
||||
],
|
||||
@@ -3517,6 +3516,9 @@
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"requires_auth": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"supports_streaming": {
|
||||
"type": "boolean",
|
||||
"nullable": true
|
||||
@@ -6698,6 +6700,9 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"requires_auth": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"supports_streaming": {
|
||||
"type": "boolean",
|
||||
"nullable": true
|
||||
|
||||
@@ -154,7 +154,7 @@ export type CspMetadata = {
|
||||
};
|
||||
|
||||
export type DeclarativeProviderConfig = {
|
||||
api_key_env: string;
|
||||
api_key_env?: string;
|
||||
base_url: string;
|
||||
description?: string | null;
|
||||
display_name: string;
|
||||
@@ -164,6 +164,7 @@ export type DeclarativeProviderConfig = {
|
||||
} | null;
|
||||
models: Array<ModelInfo>;
|
||||
name: string;
|
||||
requires_auth?: boolean;
|
||||
supports_streaming?: boolean | null;
|
||||
timeout_seconds?: number | null;
|
||||
};
|
||||
@@ -1204,6 +1205,7 @@ export type UpdateCustomProviderRequest = {
|
||||
[key: string]: string;
|
||||
} | null;
|
||||
models: Array<string>;
|
||||
requires_auth?: boolean;
|
||||
supports_streaming?: boolean | null;
|
||||
};
|
||||
|
||||
|
||||
+42
-36
@@ -24,7 +24,7 @@ export default function CustomProviderForm({
|
||||
const [apiUrl, setApiUrl] = useState('');
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [models, setModels] = useState('');
|
||||
const [isLocalModel, setIsLocalModel] = useState(false);
|
||||
const [noAuthRequired, setNoAuthRequired] = useState(false);
|
||||
const [supportsStreaming, setSupportsStreaming] = useState(true);
|
||||
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
|
||||
|
||||
@@ -40,14 +40,13 @@ export default function CustomProviderForm({
|
||||
setApiUrl(initialData.api_url);
|
||||
setModels(initialData.models.join(', '));
|
||||
setSupportsStreaming(initialData.supports_streaming ?? true);
|
||||
setNoAuthRequired(!(initialData.requires_auth ?? true));
|
||||
}
|
||||
}, [initialData]);
|
||||
|
||||
const handleLocalModels = (checked: boolean) => {
|
||||
setIsLocalModel(checked);
|
||||
const handleNoAuthChange = (checked: boolean) => {
|
||||
setNoAuthRequired(!!checked);
|
||||
if (checked) {
|
||||
setApiKey('notrequired');
|
||||
} else {
|
||||
setApiKey('');
|
||||
}
|
||||
};
|
||||
@@ -58,7 +57,8 @@ export default function CustomProviderForm({
|
||||
const errors: Record<string, string> = {};
|
||||
if (!displayName) errors.displayName = 'Display name is required';
|
||||
if (!apiUrl) errors.apiUrl = 'API URL is required';
|
||||
if (!isLocalModel && !apiKey && !initialData) errors.apiKey = 'API key is required';
|
||||
const existingHadAuth = initialData && (initialData.requires_auth ?? true);
|
||||
if (!noAuthRequired && !apiKey && !existingHadAuth) errors.apiKey = 'API key is required';
|
||||
if (!models) errors.models = 'At least one model is required';
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
@@ -78,6 +78,7 @@ export default function CustomProviderForm({
|
||||
api_key: apiKey,
|
||||
models: modelList,
|
||||
supports_streaming: supportsStreaming,
|
||||
requires_auth: !noAuthRequired,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -173,40 +174,45 @@ export default function CustomProviderForm({
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="api-key"
|
||||
className="flex items-center text-sm font-medium text-textStandard mb-2"
|
||||
>
|
||||
API Key
|
||||
{!isLocalModel && !initialData && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
<Input
|
||||
id="api-key"
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder={initialData ? 'Leave blank to keep existing key' : 'Your API key'}
|
||||
aria-invalid={!!validationErrors.apiKey}
|
||||
aria-describedby={validationErrors.apiKey ? 'api-key-error' : undefined}
|
||||
className={validationErrors.apiKey ? 'border-red-500' : ''}
|
||||
disabled={isLocalModel}
|
||||
/>
|
||||
{validationErrors.apiKey && (
|
||||
<p id="api-key-error" className="text-red-500 text-sm mt-1">
|
||||
{validationErrors.apiKey}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<Checkbox
|
||||
id="no-auth-required"
|
||||
checked={noAuthRequired}
|
||||
onCheckedChange={handleNoAuthChange}
|
||||
/>
|
||||
<label
|
||||
htmlFor="no-auth-required"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-textSubtle"
|
||||
>
|
||||
No authentication required
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{!initialData && (
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<Checkbox id="local-model" checked={isLocalModel} onCheckedChange={handleLocalModels} />
|
||||
{!noAuthRequired && (
|
||||
<>
|
||||
<label
|
||||
htmlFor="local-model"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-textSubtle"
|
||||
htmlFor="api-key"
|
||||
className="flex items-center text-sm font-medium text-textStandard mb-2"
|
||||
>
|
||||
This is a local model (no auth required)
|
||||
API Key
|
||||
{!initialData && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
</div>
|
||||
<Input
|
||||
id="api-key"
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder={initialData ? 'Leave blank to keep existing key' : 'Your API key'}
|
||||
aria-invalid={!!validationErrors.apiKey}
|
||||
aria-describedby={validationErrors.apiKey ? 'api-key-error' : undefined}
|
||||
className={validationErrors.apiKey ? 'border-red-500' : ''}
|
||||
/>
|
||||
{validationErrors.apiKey && (
|
||||
<p id="api-key-error" className="text-red-500 text-sm mt-1">
|
||||
{validationErrors.apiKey}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{isEditable && (
|
||||
|
||||
Reference in New Issue
Block a user