diff --git a/src/onboard/wizard.rs b/src/onboard/wizard.rs index 551644f..27c47da 100644 --- a/src/onboard/wizard.rs +++ b/src/onboard/wizard.rs @@ -495,6 +495,7 @@ fn default_model_for_provider(provider: &str) -> String { "groq" => "llama-3.3-70b-versatile".into(), "deepseek" => "deepseek-chat".into(), "gemini" => "gemini-2.5-pro".into(), + "kimi-code" => "kimi-for-coding".into(), _ => "anthropic/claude-sonnet-4.5".into(), } } @@ -1427,6 +1428,10 @@ fn setup_provider(workspace_dir: &Path) -> Result<(String, String, String, Optio ("bedrock", "Amazon Bedrock — AWS managed models"), ], 3 => vec![ + ( + "kimi-code", + "Kimi Code — coding-optimized Kimi API (KimiCLI)", + ), ("moonshot", "Moonshot — Kimi API (China endpoint)"), ( "moonshot-intl", @@ -4611,6 +4616,7 @@ mod tests { assert_eq!(default_model_for_provider("zai-cn"), "glm-5"); assert_eq!(default_model_for_provider("gemini"), "gemini-2.5-pro"); assert_eq!(default_model_for_provider("google"), "gemini-2.5-pro"); + assert_eq!(default_model_for_provider("kimi-code"), "kimi-for-coding"); assert_eq!( default_model_for_provider("google-gemini"), "gemini-2.5-pro" diff --git a/src/providers/compatible.rs b/src/providers/compatible.rs index 0342584..84ca28e 100644 --- a/src/providers/compatible.rs +++ b/src/providers/compatible.rs @@ -8,7 +8,10 @@ use crate::providers::traits::{ }; use async_trait::async_trait; use futures_util::{stream, StreamExt}; -use reqwest::Client; +use reqwest::{ + header::{HeaderMap, HeaderValue, USER_AGENT}, + Client, +}; use serde::{Deserialize, Serialize}; /// A provider that speaks the OpenAI-compatible chat completions API. @@ -22,6 +25,7 @@ pub struct OpenAiCompatibleProvider { /// When false, do not fall back to /v1/responses on chat completions 404. /// GLM/Zhipu does not support the responses API. supports_responses_fallback: bool, + user_agent: Option, } /// How the provider expects the API key to be sent. @@ -42,13 +46,7 @@ impl OpenAiCompatibleProvider { credential: Option<&str>, auth_style: AuthStyle, ) -> Self { - Self { - name: name.to_string(), - base_url: base_url.trim_end_matches('/').to_string(), - credential: credential.map(ToString::to_string), - auth_header: auth_style, - supports_responses_fallback: true, - } + Self::new_with_options(name, base_url, credential, auth_style, true, None) } /// Same as `new` but skips the /v1/responses fallback on 404. @@ -58,17 +56,69 @@ impl OpenAiCompatibleProvider { base_url: &str, credential: Option<&str>, auth_style: AuthStyle, + ) -> Self { + Self::new_with_options(name, base_url, credential, auth_style, false, None) + } + + /// Create a provider with a custom User-Agent header. + /// + /// Some providers (for example Kimi Code) require a specific User-Agent + /// for request routing and policy enforcement. + pub fn new_with_user_agent( + name: &str, + base_url: &str, + credential: Option<&str>, + auth_style: AuthStyle, + user_agent: &str, + ) -> Self { + Self::new_with_options( + name, + base_url, + credential, + auth_style, + true, + Some(user_agent), + ) + } + + fn new_with_options( + name: &str, + base_url: &str, + credential: Option<&str>, + auth_style: AuthStyle, + supports_responses_fallback: bool, + user_agent: Option<&str>, ) -> Self { Self { name: name.to_string(), base_url: base_url.trim_end_matches('/').to_string(), credential: credential.map(ToString::to_string), auth_header: auth_style, - supports_responses_fallback: false, + supports_responses_fallback, + user_agent: user_agent.map(ToString::to_string), } } fn http_client(&self) -> Client { + if let Some(ua) = self.user_agent.as_deref() { + let mut headers = HeaderMap::new(); + if let Ok(value) = HeaderValue::from_str(ua) { + headers.insert(USER_AGENT, value); + } + + let builder = Client::builder() + .timeout(std::time::Duration::from_secs(120)) + .connect_timeout(std::time::Duration::from_secs(10)) + .default_headers(headers); + let builder = + crate::config::apply_runtime_proxy_to_builder(builder, "provider.compatible"); + + return builder.build().unwrap_or_else(|error| { + tracing::warn!("Failed to build proxied timeout client with user-agent: {error}"); + Client::new() + }); + } + crate::config::build_runtime_proxy_client_with_timeouts("provider.compatible", 120, 10) } diff --git a/src/providers/mod.rs b/src/providers/mod.rs index 45b30e9..83e6a64 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -314,6 +314,9 @@ fn resolve_provider_credential(name: &str, credential_override: Option<&str>) -> "perplexity" => vec!["PERPLEXITY_API_KEY"], "cohere" => vec!["COHERE_API_KEY"], name if is_moonshot_alias(name) => vec!["MOONSHOT_API_KEY"], + "kimi-code" | "kimi_coding" | "kimi_for_coding" => { + vec!["KIMI_CODE_API_KEY", "MOONSHOT_API_KEY"] + } name if is_glm_alias(name) => vec!["GLM_API_KEY"], name if is_minimax_alias(name) => vec!["MINIMAX_API_KEY"], name if is_qianfan_alias(name) => vec!["QIANFAN_API_KEY"], @@ -432,6 +435,15 @@ pub fn create_provider_with_url( key, AuthStyle::Bearer, ))), + "kimi-code" | "kimi_coding" | "kimi_for_coding" => Ok(Box::new( + OpenAiCompatibleProvider::new_with_user_agent( + "Kimi Code", + "https://api.kimi.com/coding/v1", + key, + AuthStyle::Bearer, + "KimiCLI/0.77", + ), + )), "synthetic" => Ok(Box::new(OpenAiCompatibleProvider::new( "Synthetic", "https://api.synthetic.com", key, AuthStyle::Bearer, ))), @@ -787,6 +799,12 @@ pub fn list_providers() -> Vec { aliases: &["kimi"], local: false, }, + ProviderInfo { + name: "kimi-code", + display_name: "Kimi Code", + aliases: &["kimi_coding", "kimi_for_coding"], + local: false, + }, ProviderInfo { name: "synthetic", display_name: "Synthetic", @@ -1067,6 +1085,13 @@ mod tests { assert!(create_provider("kimi-cn", Some("key")).is_ok()); } + #[test] + fn factory_kimi_code() { + assert!(create_provider("kimi-code", Some("key")).is_ok()); + assert!(create_provider("kimi_coding", Some("key")).is_ok()); + assert!(create_provider("kimi_for_coding", Some("key")).is_ok()); + } + #[test] fn factory_synthetic() { assert!(create_provider("synthetic", Some("key")).is_ok()); @@ -1399,6 +1424,7 @@ mod tests { "cloudflare", "moonshot", "moonshot-intl", + "kimi-code", "moonshot-cn", "synthetic", "opencode",