Skip to content
Merged
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 interface/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,7 @@ export interface ProviderStatus {
opencode_zen: boolean;
nvidia: boolean;
minimax: boolean;
moonshot: boolean;
}

export interface ProvidersResponse {
Expand Down
2 changes: 2 additions & 0 deletions interface/src/lib/providerIcons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Together from "@lobehub/icons/es/Together";
import XAI from "@lobehub/icons/es/XAI";
import ZAI from "@lobehub/icons/es/ZAI";
import Minimax from "@lobehub/icons/es/Minimax";
import Kimi from "@lobehub/icons/es/Kimi";

interface IconProps {
size?: number;
Expand Down Expand Up @@ -109,6 +110,7 @@ export function ProviderIcon({ provider, className = "text-ink-faint", size = 24
"opencode-zen": OpenCodeZenIcon,
nvidia: NvidiaIcon,
minimax: Minimax,
moonshot: Kimi, // Kimi is Moonshot AI's product brand
};

const IconComponent = iconMap[provider.toLowerCase()];
Expand Down
8 changes: 8 additions & 0 deletions interface/src/routes/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,14 @@ const PROVIDERS = [
envVar: "MINIMAX_API_KEY",
defaultModel: "MiniMax-M1-80k",
},
{
id: "moonshot",
name: "Moonshot AI",
description: "Kimi models (Kimi K2, Kimi K2.5)",
placeholder: "sk-...",
envVar: "MOONSHOT_API_KEY",
defaultModel: "kimi-k2.5",
},
{
id: "ollama",
name: "Ollama",
Expand Down
18 changes: 18 additions & 0 deletions src/api/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,23 @@ fn extra_models() -> Vec<ModelInfo> {
tool_call: true,
reasoning: false,
},
// Moonshot AI (Kimi)
ModelInfo {
id: "moonshot/kimi-k2.5".into(),
name: "Kimi K2.5".into(),
provider: "moonshot".into(),
context_window: None,
tool_call: true,
reasoning: true,
},
ModelInfo {
id: "moonshot/moonshot-v1-8k".into(),
name: "Moonshot V1 8K".into(),
provider: "moonshot".into(),
context_window: Some(8000),
tool_call: false,
reasoning: false,
},
]
}

Expand Down Expand Up @@ -284,6 +301,7 @@ pub(super) async fn configured_providers(config_path: &std::path::Path) -> Vec<&
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("minimax_key", "MINIMAX_API_KEY") { providers.push("minimax"); }
if has_key("moonshot_key", "MOONSHOT_API_KEY") { providers.push("moonshot"); }

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 @@ -20,6 +20,7 @@ pub(super) struct ProviderStatus {
mistral: bool,
opencode_zen: bool,
minimax: bool,
moonshot: bool,
}

#[derive(Serialize)]
Expand All @@ -45,7 +46,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, minimax) = if config_path.exists() {
let (anthropic, openai, openrouter, zhipu, groq, together, fireworks, deepseek, xai, mistral, opencode_zen, minimax, moonshot) = if config_path.exists() {
let content = tokio::fs::read_to_string(&config_path)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Expand Down Expand Up @@ -80,6 +81,7 @@ pub(super) async fn get_providers(
has_key("mistral_key", "MISTRAL_API_KEY"),
has_key("opencode_zen_key", "OPENCODE_ZEN_API_KEY"),
has_key("minimax_key", "MINIMAX_API_KEY"),
has_key("moonshot_key", "MOONSHOT_API_KEY"),
)
} else {
(
Expand All @@ -95,6 +97,7 @@ pub(super) async fn get_providers(
std::env::var("MISTRAL_API_KEY").is_ok(),
std::env::var("OPENCODE_ZEN_API_KEY").is_ok(),
std::env::var("MINIMAX_API_KEY").is_ok(),
std::env::var("MOONSHOT_API_KEY").is_ok(),
)
};

Expand All @@ -111,6 +114,7 @@ pub(super) async fn get_providers(
mistral,
opencode_zen,
minimax,
moonshot,
};
let has_any = providers.anthropic
|| providers.openai
Expand All @@ -123,7 +127,8 @@ pub(super) async fn get_providers(
|| providers.xai
|| providers.mistral
|| providers.opencode_zen
|| providers.minimax;
|| providers.minimax
|| providers.moonshot;

Ok(Json(ProvidersResponse { providers, has_any }))
}
Expand All @@ -145,6 +150,7 @@ pub(super) async fn update_provider(
"mistral" => "mistral_key",
"opencode-zen" => "opencode_zen_key",
"minimax" => "minimax_key",
"moonshot" => "moonshot_key",
_ => {
return Ok(Json(ProviderUpdateResponse {
success: false,
Expand Down Expand Up @@ -215,6 +221,7 @@ pub(super) async fn update_provider(
"mistral" => has_provider_key("mistral_key", "MISTRAL_API_KEY"),
"opencode-zen" => has_provider_key("opencode_zen_key", "OPENCODE_ZEN_API_KEY"),
"minimax" => has_provider_key("minimax_key", "MINIMAX_API_KEY"),
"moonshot" => has_provider_key("moonshot_key", "MOONSHOT_API_KEY"),
_ => false,
};

Expand Down Expand Up @@ -281,6 +288,7 @@ pub(super) async fn delete_provider(
"mistral" => "mistral_key",
"opencode-zen" => "opencode_zen_key",
"minimax" => "minimax_key",
"moonshot" => "moonshot_key",
_ => {
return Ok(Json(ProviderUpdateResponse {
success: false,
Expand Down
40 changes: 40 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ pub struct LlmConfig {
pub opencode_zen_key: Option<String>,
pub nvidia_key: Option<String>,
pub minimax_key: Option<String>,
pub moonshot_key: Option<String>,
pub providers: HashMap<String, ProviderConfig>,
}

Expand All @@ -167,6 +168,7 @@ impl LlmConfig {
|| self.opencode_zen_key.is_some()
|| self.nvidia_key.is_some()
|| self.minimax_key.is_some()
|| self.moonshot_key.is_some()
|| !self.providers.is_empty()
}
}
Expand All @@ -175,6 +177,7 @@ 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 MINIMAX_PROVIDER_BASE_URL: &str = "https://api.minimax.io/anthropic";
const MOONSHOT_PROVIDER_BASE_URL: &str = "https://api.moonshot.ai";

/// Defaults inherited by all agents. Individual agents can override any field.
#[derive(Debug, Clone)]
Expand Down Expand Up @@ -1104,6 +1107,7 @@ struct TomlLlmConfigFields {
opencode_zen_key: Option<String>,
nvidia_key: Option<String>,
minimax_key: Option<String>,
moonshot_key: Option<String>,
#[serde(default)]
providers: HashMap<String, TomlProviderConfig>,
#[serde(default)]
Expand All @@ -1128,6 +1132,7 @@ struct TomlLlmConfig {
opencode_zen_key: Option<String>,
nvidia_key: Option<String>,
minimax_key: Option<String>,
moonshot_key: Option<String>,
providers: HashMap<String, TomlProviderConfig>,
}

Expand Down Expand Up @@ -1177,6 +1182,7 @@ impl<'de> Deserialize<'de> for TomlLlmConfig {
opencode_zen_key: fields.opencode_zen_key,
nvidia_key: fields.nvidia_key,
minimax_key: fields.minimax_key,
moonshot_key: fields.moonshot_key,
providers: fields.providers,
})
}
Expand Down Expand Up @@ -1517,6 +1523,9 @@ impl Config {
|| std::env::var("OLLAMA_BASE_URL").is_ok()
|| std::env::var("OPENCODE_ZEN_API_KEY").is_ok()
|| std::env::var("MINIMAX_API_KEY").is_ok();
|| std::env::var("MOONSHOT_API_KEY").is_ok();
|| std::env::var("MINIMAX_API_KEY").is_ok()
|| std::env::var("MOONSHOT_API_KEY").is_ok();

// If we have any legacy keys, no onboarding needed
if has_legacy_keys {
Expand Down Expand Up @@ -1579,6 +1588,7 @@ impl Config {
opencode_zen_key: std::env::var("OPENCODE_ZEN_API_KEY").ok(),
nvidia_key: std::env::var("NVIDIA_API_KEY").ok(),
minimax_key: std::env::var("MINIMAX_API_KEY").ok(),
moonshot_key: std::env::var("MOONSHOT_API_KEY").ok(),
providers: HashMap::new(),
};

Expand Down Expand Up @@ -1627,6 +1637,17 @@ impl Config {
});
}

if let Some(moonshot_key) = llm.moonshot_key.clone() {
llm.providers
.entry("moonshot".to_string())
.or_insert_with(|| ProviderConfig {
api_type: ApiType::OpenAiCompletions,
base_url: MOONSHOT_PROVIDER_BASE_URL.to_string(),
api_key: moonshot_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 @@ -1809,6 +1830,12 @@ impl Config {
.as_deref()
.and_then(resolve_env_value)
.or_else(|| std::env::var("MINIMAX_API_KEY").ok()),
moonshot_key: toml
.llm
.moonshot_key
.as_deref()
.and_then(resolve_env_value)
.or_else(|| std::env::var("MOONSHOT_API_KEY").ok()),
providers: toml
.llm
.providers
Expand Down Expand Up @@ -1872,6 +1899,17 @@ impl Config {
});
}

if let Some(moonshot_key) = llm.moonshot_key.clone() {
llm.providers
.entry("moonshot".to_string())
.or_insert_with(|| ProviderConfig {
api_type: ApiType::OpenAiCompletions,
base_url: MOONSHOT_PROVIDER_BASE_URL.to_string(),
api_key: moonshot_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 @@ -2919,6 +2957,7 @@ pub fn run_onboarding() -> anyhow::Result<Option<PathBuf>> {
"Ollama",
"OpenCode Zen",
"MiniMax",
"Moonshot AI (Kimi)",
];
let provider_idx = Select::new()
.with_prompt("Which LLM provider do you want to use?")
Expand All @@ -2940,6 +2979,7 @@ pub fn run_onboarding() -> anyhow::Result<Option<PathBuf>> {
10 => ("Ollama base URL", "ollama_base_url", "ollama"),
11 => ("OpenCode Zen API key", "opencode_zen_key", "opencode-zen"),
12 => ("MiniMax API key", "minimax_key", "minimax"),
13 => ("Moonshot AI API key", "moonshot_key", "moonshot"),
_ => unreachable!(),
};
let is_secret = provider_id != "ollama";
Expand Down
11 changes: 9 additions & 2 deletions src/llm/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,12 +426,19 @@ impl SpacebotModel {
provider_config.base_url.trim_end_matches('/')
);

let response = self
let mut request_builder = self
.llm_manager
.http_client()
.post(&chat_completions_url)
.header("authorization", format!("Bearer {api_key}"))
.header("content-type", "application/json")
.header("content-type", "application/json");

// Kimi endpoints require a specific user-agent header.
if chat_completions_url.contains("kimi.com") || chat_completions_url.contains("moonshot.ai") {
request_builder = request_builder.header("user-agent", "KimiCLI/1.3");
}

let response = request_builder
.json(&body)
.send()
.await
Expand Down
4 changes: 4 additions & 0 deletions src/llm/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,9 @@ pub async fn init_providers(config: &LlmConfig) -> Result<()> {
tracing::info!("MiniMax provider configured");
}

if config.moonshot_key.is_some() {
tracing::info!("Moonshot AI provider configured");
}

Ok(())
}
2 changes: 2 additions & 0 deletions src/llm/routing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ pub fn defaults_for_provider(provider: &str) -> RoutingConfig {
"nvidia" => RoutingConfig::for_model("nvidia/meta/llama-3.1-405b-instruct".into()),
"opencode-zen" => RoutingConfig::for_model("opencode-zen/kimi-k2.5".into()),
"minimax" => RoutingConfig::for_model("minimax/MiniMax-M1-80k".into()),
"moonshot" => RoutingConfig::for_model("moonshot/kimi-k2.5".into()),
_ => RoutingConfig::default(),
}
}
Expand All @@ -165,6 +166,7 @@ pub fn provider_to_prefix(provider: &str) -> &str {
"nvidia" => "nvidia/",
"opencode-zen" => "opencode-zen/",
"minimax" => "minimax/",
"moonshot" => "moonshot/",
_ => "",
}
}
Expand Down