Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/api/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ pub(super) async fn configured_providers(config_path: &std::path::Path) -> Vec<&
if has_key("xai_key", "XAI_API_KEY") { providers.push("xai"); }
if has_key("mistral_key", "MISTRAL_API_KEY") { providers.push("mistral"); }
if has_key("opencode_zen_key", "OPENCODE_ZEN_API_KEY") { providers.push("opencode-zen"); }
if has_key("featherless_key", "FEATHERLESS_API_KEY") { providers.push("featherless"); }

providers
}
Expand Down
12 changes: 10 additions & 2 deletions src/api/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub(super) struct ProviderStatus {
xai: bool,
mistral: bool,
opencode_zen: bool,
featherless: bool,
}

#[derive(Serialize)]
Expand All @@ -44,7 +45,7 @@ pub(super) async fn get_providers(
) -> Result<Json<ProvidersResponse>, StatusCode> {
let config_path = state.config_path.read().await.clone();

let (anthropic, openai, openrouter, zhipu, groq, together, fireworks, deepseek, xai, mistral, opencode_zen) = if config_path.exists() {
let (anthropic, openai, openrouter, zhipu, groq, together, fireworks, deepseek, xai, mistral, opencode_zen, featherless) = if config_path.exists() {
let content = tokio::fs::read_to_string(&config_path)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Expand Down Expand Up @@ -78,6 +79,7 @@ pub(super) async fn get_providers(
has_key("xai_key", "XAI_API_KEY"),
has_key("mistral_key", "MISTRAL_API_KEY"),
has_key("opencode_zen_key", "OPENCODE_ZEN_API_KEY"),
has_key("featherless_key", "FEATHERLESS_API_KEY"),
)
} else {
(
Expand All @@ -92,6 +94,7 @@ pub(super) async fn get_providers(
std::env::var("XAI_API_KEY").is_ok(),
std::env::var("MISTRAL_API_KEY").is_ok(),
std::env::var("OPENCODE_ZEN_API_KEY").is_ok(),
std::env::var("FEATHERLESS_API_KEY").is_ok(),
)
};

Expand All @@ -107,6 +110,7 @@ pub(super) async fn get_providers(
xai,
mistral,
opencode_zen,
featherless,
};
let has_any = providers.anthropic
|| providers.openai
Expand All @@ -118,7 +122,8 @@ pub(super) async fn get_providers(
|| providers.deepseek
|| providers.xai
|| providers.mistral
|| providers.opencode_zen;
|| providers.opencode_zen
|| providers.featherless;

Ok(Json(ProvidersResponse { providers, has_any }))
}
Expand All @@ -139,6 +144,7 @@ pub(super) async fn update_provider(
"xai" => "xai_key",
"mistral" => "mistral_key",
"opencode-zen" => "opencode_zen_key",
"featherless" => "featherless_key",
_ => {
return Ok(Json(ProviderUpdateResponse {
success: false,
Expand Down Expand Up @@ -208,6 +214,7 @@ pub(super) async fn update_provider(
"xai" => has_provider_key("xai_key", "XAI_API_KEY"),
"mistral" => has_provider_key("mistral_key", "MISTRAL_API_KEY"),
"opencode-zen" => has_provider_key("opencode_zen_key", "OPENCODE_ZEN_API_KEY"),
"featherless" => has_provider_key("featherless_key", "FEATHERLESS_API_KEY"),
_ => false,
};

Expand Down Expand Up @@ -273,6 +280,7 @@ pub(super) async fn delete_provider(
"xai" => "xai_key",
"mistral" => "mistral_key",
"opencode-zen" => "opencode_zen_key",
"featherless" => "featherless_key",
_ => {
return Ok(Json(ProviderUpdateResponse {
success: false,
Expand Down
48 changes: 46 additions & 2 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ pub struct LlmConfig {
pub ollama_base_url: Option<String>,
pub opencode_zen_key: Option<String>,
pub nvidia_key: Option<String>,
pub featherless_key: Option<String>,
pub providers: HashMap<String, ProviderConfig>,
}

Expand All @@ -165,13 +166,15 @@ impl LlmConfig {
|| self.ollama_base_url.is_some()
|| self.opencode_zen_key.is_some()
|| self.nvidia_key.is_some()
|| self.featherless_key.is_some()
|| !self.providers.is_empty()
}
}

const ANTHROPIC_PROVIDER_BASE_URL: &str = "https://api.anthropic.com";
const OPENAI_PROVIDER_BASE_URL: &str = "https://api.openai.com";
const OPENROUTER_PROVIDER_BASE_URL: &str = "https://openrouter.ai/api";
const FEATHERLESS_PROVIDER_BASE_URL: &str = "https://api.featherless.ai";

/// Defaults inherited by all agents. Individual agents can override any field.
#[derive(Debug, Clone)]
Expand Down Expand Up @@ -1087,6 +1090,7 @@ struct TomlLlmConfigFields {
ollama_base_url: Option<String>,
opencode_zen_key: Option<String>,
nvidia_key: Option<String>,
featherless_key: Option<String>,
#[serde(default)]
providers: HashMap<String, TomlProviderConfig>,
#[serde(default)]
Expand All @@ -1106,7 +1110,11 @@ struct TomlLlmConfig {
deepseek_key: Option<String>,
xai_key: Option<String>,
mistral_key: Option<String>,
ollama_key: Option<String>,
ollama_base_url: Option<String>,
opencode_zen_key: Option<String>,
nvidia_key: Option<String>,
featherless_key: Option<String>,
providers: HashMap<String, TomlProviderConfig>,
}

Expand Down Expand Up @@ -1151,7 +1159,11 @@ impl<'de> Deserialize<'de> for TomlLlmConfig {
deepseek_key: fields.deepseek_key,
xai_key: fields.xai_key,
mistral_key: fields.mistral_key,
ollama_key: fields.ollama_key,
ollama_base_url: fields.ollama_base_url,
opencode_zen_key: fields.opencode_zen_key,
nvidia_key: fields.nvidia_key,
featherless_key: fields.featherless_key,
providers: fields.providers,
})
}
Expand Down Expand Up @@ -1481,8 +1493,9 @@ impl Config {
|| std::env::var("NVIDIA_API_KEY").is_ok()
|| std::env::var("OLLAMA_API_KEY").is_ok()
|| std::env::var("OLLAMA_BASE_URL").is_ok()
|| std::env::var("OPENCODE_ZEN_API_KEY").is_ok();

|| std::env::var("OPENCODE_ZEN_API_KEY").is_ok()
|| std::env::var("FEATHERLESS_API_KEY").is_ok();

// If we have any legacy keys, no onboarding needed
if has_legacy_keys {
return false;
Expand Down Expand Up @@ -1543,6 +1556,7 @@ impl Config {
ollama_base_url: std::env::var("OLLAMA_BASE_URL").ok(),
opencode_zen_key: std::env::var("OPENCODE_ZEN_API_KEY").ok(),
nvidia_key: std::env::var("NVIDIA_API_KEY").ok(),
featherless_key: std::env::var("FEATHERLESS_API_KEY").ok(),
providers: HashMap::new(),
};

Expand Down Expand Up @@ -1580,6 +1594,17 @@ impl Config {
});
}

if let Some(featherless_key) = llm.featherless_key.clone() {
llm.providers
.entry("featherless".to_string())
.or_insert_with(|| ProviderConfig {
api_type: ApiType::OpenAiCompletions,
base_url: FEATHERLESS_PROVIDER_BASE_URL.to_string(),
api_key: featherless_key,
name: None,
});
}

// Note: We allow boot without provider keys now. System starts in setup mode.
// Agents are initialized later when keys are added via API.

Expand Down Expand Up @@ -1756,6 +1781,12 @@ impl Config {
.as_deref()
.and_then(resolve_env_value)
.or_else(|| std::env::var("NVIDIA_API_KEY").ok()),
featherless_key: toml
.llm
.featherless_key
.as_deref()
.and_then(resolve_env_value)
.or_else(|| std::env::var("FEATHERLESS_API_KEY").ok()),
providers: toml
.llm
.providers
Expand Down Expand Up @@ -1808,6 +1839,17 @@ impl Config {
});
}

if let Some(featherless_key) = llm.featherless_key.clone() {
llm.providers
.entry("featherless".to_string())
.or_insert_with(|| ProviderConfig {
api_type: ApiType::OpenAiCompletions,
base_url: FEATHERLESS_PROVIDER_BASE_URL.to_string(),
api_key: featherless_key,
name: None,
});
}

// Note: We allow boot without provider keys now. System starts in setup mode.
// Agents are initialized later when keys are added via API.

Expand Down Expand Up @@ -2838,6 +2880,7 @@ pub fn run_onboarding() -> anyhow::Result<Option<PathBuf>> {
"Mistral AI",
"Ollama",
"OpenCode Zen",
"Featherless AI",
];
let provider_idx = Select::new()
.with_prompt("Which LLM provider do you want to use?")
Expand All @@ -2858,6 +2901,7 @@ pub fn run_onboarding() -> anyhow::Result<Option<PathBuf>> {
9 => ("Mistral AI API key", "mistral_key", "mistral"),
10 => ("Ollama base URL", "ollama_base_url", "ollama"),
11 => ("OpenCode Zen API key", "opencode_zen_key", "opencode-zen"),
12 => ("Featherless AI API key", "featherless_key", "featherless"),
_ => unreachable!(),
};
let is_secret = provider_id != "ollama";
Expand Down
4 changes: 4 additions & 0 deletions src/llm/routing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ pub fn defaults_for_provider(provider: &str) -> RoutingConfig {
"mistral" => RoutingConfig::for_model("mistral/mistral-large-latest".into()),
"nvidia" => RoutingConfig::for_model("nvidia/meta/llama-3.1-405b-instruct".into()),
"opencode-zen" => RoutingConfig::for_model("opencode-zen/kimi-k2.5".into()),
"featherless" => RoutingConfig::for_model(
"featherless/Qwen/Qwen3-32B".into(),
),
_ => RoutingConfig::default(),
}
}
Expand All @@ -163,6 +166,7 @@ pub fn provider_to_prefix(provider: &str) -> &str {
"mistral" => "mistral/",
"nvidia" => "nvidia/",
"opencode-zen" => "opencode-zen/",
"featherless" => "featherless/",
_ => "",
}
}
Expand Down