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