From cb91a2f914ada63be9c4fc4a8bd8d31daa4d509e Mon Sep 17 00:00:00 2001 From: Youhana Sheriff Date: Wed, 18 Feb 2026 21:56:41 +0800 Subject: [PATCH] feat(provider): add dedicated kimi-code provider support --- src/providers/compatible.rs | 68 ++++++++++++++++++++++++++++++++----- src/providers/mod.rs | 26 ++++++++++++++ 2 files changed, 85 insertions(+), 9 deletions(-) diff --git a/src/providers/compatible.rs b/src/providers/compatible.rs index a1d498b..074ee45 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 f5bac00..c910059 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -497,6 +497,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_OAUTH_TOKEN_ENV, MINIMAX_API_KEY_ENV], name if is_qianfan_alias(name) => vec!["QIANFAN_API_KEY"], @@ -625,6 +628,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, ))), @@ -980,6 +992,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", @@ -1333,6 +1351,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()); @@ -1670,6 +1695,7 @@ mod tests { "cloudflare", "moonshot", "moonshot-intl", + "kimi-code", "moonshot-cn", "synthetic", "opencode",