Custom providers update (#4099)

Co-authored-by: developerayo <shodipovi@gmail.com>
Co-authored-by: Douwe Osinga <douwe@squareup.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Zane Staggs <zane@squareup.com>
This commit is contained in:
Douwe Osinga
2025-08-19 18:03:10 -04:00
committed by GitHub
parent 1d93a59f31
commit 942ef5b0a3
31 changed files with 1704 additions and 567 deletions
Generated
+2
View File
@@ -2687,6 +2687,7 @@ dependencies = [
"tracing",
"tracing-appender",
"tracing-subscriber",
"uuid",
"webbrowser 1.0.4",
"winapi",
]
@@ -2774,6 +2775,7 @@ dependencies = [
"tracing-appender",
"tracing-subscriber",
"utoipa",
"uuid",
]
[[package]]
+1
View File
@@ -47,6 +47,7 @@ shlex = "1.3.0"
async-trait = "0.1.86"
base64 = "0.22.1"
regex = "1.11.1"
uuid = { version = "1.11", features = ["v4"] }
nix = { version = "0.30.1", features = ["process", "signal"] }
tar = "0.4"
# Web server dependencies
+139 -12
View File
@@ -1,3 +1,4 @@
use crate::recipes::github_recipe::GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY;
use cliclack::spinner;
use console::style;
use goose::agents::extension::ToolInfo;
@@ -7,6 +8,7 @@ use goose::agents::platform_tools::{
};
use goose::agents::Agent;
use goose::agents::{extension::Envs, ExtensionConfig};
use goose::config::custom_providers::CustomProviderConfig;
use goose::config::extensions::name_to_key;
use goose::config::permission::PermissionLevel;
use goose::config::{
@@ -14,6 +16,7 @@ use goose::config::{
PermissionManager,
};
use goose::conversation::message::Message;
use goose::model::ModelConfig;
use goose::providers::{create, providers};
use rmcp::model::{Tool, ToolAnnotations};
use rmcp::object;
@@ -21,8 +24,6 @@ use serde_json::Value;
use std::collections::HashMap;
use std::error::Error;
use crate::recipes::github_recipe::GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY;
// useful for light themes where there is no dicernible colour contrast between
// cursor-selected and cursor-unselected items.
const MULTISELECT_VISIBILITY_HINT: &str = "<";
@@ -221,6 +222,11 @@ pub async fn handle_configure() -> Result<(), Box<dyn Error>> {
"Configure Providers",
"Change provider or update credentials",
)
.item(
"custom_providers",
"Custom Providers",
"Add custom provider with compatible API",
)
.item("add", "Add Extension", "Connect to a new extension")
.item(
"toggle",
@@ -241,6 +247,7 @@ pub async fn handle_configure() -> Result<(), Box<dyn Error>> {
"remove" => remove_extension_dialog(),
"settings" => configure_settings_dialog().await.and(Ok(())),
"providers" => configure_provider_dialog().await.and(Ok(())),
"custom_providers" => configure_custom_provider_dialog(),
_ => unreachable!(),
}
}
@@ -250,10 +257,7 @@ pub async fn handle_configure() -> Result<(), Box<dyn Error>> {
async fn handle_oauth_configuration(
provider_name: &str,
key_name: &str,
) -> Result<(), Box<dyn std::error::Error>> {
use goose::model::ModelConfig;
use goose::providers::create;
) -> Result<(), Box<dyn Error>> {
let _ = cliclack::log::info(format!(
"Configuring {} using OAuth device code flow...",
key_name
@@ -279,8 +283,7 @@ async fn handle_oauth_configuration(
}
}
/// Interactive model search that truncates the list to improve UX
fn interactive_model_search(models: &[String]) -> Result<String, Box<dyn std::error::Error>> {
fn interactive_model_search(models: &[String]) -> Result<String, Box<dyn Error>> {
const MAX_VISIBLE: usize = 30;
let mut query = String::new();
@@ -553,7 +556,7 @@ pub async fn configure_provider_dialog() -> Result<bool, Box<dyn Error>> {
let spin = spinner();
spin.start("Attempting to fetch supported models...");
let models_res = {
let temp_model_config = goose::model::ModelConfig::new(&provider_meta.default_model)?;
let temp_model_config = ModelConfig::new(&provider_meta.default_model)?;
let temp_provider = create(provider_name, temp_model_config)?;
temp_provider.fetch_supported_models().await
};
@@ -585,7 +588,7 @@ pub async fn configure_provider_dialog() -> Result<bool, Box<dyn Error>> {
.map(|val| val == "1" || val.to_lowercase() == "true")
.unwrap_or(false);
let model_config = goose::model::ModelConfig::new(&model)?
let model_config = ModelConfig::new(&model)?
.with_max_tokens(Some(50))
.with_toolshim(toolshim_enabled)
.with_toolshim_model(std::env::var("GOOSE_TOOLSHIM_OLLAMA_MODEL").ok());
@@ -1429,7 +1432,7 @@ pub async fn configure_tool_permissions_dialog() -> Result<(), Box<dyn Error>> {
let model: String = config
.get_param("GOOSE_MODEL")
.expect("No model configured. Please set model first");
let model_config = goose::model::ModelConfig::new(&model)?;
let model_config = ModelConfig::new(&model)?;
// Create the agent
let agent = Agent::new();
@@ -1569,7 +1572,6 @@ fn configure_recipe_dialog() -> Result<(), Box<dyn Error>> {
recipe_repo_input = recipe_repo_input.default_input(&recipe_repo);
}
let input_value: String = recipe_repo_input.interact()?;
// if input is blank, it clears the recipe github repo settings in the config file
if input_value.clone().trim().is_empty() {
config.delete(key_name)?;
} else {
@@ -1767,3 +1769,128 @@ pub async fn handle_openrouter_auth() -> Result<(), Box<dyn Error>> {
Ok(())
}
fn add_provider() -> Result<(), Box<dyn Error>> {
let provider_type = cliclack::select("What type of API is this?")
.item(
"openai_compatible",
"OpenAI Compatible",
"Uses OpenAI API format",
)
.item(
"anthropic_compatible",
"Anthropic Compatible",
"Uses Anthropic API format",
)
.item(
"ollama_compatible",
"Ollama Compatible",
"Uses Ollama API format",
)
.interact()?;
let display_name: String = cliclack::input("What should we call this provider?")
.placeholder("Your Provider Name")
.validate(|input: &String| {
if input.is_empty() {
Err("Please enter a name")
} else {
Ok(())
}
})
.interact()?;
let api_url: String = cliclack::input("Provider API URL:")
.placeholder("https://api.example.com/v1/messages")
.validate(|input: &String| {
if !input.starts_with("http://") && !input.starts_with("https://") {
Err("URL must start with either http:// or https://")
} else {
Ok(())
}
})
.interact()?;
let api_key: String = cliclack::password("API key:").mask('▪').interact()?;
let models_input: String = cliclack::input("Available models (seperate with commas):")
.placeholder("model-a, model-b, model-c")
.validate(|input: &String| {
if input.trim().is_empty() {
Err("Please enter at least one model name")
} else {
Ok(())
}
})
.interact()?;
let models: Vec<String> = models_input
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
let supports_streaming = cliclack::confirm("Does this provider support streaming responses?")
.initial_value(true)
.interact()?;
CustomProviderConfig::create_and_save(
provider_type,
display_name.clone(),
api_url,
api_key,
models,
Some(supports_streaming),
)?;
cliclack::outro(format!("Custom provider added: {}", display_name))?;
Ok(())
}
fn remove_provider() -> Result<(), Box<dyn Error>> {
let custom_providers_dir = goose::config::custom_providers::custom_providers_dir();
let custom_providers = if custom_providers_dir.exists() {
goose::config::custom_providers::load_custom_providers(&custom_providers_dir)?
} else {
Vec::new()
};
if custom_providers.is_empty() {
cliclack::outro("No custom providers added just yet.")?;
return Ok(());
}
let provider_items: Vec<_> = custom_providers
.iter()
.map(|p| (p.name.as_str(), p.display_name.as_str(), "Custom provider"))
.collect();
let selected_id = cliclack::select("Which custom provider would you like to remove?")
.items(&provider_items)
.interact()?;
CustomProviderConfig::remove(selected_id)?;
cliclack::outro(format!("Removed custom provider: {}", selected_id))?;
Ok(())
}
pub fn configure_custom_provider_dialog() -> Result<(), Box<dyn Error>> {
let action = cliclack::select("What would you like to do?")
.item(
"add",
"Add A Custom Provider",
"Add a new OpenAI/Anthropic/Ollama compatible Provider",
)
.item(
"remove",
"Remove Custom Provider",
"Remove an existing custom provider",
)
.interact()?;
match action {
"add" => add_provider(),
"remove" => remove_provider(),
_ => unreachable!(),
}
}
+1
View File
@@ -40,6 +40,7 @@ serde_yaml = "0.9.34"
utoipa = { version = "4.1", features = ["axum_extras", "chrono"] }
reqwest = { version = "0.12.9", features = ["json", "rustls-tls", "blocking", "multipart"], default-features = false }
tokio-util = "0.7.15"
uuid = { version = "1.11", features = ["v4"] }
[[bin]]
name = "goosed"
+3
View File
@@ -367,6 +367,8 @@ impl<'__s> ToSchema<'__s> for AnnotatedSchema {
super::routes::config_management::read_all_config,
super::routes::config_management::providers,
super::routes::config_management::upsert_permissions,
super::routes::config_management::create_custom_provider,
super::routes::config_management::remove_custom_provider,
super::routes::agent::get_tools,
super::routes::agent::add_sub_recipes,
super::routes::agent::extend_prompt,
@@ -402,6 +404,7 @@ impl<'__s> ToSchema<'__s> for AnnotatedSchema {
super::routes::config_management::ExtensionQuery,
super::routes::config_management::ToolPermission,
super::routes::config_management::UpsertPermissionsQuery,
super::routes::config_management::CreateCustomProviderRequest,
super::routes::reply::PermissionConfirmationRequest,
super::routes::context::ContextManageRequest,
super::routes::context::ContextManageResponse,
+2 -2
View File
@@ -192,7 +192,7 @@ async fn update_agent_provider(
let agent = state
.get_agent()
.await
.map_err(|_| StatusCode::PRECONDITION_FAILED)?;
.map_err(|_e| StatusCode::PRECONDITION_FAILED)?;
let config = Config::global();
let model = match payload
@@ -210,7 +210,7 @@ async fn update_agent_provider(
agent
.update_provider(new_provider)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
.map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(StatusCode::OK)
}
@@ -8,7 +8,6 @@ use axum::{
};
use etcetera::{choose_app_strategy, AppStrategy};
use goose::config::APP_STRATEGY;
use goose::config::{extensions::name_to_key, PermissionManager};
use goose::config::{Config, ConfigError};
use goose::config::{ExtensionConfigManager, ExtensionEntry};
use goose::model::ModelConfig;
@@ -78,6 +77,16 @@ pub struct UpsertPermissionsQuery {
pub tool_permissions: Vec<ToolPermission>,
}
#[derive(Deserialize, ToSchema)]
pub struct CreateCustomProviderRequest {
pub provider_type: String,
pub display_name: String,
pub api_url: String,
pub api_key: String,
pub models: Vec<String>,
pub supports_streaming: Option<bool>,
}
#[utoipa::path(
post,
path = "/config/upsert",
@@ -227,7 +236,7 @@ pub async fn add_extension(
let extensions =
ExtensionConfigManager::get_all().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let key = name_to_key(&extension_query.name);
let key = goose::config::extensions::name_to_key(&extension_query.name);
let is_update = extensions.iter().any(|e| e.config.key() == key);
@@ -262,7 +271,7 @@ pub async fn remove_extension(
) -> Result<Json<String>, StatusCode> {
verify_secret_key(&headers, &state)?;
let key = name_to_key(&name);
let key = goose::config::extensions::name_to_key(&name);
match ExtensionConfigManager::remove(&key) {
Ok(_) => Ok(Json(format!("Removed extension {}", name))),
Err(_) => Err(StatusCode::NOT_FOUND),
@@ -304,7 +313,62 @@ pub async fn providers(
) -> Result<Json<Vec<ProviderDetails>>, StatusCode> {
verify_secret_key(&headers, &state)?;
let providers_metadata = get_providers();
let mut providers_metadata = get_providers();
let custom_providers_dir = goose::config::custom_providers::custom_providers_dir();
if custom_providers_dir.exists() {
if let Ok(entries) = std::fs::read_dir(&custom_providers_dir) {
for entry in entries.flatten() {
if let Some(extension) = entry.path().extension() {
if extension == "json" {
if let Ok(content) = std::fs::read_to_string(entry.path()) {
if let Ok(custom_provider) = serde_json::from_str::<
goose::config::custom_providers::CustomProviderConfig,
>(&content)
{
// CustomProviderConfig => ProviderMetadata
let default_model = custom_provider
.models
.first()
.map(|m| m.name.clone())
.unwrap_or_default();
let metadata = goose::providers::base::ProviderMetadata {
name: custom_provider.name.clone(),
display_name: custom_provider.display_name.clone(),
description: custom_provider
.description
.clone()
.unwrap_or_else(|| {
format!("{} (custom)", custom_provider.display_name)
}),
default_model,
known_models: custom_provider.models.clone(),
model_doc_link: "Custom provider".to_string(),
config_keys: vec![
goose::providers::base::ConfigKey::new(
&custom_provider.api_key_env,
true,
true,
None,
),
goose::providers::base::ConfigKey::new(
"CUSTOM_PROVIDER_BASE_URL",
true,
false,
Some(&custom_provider.base_url),
),
],
};
providers_metadata.push(metadata);
}
}
}
}
}
}
}
let providers_response: Vec<ProviderDetails> = providers_metadata
.into_iter()
@@ -491,7 +555,7 @@ pub async fn upsert_permissions(
) -> Result<Json<String>, StatusCode> {
verify_secret_key(&headers, &state)?;
let mut permission_manager = PermissionManager::default();
let mut permission_manager = goose::config::PermissionManager::default();
for tool_permission in &query.tool_permissions {
permission_manager.update_user_permission(
@@ -637,6 +701,66 @@ pub async fn get_current_model(
})))
}
#[utoipa::path(
post,
path = "/config/custom-providers",
request_body = CreateCustomProviderRequest,
responses(
(status = 200, description = "Custom provider created successfully", body = String),
(status = 400, description = "Invalid request"),
(status = 500, description = "Internal server error")
)
)]
pub async fn create_custom_provider(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Json(request): Json<CreateCustomProviderRequest>,
) -> Result<Json<String>, StatusCode> {
verify_secret_key(&headers, &state)?;
let config = goose::config::custom_providers::CustomProviderConfig::create_and_save(
&request.provider_type,
request.display_name,
request.api_url,
request.api_key,
request.models,
request.supports_streaming,
)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if let Err(e) = goose::providers::refresh_custom_providers() {
tracing::warn!("Failed to refresh custom providers after creation: {}", e);
}
Ok(Json(format!("Custom provider added - ID: {}", config.id())))
}
#[utoipa::path(
delete,
path = "/config/custom-providers/{id}",
responses(
(status = 200, description = "Custom provider removed successfully", body = String),
(status = 404, description = "Provider not found"),
(status = 500, description = "Internal server error")
)
)]
pub async fn remove_custom_provider(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
axum::extract::Path(id): axum::extract::Path<String>,
) -> Result<Json<String>, StatusCode> {
verify_secret_key(&headers, &state)?;
goose::config::custom_providers::CustomProviderConfig::remove(&id)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if let Err(e) = goose::providers::refresh_custom_providers() {
tracing::warn!("Failed to refresh custom providers after deletion: {}", e);
}
Ok(Json(format!("Removed custom provider: {}", id)))
}
pub fn routes(state: Arc<AppState>) -> Router {
Router::new()
.route("/config", get(read_all_config))
@@ -654,6 +778,11 @@ pub fn routes(state: Arc<AppState>) -> Router {
.route("/config/validate", get(validate_config))
.route("/config/permissions", post(upsert_permissions))
.route("/config/current-model", get(get_current_model))
.route("/config/custom-providers", post(create_custom_provider))
.route(
"/config/custom-providers/{id}",
delete(remove_custom_provider),
)
.with_state(state)
}
+215
View File
@@ -0,0 +1,215 @@
use crate::config::{Config, APP_STRATEGY};
use crate::model::ModelConfig;
use crate::providers::anthropic::AnthropicProvider;
use crate::providers::base::ModelInfo;
use crate::providers::ollama::OllamaProvider;
use crate::providers::openai::OpenAiProvider;
use anyhow::Result;
use etcetera::{choose_app_strategy, AppStrategy};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
pub fn custom_providers_dir() -> std::path::PathBuf {
choose_app_strategy(APP_STRATEGY.clone())
.expect("goose requires a home dir")
.config_dir()
.join("custom_providers")
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ProviderEngine {
OpenAI,
Ollama,
Anthropic,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomProviderConfig {
pub name: String,
pub engine: ProviderEngine,
pub display_name: String,
pub description: Option<String>,
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>,
}
impl CustomProviderConfig {
pub fn id(&self) -> &str {
&self.name
}
pub fn display_name(&self) -> &str {
&self.display_name
}
pub fn models(&self) -> &[ModelInfo] {
&self.models
}
pub fn generate_id(display_name: &str) -> String {
format!("custom_{}", display_name.to_lowercase().replace(' ', "_"))
}
pub fn generate_api_key_name(id: &str) -> String {
format!("{}_API_KEY", id.to_uppercase())
}
pub fn create_and_save(
provider_type: &str,
display_name: String,
api_url: String,
api_key: String,
models: Vec<String>,
supports_streaming: Option<bool>,
) -> Result<Self> {
let id = Self::generate_id(&display_name);
let api_key_name = Self::generate_api_key_name(&id);
let config = Config::global();
config.set_secret(&api_key_name, serde_json::Value::String(api_key))?;
let model_infos: Vec<ModelInfo> = models
.into_iter()
.map(|name| ModelInfo::new(name, 128000))
.collect();
let provider_config = CustomProviderConfig {
name: id.clone(),
engine: match provider_type {
"openai_compatible" => ProviderEngine::OpenAI,
"anthropic_compatible" => ProviderEngine::Anthropic,
"ollama_compatible" => ProviderEngine::Ollama,
_ => return Err(anyhow::anyhow!("Invalid provider type: {}", provider_type)),
},
display_name: display_name.clone(),
description: Some(format!("Custom {} provider", display_name)),
api_key_env: api_key_name,
base_url: api_url,
models: model_infos,
headers: None,
timeout_seconds: None,
supports_streaming,
};
// save to JSON file
let custom_providers_dir = custom_providers_dir();
std::fs::create_dir_all(&custom_providers_dir)?;
let json_content = serde_json::to_string_pretty(&provider_config)?;
let file_path = custom_providers_dir.join(format!("{}.json", id));
std::fs::write(file_path, json_content)?;
Ok(provider_config)
}
pub fn remove(id: &str) -> Result<()> {
let config = Config::global();
let api_key_name = Self::generate_api_key_name(id);
let _ = config.delete_secret(&api_key_name);
let custom_providers_dir = custom_providers_dir();
let file_path = custom_providers_dir.join(format!("{}.json", id));
if file_path.exists() {
std::fs::remove_file(file_path)?;
}
Ok(())
}
}
pub fn load_custom_providers(dir: &Path) -> Result<Vec<CustomProviderConfig>> {
if !dir.exists() {
return Ok(Vec::new());
}
std::fs::read_dir(dir)?
.filter_map(|entry| {
let path = entry.ok()?.path();
(path.extension()? == "json").then_some(path)
})
.map(|path| {
let content = std::fs::read_to_string(&path)?;
serde_json::from_str(&content)
.map_err(|e| anyhow::anyhow!("Failed to parse {}: {}", path.display(), e))
})
.collect()
}
pub fn register_custom_providers(
registry: &mut crate::providers::provider_registry::ProviderRegistry,
dir: &Path,
) -> Result<()> {
let configs = load_custom_providers(dir)?;
for config in configs {
let config_clone = config.clone();
let description = config
.description
.clone()
.unwrap_or_else(|| format!("Custom {} provider", config.display_name));
let default_model = config
.models
.first()
.map(|m| m.name.clone())
.unwrap_or_default();
let known_models: Vec<ModelInfo> = config
.models
.iter()
.map(|m| ModelInfo {
name: m.name.clone(),
context_limit: m.context_limit,
input_token_cost: m.input_token_cost,
output_token_cost: m.output_token_cost,
currency: m.currency.clone(),
supports_cache_control: Some(m.supports_cache_control.unwrap_or(false)),
})
.collect();
match config.engine {
ProviderEngine::OpenAI => {
registry.register_with_name::<OpenAiProvider, _>(
config.name.clone(),
config.display_name.clone(),
description,
default_model,
known_models,
move |model: ModelConfig| {
OpenAiProvider::from_custom_config(model, config_clone.clone())
},
);
}
ProviderEngine::Ollama => {
registry.register_with_name::<OllamaProvider, _>(
config.name.clone(),
config.display_name.clone(),
description,
default_model,
known_models,
move |model: ModelConfig| {
OllamaProvider::from_custom_config(model, config_clone.clone())
},
);
}
ProviderEngine::Anthropic => {
registry.register_with_name::<AnthropicProvider, _>(
config.name.clone(),
config.display_name.clone(),
description,
default_model,
known_models,
move |model: ModelConfig| {
AnthropicProvider::from_custom_config(model, config_clone.clone())
},
);
}
}
}
Ok(())
}
+2
View File
@@ -1,4 +1,5 @@
pub mod base;
pub mod custom_providers;
mod experiments;
pub mod extensions;
pub mod permission;
@@ -6,6 +7,7 @@ pub mod signup_openrouter;
pub use crate::agents::ExtensionConfig;
pub use base::{Config, ConfigError, APP_STRATEGY};
pub use custom_providers::CustomProviderConfig;
pub use experiments::ExperimentManager;
pub use extensions::{ExtensionConfigManager, ExtensionEntry};
pub use permission::PermissionManager;
+29 -2
View File
@@ -15,6 +15,7 @@ use super::formats::anthropic::{
create_request, get_usage, response_to_message, response_to_streaming_message,
};
use super::utils::{emit_debug_trace, get_model, map_http_error_to_provider_error};
use crate::config::custom_providers::CustomProviderConfig;
use crate::conversation::message::Message;
use crate::impl_provider_default;
use crate::model::ModelConfig;
@@ -42,6 +43,7 @@ pub struct AnthropicProvider {
#[serde(skip)]
api_client: ApiClient,
model: ModelConfig,
supports_streaming: bool,
}
impl_provider_default!(AnthropicProvider);
@@ -62,7 +64,32 @@ impl AnthropicProvider {
let api_client =
ApiClient::new(host, auth)?.with_header("anthropic-version", ANTHROPIC_API_VERSION)?;
Ok(Self { api_client, model })
Ok(Self {
api_client,
model,
supports_streaming: true,
})
}
pub fn from_custom_config(model: ModelConfig, config: CustomProviderConfig) -> Result<Self> {
let global_config = crate::config::Config::global();
let api_key: String = global_config
.get_secret(&config.api_key_env)
.map_err(|_| anyhow::anyhow!("Missing API key: {}", config.api_key_env))?;
let auth = AuthMethod::ApiKey {
header_name: "x-api-key".to_string(),
key: api_key,
};
let api_client = ApiClient::new(config.base_url, auth)?
.with_header("anthropic-version", ANTHROPIC_API_VERSION)?;
Ok(Self {
api_client,
model,
supports_streaming: config.supports_streaming.unwrap_or(true),
})
}
fn get_conditional_headers(&self) -> Vec<(&str, &str)> {
@@ -260,6 +287,6 @@ impl Provider for AnthropicProvider {
}
fn supports_streaming(&self) -> bool {
true
self.supports_streaming
}
}
+160 -249
View File
@@ -1,4 +1,4 @@
use std::sync::Arc;
use std::sync::{Arc, RwLock};
use super::{
anthropic::AnthropicProvider,
@@ -17,66 +17,87 @@ use super::{
ollama::OllamaProvider,
openai::OpenAiProvider,
openrouter::OpenRouterProvider,
provider_registry::ProviderRegistry,
sagemaker_tgi::SageMakerTgiProvider,
snowflake::SnowflakeProvider,
venice::VeniceProvider,
xai::XaiProvider,
};
use crate::config::custom_providers::{custom_providers_dir, register_custom_providers};
use crate::model::ModelConfig;
use anyhow::Result;
use once_cell::sync::Lazy;
#[cfg(test)]
use super::errors::ProviderError;
#[cfg(test)]
use rmcp::model::Tool;
fn default_lead_turns() -> usize {
3
}
fn default_failure_threshold() -> usize {
2
}
fn default_fallback_turns() -> usize {
2
const DEFAULT_LEAD_TURNS: usize = 3;
const DEFAULT_FAILURE_THRESHOLD: usize = 2;
const DEFAULT_FALLBACK_TURNS: usize = 2;
static REGISTRY: Lazy<RwLock<ProviderRegistry>> = Lazy::new(|| {
let registry = ProviderRegistry::new().with_providers(|registry| {
registry.register::<AnthropicProvider, _>(AnthropicProvider::from_env);
registry.register::<AzureProvider, _>(AzureProvider::from_env);
registry.register::<BedrockProvider, _>(BedrockProvider::from_env);
registry.register::<ClaudeCodeProvider, _>(ClaudeCodeProvider::from_env);
registry.register::<CursorAgentProvider, _>(CursorAgentProvider::from_env);
registry.register::<DatabricksProvider, _>(DatabricksProvider::from_env);
registry.register::<GcpVertexAIProvider, _>(GcpVertexAIProvider::from_env);
registry.register::<GeminiCliProvider, _>(GeminiCliProvider::from_env);
registry.register::<GoogleProvider, _>(GoogleProvider::from_env);
registry.register::<GroqProvider, _>(GroqProvider::from_env);
registry.register::<LiteLLMProvider, _>(LiteLLMProvider::from_env);
registry.register::<OllamaProvider, _>(OllamaProvider::from_env);
registry.register::<OpenAiProvider, _>(OpenAiProvider::from_env);
registry.register::<OpenRouterProvider, _>(OpenRouterProvider::from_env);
registry.register::<SageMakerTgiProvider, _>(SageMakerTgiProvider::from_env);
registry.register::<SnowflakeProvider, _>(SnowflakeProvider::from_env);
registry.register::<VeniceProvider, _>(VeniceProvider::from_env);
registry.register::<XaiProvider, _>(XaiProvider::from_env);
if let Err(e) = load_custom_providers_into_registry(registry) {
tracing::warn!("Failed to load custom providers: {}", e);
}
});
RwLock::new(registry)
});
fn load_custom_providers_into_registry(registry: &mut ProviderRegistry) -> Result<()> {
let config_dir = custom_providers_dir();
register_custom_providers(registry, &config_dir)
}
pub fn providers() -> Vec<ProviderMetadata> {
vec![
AnthropicProvider::metadata(),
AzureProvider::metadata(),
BedrockProvider::metadata(),
ClaudeCodeProvider::metadata(),
CursorAgentProvider::metadata(),
DatabricksProvider::metadata(),
GcpVertexAIProvider::metadata(),
GeminiCliProvider::metadata(),
// GithubCopilotProvider::metadata(),
GoogleProvider::metadata(),
GroqProvider::metadata(),
LiteLLMProvider::metadata(),
OllamaProvider::metadata(),
OpenAiProvider::metadata(),
OpenRouterProvider::metadata(),
SageMakerTgiProvider::metadata(),
VeniceProvider::metadata(),
SnowflakeProvider::metadata(),
XaiProvider::metadata(),
]
REGISTRY.read().unwrap().all_metadata()
}
pub fn refresh_custom_providers() -> Result<()> {
let mut registry = REGISTRY.write().unwrap();
registry.remove_custom_providers();
if let Err(e) = load_custom_providers_into_registry(&mut registry) {
tracing::warn!("Failed to refresh custom providers: {}", e);
return Err(e);
}
tracing::info!("Custom providers refreshed");
Ok(())
}
pub fn create(name: &str, model: ModelConfig) -> Result<Arc<dyn Provider>> {
let config = crate::config::Config::global();
// Check for lead model environment variables
if let Ok(lead_model_name) = config.get_param::<String>("GOOSE_LEAD_MODEL") {
tracing::info!("Creating lead/worker provider from environment variables");
return create_lead_worker_from_env(name, &model, &lead_model_name);
}
create_provider(name, model)
REGISTRY.read().unwrap().create(name, model)
}
/// Create a lead/worker provider from environment variables
fn create_lead_worker_from_env(
default_provider_name: &str,
default_model: &ModelConfig,
@@ -84,61 +105,36 @@ fn create_lead_worker_from_env(
) -> Result<Arc<dyn Provider>> {
let config = crate::config::Config::global();
// Get lead provider (optional, defaults to main provider)
let lead_provider_name = config
.get_param::<String>("GOOSE_LEAD_PROVIDER")
.unwrap_or_else(|_| default_provider_name.to_string());
// Get configuration parameters with defaults
let lead_turns = config
.get_param::<usize>("GOOSE_LEAD_TURNS")
.unwrap_or(default_lead_turns());
.unwrap_or(DEFAULT_LEAD_TURNS);
let failure_threshold = config
.get_param::<usize>("GOOSE_LEAD_FAILURE_THRESHOLD")
.unwrap_or(default_failure_threshold());
.unwrap_or(DEFAULT_FAILURE_THRESHOLD);
let fallback_turns = config
.get_param::<usize>("GOOSE_LEAD_FALLBACK_TURNS")
.unwrap_or(default_fallback_turns());
.unwrap_or(DEFAULT_FALLBACK_TURNS);
let lead_model_config = ModelConfig::new_with_context_env(
lead_model_name.to_string(),
Some("GOOSE_LEAD_CONTEXT_LIMIT"),
)?;
// For worker model, preserve the original context_limit from config (highest precedence)
// while still allowing environment variable overrides
let worker_model_config = {
// Start with a clone of the original model to preserve user-specified settings
let mut worker_config = ModelConfig::new_or_fail(default_model.model_name.as_str())
.with_context_limit(default_model.context_limit)
.with_temperature(default_model.temperature)
.with_max_tokens(default_model.max_tokens)
.with_toolshim(default_model.toolshim)
.with_toolshim_model(default_model.toolshim_model.clone());
let worker_model_config = create_worker_model_config(default_model)?;
// Apply environment variable overrides with proper precedence
let global_config = crate::config::Config::global();
let lead_provider = REGISTRY
.read()
.unwrap()
.create(&lead_provider_name, lead_model_config)?;
let worker_provider = REGISTRY
.read()
.unwrap()
.create(default_provider_name, worker_model_config)?;
// Check for worker-specific context limit
if let Ok(limit_str) = global_config.get_param::<String>("GOOSE_WORKER_CONTEXT_LIMIT") {
if let Ok(limit) = limit_str.parse::<usize>() {
worker_config = worker_config.with_context_limit(Some(limit));
}
} else if let Ok(limit_str) = global_config.get_param::<String>("GOOSE_CONTEXT_LIMIT") {
// Check for general context limit if worker-specific is not set
if let Ok(limit) = limit_str.parse::<usize>() {
worker_config = worker_config.with_context_limit(Some(limit));
}
}
worker_config
};
// Create the providers
let lead_provider = create_provider(&lead_provider_name, lead_model_config)?;
let worker_provider = create_provider(default_provider_name, worker_model_config)?;
// Create the lead/worker provider with configured settings
Ok(Arc::new(LeadWorkerProvider::new_with_settings(
lead_provider,
worker_provider,
@@ -148,30 +144,27 @@ fn create_lead_worker_from_env(
)))
}
fn create_provider(name: &str, model: ModelConfig) -> Result<Arc<dyn Provider>> {
// We use Arc instead of Box to be able to clone for multiple async tasks
match name {
"anthropic" => Ok(Arc::new(AnthropicProvider::from_env(model)?)),
"aws_bedrock" => Ok(Arc::new(BedrockProvider::from_env(model)?)),
"azure_openai" => Ok(Arc::new(AzureProvider::from_env(model)?)),
"claude-code" => Ok(Arc::new(ClaudeCodeProvider::from_env(model)?)),
"cursor-agent" => Ok(Arc::new(CursorAgentProvider::from_env(model)?)),
"databricks" => Ok(Arc::new(DatabricksProvider::from_env(model)?)),
"gcp_vertex_ai" => Ok(Arc::new(GcpVertexAIProvider::from_env(model)?)),
"gemini-cli" => Ok(Arc::new(GeminiCliProvider::from_env(model)?)),
// "github_copilot" => Ok(Arc::new(GithubCopilotProvider::from_env(model)?)),
"google" => Ok(Arc::new(GoogleProvider::from_env(model)?)),
"groq" => Ok(Arc::new(GroqProvider::from_env(model)?)),
"litellm" => Ok(Arc::new(LiteLLMProvider::from_env(model)?)),
"ollama" => Ok(Arc::new(OllamaProvider::from_env(model)?)),
"openai" => Ok(Arc::new(OpenAiProvider::from_env(model)?)),
"openrouter" => Ok(Arc::new(OpenRouterProvider::from_env(model)?)),
"sagemaker_tgi" => Ok(Arc::new(SageMakerTgiProvider::from_env(model)?)),
"snowflake" => Ok(Arc::new(SnowflakeProvider::from_env(model)?)),
"venice" => Ok(Arc::new(VeniceProvider::from_env(model)?)),
"xai" => Ok(Arc::new(XaiProvider::from_env(model)?)),
_ => Err(anyhow::anyhow!("Unknown provider: {}", name)),
fn create_worker_model_config(default_model: &ModelConfig) -> Result<ModelConfig> {
let mut worker_config = ModelConfig::new_or_fail(&default_model.model_name)
.with_context_limit(default_model.context_limit)
.with_temperature(default_model.temperature)
.with_max_tokens(default_model.max_tokens)
.with_toolshim(default_model.toolshim)
.with_toolshim_model(default_model.toolshim_model.clone());
let global_config = crate::config::Config::global();
if let Ok(limit_str) = global_config.get_param::<String>("GOOSE_WORKER_CONTEXT_LIMIT") {
if let Ok(limit) = limit_str.parse::<usize>() {
worker_config = worker_config.with_context_limit(Some(limit));
}
} else if let Ok(limit_str) = global_config.get_param::<String>("GOOSE_CONTEXT_LIMIT") {
if let Ok(limit) = limit_str.parse::<usize>() {
worker_config = worker_config.with_context_limit(Some(limit));
}
}
Ok(worker_config)
}
#[cfg(test)]
@@ -183,7 +176,6 @@ mod tests {
use rmcp::model::{AnnotateAble, RawTextContent, Role};
use std::env;
#[allow(dead_code)]
#[derive(Clone)]
struct MockTestProvider {
name: String,
@@ -233,222 +225,141 @@ mod tests {
}
}
struct EnvVarGuard {
vars: Vec<(String, Option<String>)>,
}
impl EnvVarGuard {
fn new(vars: &[&str]) -> Self {
let saved_vars = vars
.iter()
.map(|&var| (var.to_string(), env::var(var).ok()))
.collect();
for &var in vars {
env::remove_var(var);
}
Self { vars: saved_vars }
}
fn set(&self, key: &str, value: &str) {
env::set_var(key, value);
}
}
impl Drop for EnvVarGuard {
fn drop(&mut self) {
for (key, value) in &self.vars {
match value {
Some(val) => env::set_var(key, val),
None => env::remove_var(key),
}
}
}
}
#[test]
fn test_create_lead_worker_provider() {
// Save current env vars
let saved_lead = env::var("GOOSE_LEAD_MODEL").ok();
let saved_provider = env::var("GOOSE_LEAD_PROVIDER").ok();
let saved_turns = env::var("GOOSE_LEAD_TURNS").ok();
let _guard = EnvVarGuard::new(&[
"GOOSE_LEAD_MODEL",
"GOOSE_LEAD_PROVIDER",
"GOOSE_LEAD_TURNS",
]);
// Test with basic lead model configuration
env::set_var("GOOSE_LEAD_MODEL", "gpt-4o");
_guard.set("GOOSE_LEAD_MODEL", "gpt-4o");
// This will try to create a lead/worker provider
let gpt4mini_config = ModelConfig::new_or_fail("gpt-4o-mini");
let result = create("openai", gpt4mini_config.clone());
// The creation might succeed or fail depending on API keys, but we can verify the logic path
match result {
Ok(_) => {
// If it succeeds, it means we created a lead/worker provider successfully
// This would happen if API keys are available in the test environment
}
Ok(_) => {}
Err(error) => {
// If it fails, it should be due to missing API keys, confirming we tried to create providers
let error_msg = error.to_string();
assert!(error_msg.contains("OPENAI_API_KEY") || error_msg.contains("secret"));
}
}
// Test with different lead provider
env::set_var("GOOSE_LEAD_PROVIDER", "anthropic");
env::set_var("GOOSE_LEAD_TURNS", "5");
_guard.set("GOOSE_LEAD_PROVIDER", "anthropic");
_guard.set("GOOSE_LEAD_TURNS", "5");
let _result = create("openai", gpt4mini_config);
// Similar validation as above - will fail due to missing API keys but confirms the logic
// Restore env vars
match saved_lead {
Some(val) => env::set_var("GOOSE_LEAD_MODEL", val),
None => env::remove_var("GOOSE_LEAD_MODEL"),
}
match saved_provider {
Some(val) => env::set_var("GOOSE_LEAD_PROVIDER", val),
None => env::remove_var("GOOSE_LEAD_PROVIDER"),
}
match saved_turns {
Some(val) => env::set_var("GOOSE_LEAD_TURNS", val),
None => env::remove_var("GOOSE_LEAD_TURNS"),
}
}
#[test]
fn test_lead_model_env_vars_with_defaults() {
// Save current env vars
let saved_vars = [
("GOOSE_LEAD_MODEL", env::var("GOOSE_LEAD_MODEL").ok()),
("GOOSE_LEAD_PROVIDER", env::var("GOOSE_LEAD_PROVIDER").ok()),
("GOOSE_LEAD_TURNS", env::var("GOOSE_LEAD_TURNS").ok()),
(
"GOOSE_LEAD_FAILURE_THRESHOLD",
env::var("GOOSE_LEAD_FAILURE_THRESHOLD").ok(),
),
(
"GOOSE_LEAD_FALLBACK_TURNS",
env::var("GOOSE_LEAD_FALLBACK_TURNS").ok(),
),
];
let _guard = EnvVarGuard::new(&[
"GOOSE_LEAD_MODEL",
"GOOSE_LEAD_PROVIDER",
"GOOSE_LEAD_TURNS",
"GOOSE_LEAD_FAILURE_THRESHOLD",
"GOOSE_LEAD_FALLBACK_TURNS",
]);
// Clear all lead env vars
for (key, _) in &saved_vars {
env::remove_var(key);
}
_guard.set("GOOSE_LEAD_MODEL", "grok-3");
// Set only the required lead model
env::set_var("GOOSE_LEAD_MODEL", "grok-3");
// This should use defaults for all other values
let result = create("openai", ModelConfig::new_or_fail("gpt-4o-mini"));
// Should attempt to create lead/worker provider (will fail due to missing API keys but confirms logic)
match result {
Ok(_) => {
// Success means we have API keys and created the provider
}
Ok(_) => {}
Err(error) => {
// Should fail due to missing API keys, confirming we tried to create providers
let error_msg = error.to_string();
assert!(error_msg.contains("OPENAI_API_KEY") || error_msg.contains("secret"));
}
}
// Test with custom values
env::set_var("GOOSE_LEAD_TURNS", "7");
env::set_var("GOOSE_LEAD_FAILURE_THRESHOLD", "4");
env::set_var("GOOSE_LEAD_FALLBACK_TURNS", "3");
_guard.set("GOOSE_LEAD_TURNS", "7");
_guard.set("GOOSE_LEAD_FAILURE_THRESHOLD", "4");
_guard.set("GOOSE_LEAD_FALLBACK_TURNS", "3");
let _result = create("openai", ModelConfig::new_or_fail("gpt-4o-mini"));
// Should still attempt to create lead/worker provider with custom settings
// Restore all env vars
for (key, value) in saved_vars {
match value {
Some(val) => env::set_var(key, val),
None => env::remove_var(key),
}
}
}
#[test]
fn test_create_regular_provider_without_lead_config() {
// Save current env vars
let saved_lead = env::var("GOOSE_LEAD_MODEL").ok();
let saved_provider = env::var("GOOSE_LEAD_PROVIDER").ok();
let saved_turns = env::var("GOOSE_LEAD_TURNS").ok();
let saved_threshold = env::var("GOOSE_LEAD_FAILURE_THRESHOLD").ok();
let saved_fallback = env::var("GOOSE_LEAD_FALLBACK_TURNS").ok();
let _guard = EnvVarGuard::new(&[
"GOOSE_LEAD_MODEL",
"GOOSE_LEAD_PROVIDER",
"GOOSE_LEAD_TURNS",
"GOOSE_LEAD_FAILURE_THRESHOLD",
"GOOSE_LEAD_FALLBACK_TURNS",
]);
// Ensure all GOOSE_LEAD_* variables are not set
env::remove_var("GOOSE_LEAD_MODEL");
env::remove_var("GOOSE_LEAD_PROVIDER");
env::remove_var("GOOSE_LEAD_TURNS");
env::remove_var("GOOSE_LEAD_FAILURE_THRESHOLD");
env::remove_var("GOOSE_LEAD_FALLBACK_TURNS");
// This should try to create a regular provider
let result = create("openai", ModelConfig::new_or_fail("gpt-4o-mini"));
// The creation might succeed or fail depending on API keys
match result {
Ok(_) => {
// If it succeeds, it means we created a regular provider successfully
// This would happen if API keys are available in the test environment
}
Ok(_) => {}
Err(error) => {
// If it fails, it should be due to missing API keys
let error_msg = error.to_string();
assert!(error_msg.contains("OPENAI_API_KEY") || error_msg.contains("secret"));
}
}
if let Some(val) = saved_lead {
env::set_var("GOOSE_LEAD_MODEL", val);
}
if let Some(val) = saved_provider {
env::set_var("GOOSE_LEAD_PROVIDER", val);
}
if let Some(val) = saved_turns {
env::set_var("GOOSE_LEAD_TURNS", val);
}
if let Some(val) = saved_threshold {
env::set_var("GOOSE_LEAD_FAILURE_THRESHOLD", val);
}
if let Some(val) = saved_fallback {
env::set_var("GOOSE_LEAD_FALLBACK_TURNS", val);
}
}
#[test]
fn test_worker_model_preserves_original_context_limit() {
use std::env;
let _guard = EnvVarGuard::new(&[
"GOOSE_LEAD_MODEL",
"GOOSE_WORKER_CONTEXT_LIMIT",
"GOOSE_CONTEXT_LIMIT",
]);
// Save current env vars
let saved_vars = [
("GOOSE_LEAD_MODEL", env::var("GOOSE_LEAD_MODEL").ok()),
(
"GOOSE_WORKER_CONTEXT_LIMIT",
env::var("GOOSE_WORKER_CONTEXT_LIMIT").ok(),
),
("GOOSE_CONTEXT_LIMIT", env::var("GOOSE_CONTEXT_LIMIT").ok()),
];
_guard.set("GOOSE_LEAD_MODEL", "gpt-4o");
// Clear env vars to ensure clean test
for (key, _) in &saved_vars {
env::remove_var(key);
}
// Set up lead model to trigger lead/worker mode
env::set_var("GOOSE_LEAD_MODEL", "gpt-4o");
// Create a default model with explicit context_limit
let default_model =
ModelConfig::new_or_fail("gpt-3.5-turbo").with_context_limit(Some(16_000));
// Test case 1: No environment variables - should preserve original context_limit
let result = create_lead_worker_from_env("openai", &default_model, "gpt-4o");
// Test case 2: With GOOSE_WORKER_CONTEXT_LIMIT - should override original
env::set_var("GOOSE_WORKER_CONTEXT_LIMIT", "32000");
_guard.set("GOOSE_WORKER_CONTEXT_LIMIT", "32000");
let _result = create_lead_worker_from_env("openai", &default_model, "gpt-4o");
env::remove_var("GOOSE_WORKER_CONTEXT_LIMIT");
// Test case 3: With GOOSE_CONTEXT_LIMIT - should override original
env::set_var("GOOSE_CONTEXT_LIMIT", "64000");
_guard.set("GOOSE_CONTEXT_LIMIT", "64000");
let _result = create_lead_worker_from_env("openai", &default_model, "gpt-4o");
env::remove_var("GOOSE_CONTEXT_LIMIT");
// Restore env vars
for (key, value) in saved_vars {
match value {
Some(val) => env::set_var(key, val),
None => env::remove_var(key),
}
}
// The main verification is that the function doesn't panic and handles
// the context limit preservation logic correctly. More detailed testing
// would require mocking the provider creation.
// The result could be Ok or Err depending on whether API keys are available
// in the test environment - both are acceptable for this test
match result {
Ok(_) => {
// Success means API keys are available and lead/worker provider was created
// This confirms our logic path is working
}
Err(_) => {
// Error is expected if API keys are not available
// This also confirms our logic path is working
}
Ok(_) => {}
Err(_) => {}
}
}
}
+2 -1
View File
@@ -24,6 +24,7 @@ pub mod ollama;
pub mod openai;
pub mod openrouter;
pub mod pricing;
pub mod provider_registry;
mod retry;
pub mod sagemaker_tgi;
pub mod snowflake;
@@ -35,4 +36,4 @@ pub mod utils_universal_openai_stream;
pub mod venice;
pub mod xai;
pub use factory::{create, providers};
pub use factory::{create, providers, refresh_custom_providers};
+47 -1
View File
@@ -3,6 +3,7 @@ use super::base::{ConfigKey, Provider, ProviderMetadata, ProviderUsage, Usage};
use super::errors::ProviderError;
use super::retry::ProviderRetry;
use super::utils::{get_model, handle_response_openai_compat};
use crate::config::custom_providers::CustomProviderConfig;
use crate::conversation::message::Message;
use crate::conversation::Conversation;
use crate::impl_provider_default;
@@ -30,6 +31,7 @@ pub struct OllamaProvider {
#[serde(skip)]
api_client: ApiClient,
model: ModelConfig,
supports_streaming: bool,
}
impl_provider_default!(OllamaProvider);
@@ -73,7 +75,47 @@ impl OllamaProvider {
let auth = AuthMethod::Custom(Box::new(NoAuth));
let api_client = ApiClient::with_timeout(base_url.to_string(), auth, timeout)?;
Ok(Self { api_client, model })
Ok(Self {
api_client,
model,
supports_streaming: true,
})
}
pub fn from_custom_config(model: ModelConfig, config: CustomProviderConfig) -> Result<Self> {
let timeout = Duration::from_secs(config.timeout_seconds.unwrap_or(OLLAMA_TIMEOUT));
// Parse and normalize the custom URL
let base =
if config.base_url.starts_with("http://") || config.base_url.starts_with("https://") {
config.base_url.clone()
} else {
format!("http://{}", config.base_url)
};
let mut base_url = Url::parse(&base)
.map_err(|e| anyhow::anyhow!("Invalid base URL '{}': {}", config.base_url, e))?;
// Set default port if missing and not using standard ports
let explicit_default_port =
config.base_url.ends_with(":80") || config.base_url.ends_with(":443");
let is_https = base_url.scheme() == "https";
if base_url.port().is_none() && !explicit_default_port && !is_https {
base_url
.set_port(Some(OLLAMA_DEFAULT_PORT))
.map_err(|_| anyhow::anyhow!("Failed to set default port"))?;
}
// No authentication for Ollama
let auth = AuthMethod::Custom(Box::new(NoAuth));
let api_client = ApiClient::with_timeout(base_url.to_string(), auth, timeout)?;
Ok(Self {
api_client,
model,
supports_streaming: config.supports_streaming.unwrap_or(true),
})
}
async fn post(&self, payload: &Value) -> Result<Value, ProviderError> {
@@ -181,6 +223,10 @@ impl Provider for OllamaProvider {
Ok(safe_truncate(&description, 100))
}
fn supports_streaming(&self) -> bool {
self.supports_streaming
}
}
impl OllamaProvider {
+48 -1
View File
@@ -20,6 +20,7 @@ use super::utils::{
emit_debug_trace, get_model, handle_response_openai_compat, handle_status_openai_compat,
ImageFormat,
};
use crate::config::custom_providers::CustomProviderConfig;
use crate::conversation::message::Message;
use crate::impl_provider_default;
use crate::model::ModelConfig;
@@ -51,6 +52,7 @@ pub struct OpenAiProvider {
project: Option<String>,
model: ModelConfig,
custom_headers: Option<HashMap<String, String>>,
supports_streaming: bool,
}
impl_provider_default!(OpenAiProvider);
@@ -103,6 +105,51 @@ impl OpenAiProvider {
project,
model,
custom_headers,
supports_streaming: true,
})
}
pub fn from_custom_config(model: ModelConfig, config: CustomProviderConfig) -> 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 url = url::Url::parse(&config.base_url)
.map_err(|e| anyhow::anyhow!("Invalid base URL '{}': {}", config.base_url, e))?;
let host = format!("{}://{}", url.scheme(), url.host_str().unwrap_or(""));
let base_path = url.path().trim_start_matches('/').to_string();
let base_path = if base_path.is_empty() {
"v1/chat/completions".to_string()
} else {
base_path
};
let timeout_secs = config.timeout_seconds.unwrap_or(600);
let auth = AuthMethod::BearerToken(api_key);
let mut api_client =
ApiClient::with_timeout(host, auth, std::time::Duration::from_secs(timeout_secs))?;
// Add custom headers if present
if let Some(headers) = &config.headers {
let mut header_map = reqwest::header::HeaderMap::new();
for (key, value) in headers {
let header_name = reqwest::header::HeaderName::from_bytes(key.as_bytes())?;
let header_value = reqwest::header::HeaderValue::from_str(value)?;
header_map.insert(header_name, header_value);
}
api_client = api_client.with_headers(header_map)?;
}
Ok(Self {
api_client,
base_path,
organization: None,
project: None,
model,
custom_headers: config.headers,
supports_streaming: config.supports_streaming.unwrap_or(true),
})
}
@@ -206,7 +253,7 @@ impl Provider for OpenAiProvider {
}
fn supports_streaming(&self) -> bool {
true
self.supports_streaming
}
async fn stream(
@@ -0,0 +1,102 @@
use super::base::{Provider, ProviderMetadata};
use crate::model::ModelConfig;
use anyhow::Result;
use std::collections::HashMap;
use std::sync::Arc;
type ProviderConstructor = Box<dyn Fn(ModelConfig) -> Result<Arc<dyn Provider>> + Send + Sync>;
struct ProviderEntry {
metadata: ProviderMetadata,
constructor: ProviderConstructor,
}
#[derive(Default)]
pub struct ProviderRegistry {
entries: HashMap<String, ProviderEntry>,
}
impl ProviderRegistry {
pub fn new() -> Self {
Self {
entries: HashMap::new(),
}
}
pub fn register<P, F>(&mut self, constructor: F)
where
P: Provider + 'static,
F: Fn(ModelConfig) -> Result<P> + Send + Sync + 'static,
{
let metadata = P::metadata();
let name = metadata.name.clone();
self.entries.insert(
name,
ProviderEntry {
metadata,
constructor: Box::new(move |model| Ok(Arc::new(constructor(model)?))),
},
);
}
/// create provider with custom name
pub fn register_with_name<P, F>(
&mut self,
custom_name: String,
display_name: String,
description: String,
default_model: String,
known_models: Vec<super::base::ModelInfo>,
constructor: F,
) where
P: Provider + 'static,
F: Fn(ModelConfig) -> Result<P> + Send + Sync + 'static,
{
let base_metadata = P::metadata();
let custom_metadata = ProviderMetadata {
name: custom_name.clone(),
display_name,
description,
default_model,
known_models,
model_doc_link: base_metadata.model_doc_link,
config_keys: base_metadata.config_keys,
};
self.entries.insert(
custom_name,
ProviderEntry {
metadata: custom_metadata,
constructor: Box::new(move |model| Ok(Arc::new(constructor(model)?))),
},
);
}
pub fn with_providers<F>(mut self, setup: F) -> Self
where
F: FnOnce(&mut Self),
{
setup(&mut self);
self
}
pub fn create(&self, name: &str, model: ModelConfig) -> Result<Arc<dyn Provider>> {
let _available_providers: Vec<_> = self.entries.keys().collect();
let entry = self
.entries
.get(name)
.ok_or_else(|| anyhow::anyhow!("Unknown provider: {}", name))?;
(entry.constructor)(model)
}
pub fn all_metadata(&self) -> Vec<ProviderMetadata> {
self.entries.values().map(|e| e.metadata.clone()).collect()
}
pub fn remove_custom_providers(&mut self) {
self.entries.retain(|name, _| !name.starts_with("custom_"));
}
}
+106
View File
@@ -275,6 +275,78 @@
}
}
},
"/config/custom-providers": {
"post": {
"tags": [
"super::routes::config_management"
],
"operationId": "create_custom_provider",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateCustomProviderRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Custom provider created successfully",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
}
}
},
"400": {
"description": "Invalid request"
},
"500": {
"description": "Internal server error"
}
}
}
},
"/config/custom-providers/{id}": {
"delete": {
"tags": [
"super::routes::config_management"
],
"operationId": "remove_custom_provider",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Custom provider removed successfully",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
}
}
},
"404": {
"description": "Provider not found"
},
"500": {
"description": "Internal server error"
}
}
}
},
"/config/extensions": {
"get": {
"tags": [
@@ -1469,6 +1541,40 @@
}
}
},
"CreateCustomProviderRequest": {
"type": "object",
"required": [
"provider_type",
"display_name",
"api_url",
"api_key",
"models"
],
"properties": {
"api_key": {
"type": "string"
},
"api_url": {
"type": "string"
},
"display_name": {
"type": "string"
},
"models": {
"type": "array",
"items": {
"type": "string"
}
},
"provider_type": {
"type": "string"
},
"supports_streaming": {
"type": "boolean",
"nullable": true
}
}
},
"CreateRecipeRequest": {
"type": "object",
"required": [
+217 -216
View File
@@ -119,13 +119,13 @@
"license": "MIT"
},
"node_modules/@ai-sdk/gateway": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-1.0.6.tgz",
"integrity": "sha512-JuSj1MtTr4vw2VBBth4wlbciQnQIV0o1YV9qGLFA+r85nR5H+cJp3jaYE0nprqfzC9rYG8w9c6XGHB3SDKgcgA==",
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-1.0.8.tgz",
"integrity": "sha512-yiHYz0bAHEvhL+fSUBI2dNmyj0LOI8zw5qrYpa4gp1ojPgZq/7T1WXoIWRmVdjQwvT4PzSmRKLtbMPfz+umgfw==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "2.0.0",
"@ai-sdk/provider-utils": "3.0.3"
"@ai-sdk/provider-utils": "3.0.4"
},
"engines": {
"node": ">=18"
@@ -135,13 +135,13 @@
}
},
"node_modules/@ai-sdk/openai": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-2.0.14.tgz",
"integrity": "sha512-u/wi1ixcvcg29wAJySjO803HlXpyCl6mkcOHn+Zn7DA+CtjuQNkKikJ4pZBc7I3Qhi90kA4XnOfKikhBXh4c4Q==",
"version": "2.0.16",
"resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-2.0.16.tgz",
"integrity": "sha512-Boe715q4SkSJedFfAtbP0yuo8DmF9iYElAaDH2g4YgqJqqkskIJJx4hlCYGMMk1eMesRrB2NqZvtOeyTZ/u4fA==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "2.0.0",
"@ai-sdk/provider-utils": "3.0.3"
"@ai-sdk/provider-utils": "3.0.4"
},
"engines": {
"node": ">=18"
@@ -163,9 +163,9 @@
}
},
"node_modules/@ai-sdk/provider-utils": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.3.tgz",
"integrity": "sha512-kAxIw1nYmFW1g5TvE54ZB3eNtgZna0RnLjPUp1ltz1+t9xkXJIuDT4atrwfau9IbS0BOef38wqrI8CjFfQrxhw==",
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.4.tgz",
"integrity": "sha512-/3Z6lfUp8r+ewFd9yzHkCmPlMOJUXup2Sx3aoUyrdXLhOmAfHRl6Z4lDbIdV0uvw/QYoBcVLJnvXN7ncYeS3uQ==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "2.0.0",
@@ -2367,9 +2367,9 @@
}
},
"node_modules/@hey-api/openapi-ts": {
"version": "0.80.10",
"resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.80.10.tgz",
"integrity": "sha512-jFCaQJeZV9B6lbYCU8JFVkC2/FE+31FG36aT4aiOXeGvgJsIa7v3HFXQ3sdcN0uHRuLyf11GUDrVupPBPvgkuA==",
"version": "0.80.15",
"resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.80.15.tgz",
"integrity": "sha512-THERBZUJG9lKHBqY7yBWL8icuwLJc/BY0kjuuZKZST/ZZzt+Zl78nMjP1RFaKybyT8yX0YlA/liem51dSSf+9Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2379,6 +2379,7 @@
"color-support": "1.1.3",
"commander": "13.0.0",
"handlebars": "4.7.8",
"js-yaml": "4.1.0",
"open": "10.1.2",
"semver": "7.7.2"
},
@@ -3108,9 +3109,9 @@
"license": "MIT"
},
"node_modules/@preact/signals-core": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.11.0.tgz",
"integrity": "sha512-jglbibeWHuFRzEWVFY/TT7wB1PppJxmcSfUHcK+2J9vBRtiooMfw6tAPttojNYrrpdGViqAYCbPpmWYlMm+eMQ==",
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.12.0.tgz",
"integrity": "sha512-etWpENXm469RHMWIZGblgWrapbIGcRcbccEGGaLkFez3PjlI3XkBrUtSiNFsIfV/DN16PxMOxbWAZUIaLFyJDg==",
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -4741,16 +4742,16 @@
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.30",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.30.tgz",
"integrity": "sha512-whXaSoNUFiyDAjkUF8OBpOm77Szdbk5lGNqFe6CbVbJFrhCCPinCbRA3NjawwlNHla1No7xvXXh+CpSxnPfUEw==",
"version": "1.0.0-beta.32",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.32.tgz",
"integrity": "sha512-QReCdvxiUZAPkvp1xpAg62IeNzykOFA6syH2CnClif4YmALN1XKpB39XneL80008UbtMShthSVDKmrx05N1q/g==",
"dev": true,
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.46.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz",
"integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==",
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.3.tgz",
"integrity": "sha512-UmTdvXnLlqQNOCJnyksjPs1G4GqXNGW1LrzCe8+8QoaLhhDeTXYBgJ3k6x61WIhlHX2U+VzEJ55TtIjR/HTySA==",
"cpu": [
"arm"
],
@@ -4762,9 +4763,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.46.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz",
"integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==",
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.3.tgz",
"integrity": "sha512-8NoxqLpXm7VyeI0ocidh335D6OKT0UJ6fHdnIxf3+6oOerZZc+O7r+UhvROji6OspyPm+rrIdb1gTXtVIqn+Sg==",
"cpu": [
"arm64"
],
@@ -4776,9 +4777,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.46.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz",
"integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==",
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.3.tgz",
"integrity": "sha512-csnNavqZVs1+7/hUKtgjMECsNG2cdB8F7XBHP6FfQjqhjF8rzMzb3SLyy/1BG7YSfQ+bG75Ph7DyedbUqwq1rA==",
"cpu": [
"arm64"
],
@@ -4790,9 +4791,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.46.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz",
"integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==",
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.3.tgz",
"integrity": "sha512-r2MXNjbuYabSIX5yQqnT8SGSQ26XQc8fmp6UhlYJd95PZJkQD1u82fWP7HqvGUf33IsOC6qsiV+vcuD4SDP6iw==",
"cpu": [
"x64"
],
@@ -4804,9 +4805,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.46.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz",
"integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==",
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.3.tgz",
"integrity": "sha512-uluObTmgPJDuJh9xqxyr7MV61Imq+0IvVsAlWyvxAaBSNzCcmZlhfYcRhCdMaCsy46ccZa7vtDDripgs9Jkqsw==",
"cpu": [
"arm64"
],
@@ -4818,9 +4819,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.46.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz",
"integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==",
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.3.tgz",
"integrity": "sha512-AVJXEq9RVHQnejdbFvh1eWEoobohUYN3nqJIPI4mNTMpsyYN01VvcAClxflyk2HIxvLpRcRggpX1m9hkXkpC/A==",
"cpu": [
"x64"
],
@@ -4832,9 +4833,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.46.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz",
"integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==",
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.3.tgz",
"integrity": "sha512-byyflM+huiwHlKi7VHLAYTKr67X199+V+mt1iRgJenAI594vcmGGddWlu6eHujmcdl6TqSNnvqaXJqZdnEWRGA==",
"cpu": [
"arm"
],
@@ -4846,9 +4847,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.46.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz",
"integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==",
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.3.tgz",
"integrity": "sha512-aLm3NMIjr4Y9LklrH5cu7yybBqoVCdr4Nvnm8WB7PKCn34fMCGypVNpGK0JQWdPAzR/FnoEoFtlRqZbBBLhVoQ==",
"cpu": [
"arm"
],
@@ -4860,9 +4861,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.46.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz",
"integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==",
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.3.tgz",
"integrity": "sha512-VtilE6eznJRDIoFOzaagQodUksTEfLIsvXymS+UdJiSXrPW7Ai+WG4uapAc3F7Hgs791TwdGh4xyOzbuzIZrnw==",
"cpu": [
"arm64"
],
@@ -4874,9 +4875,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.46.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz",
"integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==",
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.3.tgz",
"integrity": "sha512-dG3JuS6+cRAL0GQ925Vppafi0qwZnkHdPeuZIxIPXqkCLP02l7ka+OCyBoDEv8S+nKHxfjvjW4OZ7hTdHkx8/w==",
"cpu": [
"arm64"
],
@@ -4888,9 +4889,9 @@
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.46.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz",
"integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==",
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.3.tgz",
"integrity": "sha512-iU8DxnxEKJptf8Vcx4XvAUdpkZfaz0KWfRrnIRrOndL0SvzEte+MTM7nDH4A2Now4FvTZ01yFAgj6TX/mZl8hQ==",
"cpu": [
"loong64"
],
@@ -4902,9 +4903,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.46.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz",
"integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==",
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.3.tgz",
"integrity": "sha512-VrQZp9tkk0yozJoQvQcqlWiqaPnLM6uY1qPYXvukKePb0fqaiQtOdMJSxNFUZFsGw5oA5vvVokjHrx8a9Qsz2A==",
"cpu": [
"ppc64"
],
@@ -4916,9 +4917,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.46.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz",
"integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==",
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.3.tgz",
"integrity": "sha512-uf2eucWSUb+M7b0poZ/08LsbcRgaDYL8NCGjUeFMwCWFwOuFcZ8D9ayPl25P3pl+D2FH45EbHdfyUesQ2Lt9wA==",
"cpu": [
"riscv64"
],
@@ -4930,9 +4931,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.46.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz",
"integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==",
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.3.tgz",
"integrity": "sha512-7tnUcDvN8DHm/9ra+/nF7lLzYHDeODKKKrh6JmZejbh1FnCNZS8zMkZY5J4sEipy2OW1d1Ncc4gNHUd0DLqkSg==",
"cpu": [
"riscv64"
],
@@ -4944,9 +4945,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.46.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz",
"integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==",
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.3.tgz",
"integrity": "sha512-MUpAOallJim8CsJK+4Lc9tQzlfPbHxWDrGXZm2z6biaadNpvh3a5ewcdat478W+tXDoUiHwErX/dOql7ETcLqg==",
"cpu": [
"s390x"
],
@@ -4958,9 +4959,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.46.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz",
"integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==",
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.3.tgz",
"integrity": "sha512-F42IgZI4JicE2vM2PWCe0N5mR5vR0gIdORPqhGQ32/u1S1v3kLtbZ0C/mi9FFk7C5T0PgdeyWEPajPjaUpyoKg==",
"cpu": [
"x64"
],
@@ -4972,9 +4973,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.46.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz",
"integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==",
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.3.tgz",
"integrity": "sha512-oLc+JrwwvbimJUInzx56Q3ujL3Kkhxehg7O1gWAYzm8hImCd5ld1F2Gry5YDjR21MNb5WCKhC9hXgU7rRlyegQ==",
"cpu": [
"x64"
],
@@ -4986,9 +4987,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.46.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz",
"integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==",
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.3.tgz",
"integrity": "sha512-lOrQ+BVRstruD1fkWg9yjmumhowR0oLAAzavB7yFSaGltY8klttmZtCLvOXCmGE9mLIn8IBV/IFrQOWz5xbFPg==",
"cpu": [
"arm64"
],
@@ -5000,9 +5001,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.46.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz",
"integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==",
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.3.tgz",
"integrity": "sha512-vvrVKPRS4GduGR7VMH8EylCBqsDcw6U+/0nPDuIjXQRbHJc6xOBj+frx8ksfZAh6+Fptw5wHrN7etlMmQnPQVg==",
"cpu": [
"ia32"
],
@@ -5014,9 +5015,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.46.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz",
"integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==",
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.3.tgz",
"integrity": "sha512-fi3cPxCnu3ZeM3EwKZPgXbWoGzm2XHgB/WShKI81uj8wG0+laobmqy5wbgEwzstlbLu4MyO8C19FyhhWseYKNQ==",
"cpu": [
"x64"
],
@@ -5807,9 +5808,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.2.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz",
"integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
"version": "24.3.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
"integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5951,17 +5952,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz",
"integrity": "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==",
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz",
"integrity": "sha512-w/EboPlBwnmOBtRbiOvzjD+wdiZdgFeo17lkltrtn7X37vagKKWJABvyfsJXTlHe6XBzugmYgd4A4nW+k8Mixw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.39.1",
"@typescript-eslint/type-utils": "8.39.1",
"@typescript-eslint/utils": "8.39.1",
"@typescript-eslint/visitor-keys": "8.39.1",
"@typescript-eslint/scope-manager": "8.40.0",
"@typescript-eslint/type-utils": "8.40.0",
"@typescript-eslint/utils": "8.40.0",
"@typescript-eslint/visitor-keys": "8.40.0",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
@@ -5975,22 +5976,22 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.39.1",
"@typescript-eslint/parser": "^8.40.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.1.tgz",
"integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==",
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.40.0.tgz",
"integrity": "sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.39.1",
"@typescript-eslint/types": "8.39.1",
"@typescript-eslint/typescript-estree": "8.39.1",
"@typescript-eslint/visitor-keys": "8.39.1",
"@typescript-eslint/scope-manager": "8.40.0",
"@typescript-eslint/types": "8.40.0",
"@typescript-eslint/typescript-estree": "8.40.0",
"@typescript-eslint/visitor-keys": "8.40.0",
"debug": "^4.3.4"
},
"engines": {
@@ -6006,14 +6007,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz",
"integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==",
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.40.0.tgz",
"integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.39.1",
"@typescript-eslint/types": "^8.39.1",
"@typescript-eslint/tsconfig-utils": "^8.40.0",
"@typescript-eslint/types": "^8.40.0",
"debug": "^4.3.4"
},
"engines": {
@@ -6028,14 +6029,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz",
"integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==",
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz",
"integrity": "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.39.1",
"@typescript-eslint/visitor-keys": "8.39.1"
"@typescript-eslint/types": "8.40.0",
"@typescript-eslint/visitor-keys": "8.40.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -6046,9 +6047,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz",
"integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==",
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz",
"integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -6063,15 +6064,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.1.tgz",
"integrity": "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==",
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.40.0.tgz",
"integrity": "sha512-eE60cK4KzAc6ZrzlJnflXdrMqOBaugeukWICO2rB0KNvwdIMaEaYiywwHMzA1qFpTxrLhN9Lp4E/00EgWcD3Ow==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.39.1",
"@typescript-eslint/typescript-estree": "8.39.1",
"@typescript-eslint/utils": "8.39.1",
"@typescript-eslint/types": "8.40.0",
"@typescript-eslint/typescript-estree": "8.40.0",
"@typescript-eslint/utils": "8.40.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@@ -6088,9 +6089,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz",
"integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==",
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz",
"integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -6102,16 +6103,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz",
"integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==",
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz",
"integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.39.1",
"@typescript-eslint/tsconfig-utils": "8.39.1",
"@typescript-eslint/types": "8.39.1",
"@typescript-eslint/visitor-keys": "8.39.1",
"@typescript-eslint/project-service": "8.40.0",
"@typescript-eslint/tsconfig-utils": "8.40.0",
"@typescript-eslint/types": "8.40.0",
"@typescript-eslint/visitor-keys": "8.40.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@@ -6157,16 +6158,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.1.tgz",
"integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==",
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.40.0.tgz",
"integrity": "sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.39.1",
"@typescript-eslint/types": "8.39.1",
"@typescript-eslint/typescript-estree": "8.39.1"
"@typescript-eslint/scope-manager": "8.40.0",
"@typescript-eslint/types": "8.40.0",
"@typescript-eslint/typescript-estree": "8.40.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -6181,13 +6182,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz",
"integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==",
"version": "8.40.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz",
"integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.39.1",
"@typescript-eslint/types": "8.40.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@@ -6218,16 +6219,16 @@
"license": "ISC"
},
"node_modules/@vitejs/plugin-react": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.0.tgz",
"integrity": "sha512-Jx9JfsTa05bYkS9xo0hkofp2dCmp1blrKjw9JONs5BTHOvJCgLbaPSuZLGSVJW6u2qe0tc4eevY0+gSNNi0YCw==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.1.tgz",
"integrity": "sha512-DE4UNaBXwtVoDJ0ccBdLVjFTWL70NRuWNCxEieTI3lrq9ORB9aOCQEKstwDXBl87NvFdbqh/p7eINGyj0BthJA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.28.0",
"@babel/core": "^7.28.3",
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
"@rolldown/pluginutils": "1.0.0-beta.30",
"@rolldown/pluginutils": "1.0.0-beta.32",
"@types/babel__core": "^7.20.5",
"react-refresh": "^0.17.0"
},
@@ -6410,9 +6411,9 @@
}
},
"node_modules/@xmldom/xmldom": {
"version": "0.8.10",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
"integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==",
"version": "0.8.11",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
"integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -6500,14 +6501,14 @@
}
},
"node_modules/ai": {
"version": "5.0.14",
"resolved": "https://registry.npmjs.org/ai/-/ai-5.0.14.tgz",
"integrity": "sha512-xiujFa879skB7YxGzbeHAxepsr6AEaWcHPXrc5a9MRM6p4WdVAwn6mGwVZkBnhqGfZtXFr4LUnU2ayvcjWp5ig==",
"version": "5.0.17",
"resolved": "https://registry.npmjs.org/ai/-/ai-5.0.17.tgz",
"integrity": "sha512-DLZikqZZJdwSkRhFikw6Mt7pUmPZ7Ue38TjdOcw2U6iZtBbuiyWGIhHyJXlUpLcZrtBE5yqPTozyZri1lRjduw==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/gateway": "1.0.6",
"@ai-sdk/gateway": "1.0.8",
"@ai-sdk/provider": "2.0.0",
"@ai-sdk/provider-utils": "3.0.3",
"@ai-sdk/provider-utils": "3.0.4",
"@opentelemetry/api": "1.9.0"
},
"engines": {
@@ -6990,9 +6991,9 @@
}
},
"node_modules/browserslist": {
"version": "4.25.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz",
"integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==",
"version": "4.25.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.3.tgz",
"integrity": "sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==",
"dev": true,
"funding": [
{
@@ -7010,8 +7011,8 @@
],
"license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001733",
"electron-to-chromium": "^1.5.199",
"caniuse-lite": "^1.0.30001735",
"electron-to-chromium": "^1.5.204",
"node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.3"
},
@@ -7376,9 +7377,9 @@
}
},
"node_modules/chai": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz",
"integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==",
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/chai/-/chai-5.3.1.tgz",
"integrity": "sha512-48af6xm9gQK8rhIcOxWwdGzIervm8BVTin+yRp9HEvU20BtVZ2lBywlIJBzwaDtvo0FvjeL7QdCADoUoqIbV3A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8869,9 +8870,9 @@
}
},
"node_modules/electron-log": {
"version": "5.4.2",
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.4.2.tgz",
"integrity": "sha512-L55kJzbVVoBY6kyJ3A+cjPchL1aXLdV2/Q8SvIj4sE5VmOcwZa6KXaFICZE+Z+RMXhAIFta79kSaXBqn+0XXMA==",
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.4.3.tgz",
"integrity": "sha512-sOUsM3LjZdugatazSQ/XTyNcw8dfvH1SYhXWiJyfYodAAKOZdHs0txPiLDXFzOZbhXgAgshQkshH2ccq0feyLQ==",
"license": "MIT",
"engines": {
"node": ">= 14"
@@ -8902,9 +8903,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.201",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.201.tgz",
"integrity": "sha512-ZG65vsrLClodGqywuigc+7m0gr4ISoTQttfVh7nfpLv0M7SIwF4WbFNEOywcqTiujs12AUeeXbFyQieDICAIxg==",
"version": "1.5.207",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.207.tgz",
"integrity": "sha512-mryFrrL/GXDTmAtIVMVf+eIXM09BBPlO5IQ7lUyKmK8d+A4VpRGG+M3ofoVef6qyF8s60rJei8ymlJxjUA8Faw==",
"dev": true,
"license": "ISC"
},
@@ -9009,9 +9010,9 @@
}
},
"node_modules/electron/node_modules/@types/node": {
"version": "22.17.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.1.tgz",
"integrity": "sha512-y3tBaz+rjspDTylNjAX37jEC3TETEFGNJL6uQDxwF9/8GLLIjW1rvVHlynyuUKMnMr1Roq8jOv3vkopBjC4/VA==",
"version": "22.17.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.2.tgz",
"integrity": "sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -9736,9 +9737,9 @@
}
},
"node_modules/eventsource-parser": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.3.tgz",
"integrity": "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==",
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.5.tgz",
"integrity": "sha512-bSRG85ZrMdmWtm7qkF9He9TNRzc/Bm99gEJMaQoHJ9E6Kv9QBbsldh2oMj7iXmYNEAVvNgvv5vPorG6W+XtBhQ==",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
@@ -11962,9 +11963,9 @@
}
},
"node_modules/istanbul-reports": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz",
"integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -12583,9 +12584,9 @@
}
},
"node_modules/lint-staged/node_modules/chalk": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.5.0.tgz",
"integrity": "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==",
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz",
"integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -15916,9 +15917,9 @@
}
},
"node_modules/react-router": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.0.tgz",
"integrity": "sha512-r15M3+LHKgM4SOapNmsH3smAizWds1vJ0Z9C4mWaKnT9/wD7+d/0jYcj6LmOvonkrO4Rgdyp4KQ/29gWN2i1eg==",
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.1.tgz",
"integrity": "sha512-5cy/M8DHcG51/KUIka1nfZ2QeylS4PJRs6TT8I4PF5axVsI5JUxp0hC0NZ/AEEj8Vw7xsEoD7L/6FY+zoYaOGA==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
@@ -15938,12 +15939,12 @@
}
},
"node_modules/react-router-dom": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.8.0.tgz",
"integrity": "sha512-ntInsnDVnVRdtSu6ODmTQ41cbluak/ENeTif7GBce0L6eztFg6/e1hXAysFQI8X25C8ipKmT9cClbJwxx3Kaqw==",
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.8.1.tgz",
"integrity": "sha512-NkgBCF3sVgCiAWIlSt89GR2PLaksMpoo3HDCorpRfnCEfdtRPLiuTf+CNXvqZMI5SJLZCLpVCvcZrTdtGW64xQ==",
"license": "MIT",
"dependencies": {
"react-router": "7.8.0"
"react-router": "7.8.1"
},
"engines": {
"node": ">=20.0.0"
@@ -16610,9 +16611,9 @@
}
},
"node_modules/rollup": {
"version": "4.46.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz",
"integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==",
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.3.tgz",
"integrity": "sha512-RZn2XTjXb8t5g13f5YclGoilU/kwT696DIkY3sywjdZidNSi3+vseaQov7D7BZXVJCPv3pDWUN69C78GGbXsKw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -16626,26 +16627,26 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.46.2",
"@rollup/rollup-android-arm64": "4.46.2",
"@rollup/rollup-darwin-arm64": "4.46.2",
"@rollup/rollup-darwin-x64": "4.46.2",
"@rollup/rollup-freebsd-arm64": "4.46.2",
"@rollup/rollup-freebsd-x64": "4.46.2",
"@rollup/rollup-linux-arm-gnueabihf": "4.46.2",
"@rollup/rollup-linux-arm-musleabihf": "4.46.2",
"@rollup/rollup-linux-arm64-gnu": "4.46.2",
"@rollup/rollup-linux-arm64-musl": "4.46.2",
"@rollup/rollup-linux-loongarch64-gnu": "4.46.2",
"@rollup/rollup-linux-ppc64-gnu": "4.46.2",
"@rollup/rollup-linux-riscv64-gnu": "4.46.2",
"@rollup/rollup-linux-riscv64-musl": "4.46.2",
"@rollup/rollup-linux-s390x-gnu": "4.46.2",
"@rollup/rollup-linux-x64-gnu": "4.46.2",
"@rollup/rollup-linux-x64-musl": "4.46.2",
"@rollup/rollup-win32-arm64-msvc": "4.46.2",
"@rollup/rollup-win32-ia32-msvc": "4.46.2",
"@rollup/rollup-win32-x64-msvc": "4.46.2",
"@rollup/rollup-android-arm-eabi": "4.46.3",
"@rollup/rollup-android-arm64": "4.46.3",
"@rollup/rollup-darwin-arm64": "4.46.3",
"@rollup/rollup-darwin-x64": "4.46.3",
"@rollup/rollup-freebsd-arm64": "4.46.3",
"@rollup/rollup-freebsd-x64": "4.46.3",
"@rollup/rollup-linux-arm-gnueabihf": "4.46.3",
"@rollup/rollup-linux-arm-musleabihf": "4.46.3",
"@rollup/rollup-linux-arm64-gnu": "4.46.3",
"@rollup/rollup-linux-arm64-musl": "4.46.3",
"@rollup/rollup-linux-loongarch64-gnu": "4.46.3",
"@rollup/rollup-linux-ppc64-gnu": "4.46.3",
"@rollup/rollup-linux-riscv64-gnu": "4.46.3",
"@rollup/rollup-linux-riscv64-musl": "4.46.3",
"@rollup/rollup-linux-s390x-gnu": "4.46.3",
"@rollup/rollup-linux-x64-gnu": "4.46.3",
"@rollup/rollup-linux-x64-musl": "4.46.3",
"@rollup/rollup-win32-arm64-msvc": "4.46.3",
"@rollup/rollup-win32-ia32-msvc": "4.46.3",
"@rollup/rollup-win32-x64-msvc": "4.46.3",
"fsevents": "~2.3.2"
}
},
@@ -17557,9 +17558,9 @@
}
},
"node_modules/strip-ansi/node_modules/ansi-regex": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz",
"integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -18171,9 +18172,9 @@
"license": "0BSD"
},
"node_modules/tw-animate-css": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.6.tgz",
"integrity": "sha512-9dy0R9UsYEGmgf26L8UcHiLmSFTHa9+D7+dAt/G/sF5dCnPePZbfgDYinc7/UzAM7g/baVrmS6m9yEpU46d+LA==",
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.7.tgz",
"integrity": "sha512-lvLb3hTIpB5oGsk8JmLoAjeCHV58nKa2zHYn8yWOoG5JJusH3bhJlF2DLAZ/5NmJ+jyH3ssiAx/2KmbhavJy/A==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/Wombosvideo"
@@ -18678,14 +18679,14 @@
}
},
"node_modules/vite": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.2.tgz",
"integrity": "sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==",
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz",
"integrity": "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.6",
"fdir": "^6.5.0",
"picomatch": "^4.0.3",
"postcss": "^8.5.6",
"rollup": "^4.43.0",
+23 -2
View File
@@ -188,6 +188,25 @@ export const getParseAs = (
return;
};
const checkForExistence = (
options: Pick<RequestOptions, 'auth' | 'query'> & {
headers: Headers;
},
name?: string,
): boolean => {
if (!name) {
return false;
}
if (
options.headers.has(name) ||
options.query?.[name] ||
options.headers.get('Cookie')?.includes(`${name}=`)
) {
return true;
}
return false;
};
export const setAuthParams = async ({
security,
...options
@@ -196,6 +215,10 @@ export const setAuthParams = async ({
headers: Headers;
}) => {
for (const auth of security) {
if (checkForExistence(options, auth.name)) {
continue;
}
const token = await getAuthToken(auth, options.auth);
if (!token) {
@@ -219,8 +242,6 @@ export const setAuthParams = async ({
options.headers.set(name, token);
break;
}
return;
}
};
+19 -1
View File
@@ -1,7 +1,7 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { Options as ClientOptions, TDataShape, Client } from './client';
import type { AddSubRecipesData, AddSubRecipesResponses, AddSubRecipesErrors, ExtendPromptData, ExtendPromptResponses, ExtendPromptErrors, UpdateSessionConfigData, UpdateSessionConfigResponses, UpdateSessionConfigErrors, GetToolsData, GetToolsResponses, GetToolsErrors, UpdateAgentProviderData, UpdateAgentProviderResponses, UpdateAgentProviderErrors, UpdateRouterToolSelectorData, UpdateRouterToolSelectorResponses, UpdateRouterToolSelectorErrors, ReadAllConfigData, ReadAllConfigResponses, BackupConfigData, BackupConfigResponses, BackupConfigErrors, GetExtensionsData, GetExtensionsResponses, GetExtensionsErrors, AddExtensionData, AddExtensionResponses, AddExtensionErrors, RemoveExtensionData, RemoveExtensionResponses, RemoveExtensionErrors, InitConfigData, InitConfigResponses, InitConfigErrors, UpsertPermissionsData, UpsertPermissionsResponses, UpsertPermissionsErrors, ProvidersData, ProvidersResponses, ReadConfigData, ReadConfigResponses, ReadConfigErrors, RecoverConfigData, RecoverConfigResponses, RecoverConfigErrors, RemoveConfigData, RemoveConfigResponses, RemoveConfigErrors, UpsertConfigData, UpsertConfigResponses, UpsertConfigErrors, ValidateConfigData, ValidateConfigResponses, ValidateConfigErrors, ConfirmPermissionData, ConfirmPermissionResponses, ConfirmPermissionErrors, ManageContextData, ManageContextResponses, ManageContextErrors, CreateRecipeData, CreateRecipeResponses, CreateRecipeErrors, DecodeRecipeData, DecodeRecipeResponses, DecodeRecipeErrors, EncodeRecipeData, EncodeRecipeResponses, EncodeRecipeErrors, ScanRecipeData, ScanRecipeResponses, CreateScheduleData, CreateScheduleResponses, CreateScheduleErrors, DeleteScheduleData, DeleteScheduleResponses, DeleteScheduleErrors, ListSchedulesData, ListSchedulesResponses, ListSchedulesErrors, UpdateScheduleData, UpdateScheduleResponses, UpdateScheduleErrors, InspectRunningJobData, InspectRunningJobResponses, InspectRunningJobErrors, KillRunningJobData, KillRunningJobResponses, PauseScheduleData, PauseScheduleResponses, PauseScheduleErrors, RunNowHandlerData, RunNowHandlerResponses, RunNowHandlerErrors, SessionsHandlerData, SessionsHandlerResponses, SessionsHandlerErrors, UnpauseScheduleData, UnpauseScheduleResponses, UnpauseScheduleErrors, ListSessionsData, ListSessionsResponses, ListSessionsErrors, GetSessionHistoryData, GetSessionHistoryResponses, GetSessionHistoryErrors } from './types.gen';
import type { AddSubRecipesData, AddSubRecipesResponses, AddSubRecipesErrors, ExtendPromptData, ExtendPromptResponses, ExtendPromptErrors, UpdateSessionConfigData, UpdateSessionConfigResponses, UpdateSessionConfigErrors, GetToolsData, GetToolsResponses, GetToolsErrors, UpdateAgentProviderData, UpdateAgentProviderResponses, UpdateAgentProviderErrors, UpdateRouterToolSelectorData, UpdateRouterToolSelectorResponses, UpdateRouterToolSelectorErrors, ReadAllConfigData, ReadAllConfigResponses, BackupConfigData, BackupConfigResponses, BackupConfigErrors, CreateCustomProviderData, CreateCustomProviderResponses, CreateCustomProviderErrors, RemoveCustomProviderData, RemoveCustomProviderResponses, RemoveCustomProviderErrors, GetExtensionsData, GetExtensionsResponses, GetExtensionsErrors, AddExtensionData, AddExtensionResponses, AddExtensionErrors, RemoveExtensionData, RemoveExtensionResponses, RemoveExtensionErrors, InitConfigData, InitConfigResponses, InitConfigErrors, UpsertPermissionsData, UpsertPermissionsResponses, UpsertPermissionsErrors, ProvidersData, ProvidersResponses, ReadConfigData, ReadConfigResponses, ReadConfigErrors, RecoverConfigData, RecoverConfigResponses, RecoverConfigErrors, RemoveConfigData, RemoveConfigResponses, RemoveConfigErrors, UpsertConfigData, UpsertConfigResponses, UpsertConfigErrors, ValidateConfigData, ValidateConfigResponses, ValidateConfigErrors, ConfirmPermissionData, ConfirmPermissionResponses, ConfirmPermissionErrors, ManageContextData, ManageContextResponses, ManageContextErrors, CreateRecipeData, CreateRecipeResponses, CreateRecipeErrors, DecodeRecipeData, DecodeRecipeResponses, DecodeRecipeErrors, EncodeRecipeData, EncodeRecipeResponses, EncodeRecipeErrors, ScanRecipeData, ScanRecipeResponses, CreateScheduleData, CreateScheduleResponses, CreateScheduleErrors, DeleteScheduleData, DeleteScheduleResponses, DeleteScheduleErrors, ListSchedulesData, ListSchedulesResponses, ListSchedulesErrors, UpdateScheduleData, UpdateScheduleResponses, UpdateScheduleErrors, InspectRunningJobData, InspectRunningJobResponses, InspectRunningJobErrors, KillRunningJobData, KillRunningJobResponses, PauseScheduleData, PauseScheduleResponses, PauseScheduleErrors, RunNowHandlerData, RunNowHandlerResponses, RunNowHandlerErrors, SessionsHandlerData, SessionsHandlerResponses, SessionsHandlerErrors, UnpauseScheduleData, UnpauseScheduleResponses, UnpauseScheduleErrors, ListSessionsData, ListSessionsResponses, ListSessionsErrors, GetSessionHistoryData, GetSessionHistoryResponses, GetSessionHistoryErrors } from './types.gen';
import { client as _heyApiClient } from './client.gen';
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<TData, ThrowOnError> & {
@@ -90,6 +90,24 @@ export const backupConfig = <ThrowOnError extends boolean = false>(options?: Opt
});
};
export const createCustomProvider = <ThrowOnError extends boolean = false>(options: Options<CreateCustomProviderData, ThrowOnError>) => {
return (options.client ?? _heyApiClient).post<CreateCustomProviderResponses, CreateCustomProviderErrors, ThrowOnError>({
url: '/config/custom-providers',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
};
export const removeCustomProvider = <ThrowOnError extends boolean = false>(options: Options<RemoveCustomProviderData, ThrowOnError>) => {
return (options.client ?? _heyApiClient).delete<RemoveCustomProviderResponses, RemoveCustomProviderErrors, ThrowOnError>({
url: '/config/custom-providers/{id}',
...options
});
};
export const getExtensions = <ThrowOnError extends boolean = false>(options?: Options<GetExtensionsData, ThrowOnError>) => {
return (options?.client ?? _heyApiClient).get<GetExtensionsResponses, GetExtensionsErrors, ThrowOnError>({
url: '/config/extensions',
+65
View File
@@ -98,6 +98,15 @@ export type ContextManageResponse = {
tokenCounts: Array<number>;
};
export type CreateCustomProviderRequest = {
api_key: string;
api_url: string;
display_name: string;
models: Array<string>;
provider_type: string;
supports_streaming?: boolean | null;
};
export type CreateRecipeRequest = {
activities?: Array<string> | null;
author?: AuthorRequest | null;
@@ -1047,6 +1056,62 @@ export type BackupConfigResponses = {
export type BackupConfigResponse = BackupConfigResponses[keyof BackupConfigResponses];
export type CreateCustomProviderData = {
body: CreateCustomProviderRequest;
path?: never;
query?: never;
url: '/config/custom-providers';
};
export type CreateCustomProviderErrors = {
/**
* Invalid request
*/
400: unknown;
/**
* Internal server error
*/
500: unknown;
};
export type CreateCustomProviderResponses = {
/**
* Custom provider created successfully
*/
200: string;
};
export type CreateCustomProviderResponse = CreateCustomProviderResponses[keyof CreateCustomProviderResponses];
export type RemoveCustomProviderData = {
body?: never;
path: {
id: string;
};
query?: never;
url: '/config/custom-providers/{id}';
};
export type RemoveCustomProviderErrors = {
/**
* Provider not found
*/
404: unknown;
/**
* Internal server error
*/
500: unknown;
};
export type RemoveCustomProviderResponses = {
/**
* Custom provider removed successfully
*/
200: string;
};
export type RemoveCustomProviderResponse = RemoveCustomProviderResponses[keyof RemoveCustomProviderResponses];
export type GetExtensionsData = {
body?: never;
path?: never;
+12 -4
View File
@@ -170,9 +170,15 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
const getProviders = useCallback(
async (forceRefresh = false): Promise<ProviderDetails[]> => {
if (forceRefresh || providersList.length === 0) {
const response = await providers();
setProvidersList(response.data || []);
return response.data || [];
try {
const response = await providers();
const providersData = response.data || [];
setProvidersList(providersData);
return providersData;
} catch (error) {
console.error('Failed to fetch providers:', error);
return [];
}
}
return providersList;
},
@@ -189,9 +195,11 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
// Load providers
try {
const providersResponse = await providers();
setProvidersList(providersResponse.data || []);
const providersData = providersResponse.data || [];
setProvidersList(providersData);
} catch (error) {
console.error('Failed to load providers:', error);
setProvidersList([]);
}
// Load extensions
@@ -1,5 +1,6 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import { render } from '@testing-library/react';
import { screen, waitFor } from '@testing-library/dom';
import MarkdownContent from './MarkdownContent';
// Mock the icons to avoid import issues
@@ -159,11 +159,14 @@ export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => {
// Add the "Custom model" option to each provider group
formattedModelOptions.forEach((group) => {
group.options.push({
value: 'custom',
label: 'Use custom model',
provider: group.options[0]?.provider,
});
const providerName = group.options[0]?.provider;
if (providerName && !providerName.startsWith('custom_')) {
group.options.push({
value: 'custom',
label: 'Use custom model',
provider: providerName,
});
}
});
setModelOptions(formattedModelOptions);
@@ -45,7 +45,7 @@ export default function PermissionModal({ extensionName, onClose }: PermissionMo
console.error('Failed to get tools');
} else {
const filteredTools = (response.data || []).filter(
(tool) =>
(tool: ToolInfo) =>
tool.name !== 'platform__read_resource' && tool.name !== 'platform__list_resources'
);
setTools(filteredTools);
@@ -1,8 +1,12 @@
import React, { memo, useMemo, useCallback } from 'react';
import React, { memo, useMemo, useCallback, useState } from 'react';
import { ProviderCard } from './subcomponents/ProviderCard';
import CardContainer from './subcomponents/CardContainer';
import { ProviderModalProvider, useProviderModal } from './modal/ProviderModalProvider';
import ProviderConfigurationModal from './modal/ProviderConfiguationModal';
import { ProviderDetails } from '../../../api';
import { ProviderDetails, CreateCustomProviderRequest } from '../../../api';
import { Plus } from 'lucide-react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../ui/dialog';
import CustomProviderForm from './modal/subcomponents/forms/CustomProviderForm';
const GridLayout = memo(function GridLayout({ children }: { children: React.ReactNode }) {
return (
@@ -18,6 +22,27 @@ const GridLayout = memo(function GridLayout({ children }: { children: React.Reac
);
});
const CustomProviderCard = memo(function CustomProviderCard({ onClick }: { onClick: () => void }) {
return (
<CardContainer
testId="add-custom-provider-card"
onClick={onClick}
header={null}
body={
<div className="flex flex-col items-center justify-center min-h-[200px]">
<Plus className="w-8 h-8 text-gray-400 mb-2" />
<div className="text-sm text-gray-600 dark:text-gray-400 text-center">
<div>Add</div>
<div>Custom Provider</div>
</div>
</div>
}
grayedOut={false}
borderStyle="dashed"
/>
);
});
// Memoize the ProviderCards component
const ProviderCards = memo(function ProviderCards({
providers,
@@ -31,6 +56,7 @@ const ProviderCards = memo(function ProviderCards({
onProviderLaunch: (provider: ProviderDetails) => void;
}) {
const { openModal } = useProviderModal();
const [showCustomProviderModal, setShowCustomProviderModal] = useState(false);
// Memoize these functions so they don't get recreated on every render
const configureProviderViaModal = useCallback(
@@ -42,7 +68,7 @@ const ProviderCards = memo(function ProviderCards({
refreshProviders();
}
},
onDelete: () => {
onDelete: (_values: unknown) => {
if (refreshProviders) {
refreshProviders();
}
@@ -56,7 +82,7 @@ const ProviderCards = memo(function ProviderCards({
const deleteProviderConfigViaModal = useCallback(
(provider: ProviderDetails) => {
openModal(provider, {
onDelete: () => {
onDelete: (_values: unknown) => {
// Only refresh if the function is provided
if (refreshProviders) {
refreshProviders();
@@ -68,12 +94,27 @@ const ProviderCards = memo(function ProviderCards({
[openModal, refreshProviders]
);
// We don't need an intermediate function here
// Just pass the onProviderLaunch directly
const handleCreateCustomProvider = useCallback(
async (data: CreateCustomProviderRequest) => {
try {
const { createCustomProvider } = await import('../../../api');
await createCustomProvider({ body: data });
setShowCustomProviderModal(false);
if (refreshProviders) {
refreshProviders();
}
} catch (error) {
console.error('Failed to create custom provider:', error);
}
},
[refreshProviders]
);
// Use useMemo to memoize the cards array
const providerCards = useMemo(() => {
return providers.map((provider) => (
// providers needs to be an array
const providersArray = Array.isArray(providers) ? providers : [];
const cards = providersArray.map((provider) => (
<ProviderCard
key={provider.name}
provider={provider}
@@ -83,6 +124,12 @@ const ProviderCards = memo(function ProviderCards({
isOnboarding={isOnboarding}
/>
));
cards.push(
<CustomProviderCard key="add-custom" onClick={() => setShowCustomProviderModal(true)} />
);
return cards;
}, [
providers,
isOnboarding,
@@ -91,7 +138,23 @@ const ProviderCards = memo(function ProviderCards({
onProviderLaunch,
]);
return <>{providerCards}</>;
return (
<>
{providerCards}
<Dialog open={showCustomProviderModal} onOpenChange={setShowCustomProviderModal}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>Add Custom Provider</DialogTitle>
</DialogHeader>
<CustomProviderForm
onSubmit={handleCreateCustomProvider}
onCancel={() => setShowCustomProviderModal(false)}
/>
</DialogContent>
</Dialog>
</>
);
});
export default memo(function ProviderGrid({
@@ -18,7 +18,7 @@ import OllamaForm from './subcomponents/forms/OllamaForm';
import { useConfig } from '../../../ConfigContext';
import { useModelAndProvider } from '../../../ModelAndProviderContext';
import { AlertTriangle } from 'lucide-react';
import { ConfigKey } from '../../../../api';
import { ConfigKey, removeCustomProvider } from '../../../../api';
interface FormValues {
[key: string]: string | number | boolean | null;
@@ -162,13 +162,21 @@ export default function ProviderConfigurationModal() {
}
try {
// Remove the provider configuration
// get the keys
const params = currentProvider.metadata.config_keys;
const isCustomProvider = currentProvider.name.startsWith('custom_');
// go through the keys are remove them
for (const param of params) {
await remove(param.name, param.secret);
if (isCustomProvider) {
await removeCustomProvider({
path: { id: currentProvider.name },
});
} else {
// Remove the provider configuration
// get the keys
const params = currentProvider.metadata.config_keys;
// go through the keys are remove them
for (const param of params) {
await remove(param.name, param.secret);
}
}
// Call onDelete callback if provided
@@ -247,9 +255,9 @@ export default function ProviderConfigurationModal() {
setShowDeleteConfirmation(false);
setIsActiveProvider(false);
}}
canDelete={isConfigured && !isActiveProvider} // Disable delete button for active provider
canDelete={isConfigured && !isActiveProvider}
providerName={currentProvider.metadata.display_name}
isActiveProvider={isActiveProvider} // Pass this to actions for button state
isActiveProvider={isActiveProvider}
/>
</DialogFooter>
</DialogContent>
@@ -0,0 +1,198 @@
import React, { useState } from 'react';
import { Input } from '../../../../../ui/input';
import { Select } from '../../../../../ui/Select';
import { Button } from '../../../../../ui/button';
import { SecureStorageNotice } from '../SecureStorageNotice';
import { Checkbox } from '@radix-ui/themes';
interface CustomProviderFormProps {
onSubmit: (data: {
provider_type: string;
display_name: string;
api_url: string;
api_key: string;
models: string[];
supports_streaming: boolean;
}) => void;
onCancel: () => void;
}
export default function CustomProviderForm({ onSubmit, onCancel }: CustomProviderFormProps) {
const [providerType, setProviderType] = useState('openai_compatible');
const [displayName, setDisplayName] = useState('');
const [apiUrl, setApiUrl] = useState('');
const [apiKey, setApiKey] = useState('');
const [models, setModels] = useState('');
const [isLocalModel, setIsLocalModel] = useState(false);
const [supportsStreaming, setSupportsStreaming] = useState(true);
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
const handleLocalModels = (checked: boolean) => {
setIsLocalModel(checked);
if (checked) {
setApiKey('notrequired');
} else {
setApiKey('');
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const errors: Record<string, string> = {};
if (!displayName) errors.displayName = 'Display name is required';
if (!apiUrl) errors.apiUrl = 'API URL is required';
if (!isLocalModel && !apiKey) errors.apiKey = 'API key is required';
if (!models) errors.models = 'At least one model is required';
if (Object.keys(errors).length > 0) {
setValidationErrors(errors);
return;
}
const modelList = models
.split(',')
.map((m) => m.trim())
.filter((m) => m);
onSubmit({
provider_type: providerType,
display_name: displayName,
api_url: apiUrl,
api_key: apiKey,
models: modelList,
supports_streaming: supportsStreaming,
});
};
return (
<form onSubmit={handleSubmit} className="mt-4 space-y-4">
<div>
<label className="flex items-center text-sm font-medium text-white mb-1">
Provider Type
<span className="text-red-500 ml-1">*</span>
</label>
<Select
options={[
{ value: 'openai_compatible', label: 'OpenAI Compatible' },
{ value: 'anthropic_compatible', label: 'Anthropic Compatible' },
{ value: 'ollama_compatible', label: 'Ollama Compatible' },
]}
value={{
value: providerType,
label:
providerType === 'openai_compatible'
? 'OpenAI Compatible'
: providerType === 'anthropic_compatible'
? 'Anthropic Compatible'
: 'Ollama Compatible',
}}
onChange={(option: unknown) => {
const selectedOption = option as { value: string; label: string } | null;
if (selectedOption) setProviderType(selectedOption.value);
}}
isSearchable={false}
/>
</div>
<div>
<label className="flex items-center text-sm font-medium text-white mb-1">
Display Name
<span className="text-red-500 ml-1">*</span>
</label>
<Input
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder="Your Provider Name"
className={validationErrors.displayName ? 'border-red-500' : ''}
/>
{validationErrors.displayName && (
<p className="text-red-500 text-sm mt-1">{validationErrors.displayName}</p>
)}
</div>
<div>
<label className="flex items-center text-sm font-medium text-white mb-1">
API URL
<span className="text-red-500 ml-1">*</span>
</label>
<Input
value={apiUrl}
onChange={(e) => setApiUrl(e.target.value)}
placeholder="https://api.example.com/v1/messages"
className={validationErrors.apiUrl ? 'border-red-500' : ''}
/>
{validationErrors.apiUrl && (
<p className="text-red-500 text-sm mt-1">{validationErrors.apiUrl}</p>
)}
</div>
<div>
<label className="flex items-center text-sm font-medium text-white mb-1">
API Key
{!isLocalModel && <span className="text-red-500 ml-1">*</span>}
</label>
<Input
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="Your API key"
className={validationErrors.apiKey ? 'border-red-500' : ''}
disabled={isLocalModel}
/>
{validationErrors.apiKey && (
<p className="text-red-500 text-sm mt-1">{validationErrors.apiKey}</p>
)}
<div className="flex items-center space-x-2 mt-2">
<Checkbox id="local-model" checked={isLocalModel} onCheckedChange={handleLocalModels} />
<label
htmlFor="local-model"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-gray-400"
>
This is a local model (no auth required)
</label>
</div>
</div>
<div>
<label className="flex items-center text-sm font-medium text-white mb-1">
Available Models (comma-separated)
<span className="text-red-500 ml-1">*</span>
</label>
<Input
value={models}
onChange={(e) => setModels(e.target.value)}
placeholder="model-a, model-b, model-c"
className={validationErrors.models ? 'border-red-500' : ''}
/>
{validationErrors.models && (
<p className="text-red-500 text-sm mt-1">{validationErrors.models}</p>
)}
</div>
<div className="flex items-center space-x-2 mb-10">
<Checkbox
id="supports-streaming"
checked={supportsStreaming}
onCheckedChange={(checked) => setSupportsStreaming(checked as boolean)}
/>
<label
htmlFor="supports-streaming"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-gray-400"
>
Provider supports streaming responses
</label>
</div>
<SecureStorageNotice />
<div className="flex justify-end space-x-2 pt-4">
<Button type="button" variant="outline" onClick={onCancel}>
Cancel
</Button>
<Button type="submit">Create Provider</Button>
</div>
</form>
);
}
@@ -3,9 +3,7 @@ import { Input } from '../../../../../ui/input';
import { useConfig } from '../../../../../ConfigContext'; // Adjust this import path as needed
import { ProviderDetails, ConfigKey } from '../../../../../../api';
interface ValidationErrors {
[key: string]: string;
}
type ValidationErrors = Record<string, string>;
interface DefaultProviderSetupFormProps {
configValues: Record<string, string>;
@@ -36,33 +34,30 @@ export default function DefaultProviderSetupForm({
// Try to load actual values from config for each parameter that is not secret
for (const parameter of parameters) {
if (parameter.required) {
try {
// Check if there's a stored value in the config system
const configKey = `${parameter.name}`;
const configResponse = await read(configKey, parameter.secret || false);
try {
// Check if there's a stored value in the config system
const configKey = `${parameter.name}`;
const configResponse = await read(configKey, parameter.secret || false);
if (configResponse) {
// Use the value from the config provider
newValues[parameter.name] = String(configResponse);
} else if (
parameter.default !== undefined &&
parameter.default !== null &&
!configValues[parameter.name]
) {
// Fall back to default value if no config value exists
newValues[parameter.name] = String(parameter.default);
}
} catch (error) {
console.error(`Failed to load config for ${parameter.name}:`, error);
// Fall back to default if read operation fails
if (
parameter.default !== undefined &&
parameter.default !== null &&
!configValues[parameter.name]
) {
newValues[parameter.name] = String(parameter.default);
}
if (configResponse) {
newValues[parameter.name] = parameter.secret ? 'true' : String(configResponse);
} else if (
parameter.default !== undefined &&
parameter.default !== null &&
!configValues[parameter.name]
) {
// Fall back to default value if no config value exists
newValues[parameter.name] = String(parameter.default);
}
} catch (error) {
console.error(`Failed to load config for ${parameter.name}:`, error);
// Fall back to default if read operation fails
if (
parameter.default !== undefined &&
parameter.default !== null &&
!configValues[parameter.name]
) {
newValues[parameter.name] = String(parameter.default);
}
}
}
@@ -85,6 +80,11 @@ export default function DefaultProviderSetupForm({
return parameters.filter((param) => param.required === true);
}, [parameters]);
// TODO: show all params, not just required ones
// const allParameters = useMemo(() => {
// return parameters;
// }, [parameters]);
// Helper function to generate appropriate placeholder text
const getPlaceholder = (parameter: ConfigKey): string => {
// If default is defined and not null, show it
@@ -92,8 +92,30 @@ export default function DefaultProviderSetupForm({
return `Default: ${parameter.default}`;
}
// Otherwise, use the parameter name as a hint
return parameter.name.toUpperCase();
const name = parameter.name.toLowerCase();
if (name.includes('api_key')) return 'Your API key';
if (name.includes('api_url') || name.includes('host')) return 'https://api.example.com';
if (name.includes('models')) return 'model-a, model-b';
return parameter.name
.replace(/_/g, ' ')
.replace(/([A-Z])/g, ' $1')
.replace(/^./, (str) => str.toUpperCase())
.trim();
};
// helper for custom labels
const getFieldLabel = (parameter: ConfigKey): string => {
const name = parameter.name.toLowerCase();
if (name.includes('api_key')) return 'API Key';
if (name.includes('api_url') || name.includes('host')) return 'API Host';
if (name.includes('models')) return 'Models';
return parameter.name
.replace(/_/g, ' ')
.replace(/([A-Z])/g, ' $1')
.replace(/^./, (str) => str.toUpperCase())
.trim();
};
if (isLoading) {
@@ -111,25 +133,30 @@ export default function DefaultProviderSetupForm({
requiredParameters.map((parameter) => (
<div key={parameter.name}>
<label className="block text-sm font-medium text-textStandard mb-1">
{parameter.name}
{getFieldLabel(parameter)}
{parameter.required && <span className="text-red-500 ml-1">*</span>}
</label>
<Input
type={parameter.secret ? 'password' : 'text'}
value={configValues[parameter.name] || ''}
onChange={(e) =>
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
console.log(`Setting ${parameter.name} to:`, e.target.value);
setConfigValues((prev) => ({
...prev,
[parameter.name]: e.target.value,
}))
}
}));
}}
placeholder={getPlaceholder(parameter)}
className={`w-full h-14 px-4 font-regular rounded-lg shadow-none ${
validationErrors[parameter.name]
? 'border-2 border-red-500'
: 'border border-borderSubtle hover:border-borderStandard'
} bg-background-default text-lg placeholder:text-textSubtle font-regular text-textStandard`}
required={true}
required={parameter.required}
/>
{validationErrors[parameter.name] && (
<p className="text-red-500 text-sm mt-1">{validationErrors[parameter.name]}</p>
)}
</div>
))
)}
@@ -6,6 +6,7 @@ interface CardContainerProps {
onClick: () => void;
grayedOut: boolean;
testId?: string;
borderStyle?: 'solid' | 'dashed';
}
function GlowingRing() {
@@ -33,6 +34,7 @@ export default function CardContainer({
onClick,
grayedOut = false,
testId,
borderStyle = 'solid',
}: CardContainerProps) {
return (
<div
@@ -50,19 +52,21 @@ export default function CardContainer({
>
{!grayedOut && <GlowingRing />}
<div
className={`relative bg-background-default rounded-lg p-3 transition-all duration-200 h-[160px] flex flex-col justify-between
className={`relative bg-background-default rounded-lg p-3 transition-all duration-200 h-[160px] flex flex-col
${header ? 'justify-between' : 'justify-center'}
${borderStyle === 'dashed' ? 'border-2 border-dashed' : 'border'}
${
grayedOut
? 'border border-borderSubtle'
: 'border border-borderSubtle hover:border-borderStandard'
? 'border-borderSubtle'
: 'border-borderSubtle hover:border-borderStandard'
}`}
>
{/* Apply opacity only to the header when grayed out */}
<div style={{ opacity: grayedOut ? '0.5' : '1' }}>
<HeaderContainer>{header}</HeaderContainer>
</div>
{header && (
<div style={{ opacity: grayedOut ? '0.5' : '1' }}>
<HeaderContainer>{header}</HeaderContainer>
</div>
)}
{/* Body always at full opacity */}
<div>{body}</div>
</div>
</div>
+1 -1
View File
@@ -4,7 +4,7 @@ import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '../../utils';
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[1px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm transition-all cursor-pointer disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[1px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
+2 -2
View File
@@ -1,5 +1,5 @@
import { Message } from './types/message';
import { getSessionHistory, listSessions, SessionInfo } from './api';
import { getSessionHistory, listSessions, SessionInfo, Message as ApiMessage } from './api';
import { convertApiMessageToFrontendMessage } from './components/context_management';
import { getApiUrl } from './config';
@@ -118,7 +118,7 @@ export async function fetchSessionDetails(sessionId: string): Promise<SessionDet
return {
session_id: response.data.sessionId,
metadata: ensureWorkingDir(response.data.metadata),
messages: response.data.messages.map((message) =>
messages: response.data.messages.map((message: ApiMessage) =>
convertApiMessageToFrontendMessage(message, true, true)
), // slight diffs between backend and frontend Message obj
};