feat(provider): add dedicated kimi-code provider support

This commit is contained in:
Chummy 2026-02-18 21:56:41 +08:00
parent ce104bed45
commit 88dcd17a30
3 changed files with 91 additions and 9 deletions

View file

@ -495,6 +495,7 @@ fn default_model_for_provider(provider: &str) -> String {
"groq" => "llama-3.3-70b-versatile".into(), "groq" => "llama-3.3-70b-versatile".into(),
"deepseek" => "deepseek-chat".into(), "deepseek" => "deepseek-chat".into(),
"gemini" => "gemini-2.5-pro".into(), "gemini" => "gemini-2.5-pro".into(),
"kimi-code" => "kimi-for-coding".into(),
_ => "anthropic/claude-sonnet-4.5".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"), ("bedrock", "Amazon Bedrock — AWS managed models"),
], ],
3 => vec![ 3 => vec![
(
"kimi-code",
"Kimi Code — coding-optimized Kimi API (KimiCLI)",
),
("moonshot", "Moonshot — Kimi API (China endpoint)"), ("moonshot", "Moonshot — Kimi API (China endpoint)"),
( (
"moonshot-intl", "moonshot-intl",
@ -4611,6 +4616,7 @@ mod tests {
assert_eq!(default_model_for_provider("zai-cn"), "glm-5"); 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("gemini"), "gemini-2.5-pro");
assert_eq!(default_model_for_provider("google"), "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!( assert_eq!(
default_model_for_provider("google-gemini"), default_model_for_provider("google-gemini"),
"gemini-2.5-pro" "gemini-2.5-pro"

View file

@ -8,7 +8,10 @@ use crate::providers::traits::{
}; };
use async_trait::async_trait; use async_trait::async_trait;
use futures_util::{stream, StreamExt}; use futures_util::{stream, StreamExt};
use reqwest::Client; use reqwest::{
header::{HeaderMap, HeaderValue, USER_AGENT},
Client,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// A provider that speaks the OpenAI-compatible chat completions API. /// 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. /// When false, do not fall back to /v1/responses on chat completions 404.
/// GLM/Zhipu does not support the responses API. /// GLM/Zhipu does not support the responses API.
supports_responses_fallback: bool, supports_responses_fallback: bool,
user_agent: Option<String>,
} }
/// How the provider expects the API key to be sent. /// How the provider expects the API key to be sent.
@ -42,13 +46,7 @@ impl OpenAiCompatibleProvider {
credential: Option<&str>, credential: Option<&str>,
auth_style: AuthStyle, auth_style: AuthStyle,
) -> Self { ) -> Self {
Self { Self::new_with_options(name, base_url, credential, auth_style, true, None)
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,
}
} }
/// Same as `new` but skips the /v1/responses fallback on 404. /// Same as `new` but skips the /v1/responses fallback on 404.
@ -58,17 +56,69 @@ impl OpenAiCompatibleProvider {
base_url: &str, base_url: &str,
credential: Option<&str>, credential: Option<&str>,
auth_style: AuthStyle, 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 {
Self { Self {
name: name.to_string(), name: name.to_string(),
base_url: base_url.trim_end_matches('/').to_string(), base_url: base_url.trim_end_matches('/').to_string(),
credential: credential.map(ToString::to_string), credential: credential.map(ToString::to_string),
auth_header: auth_style, auth_header: auth_style,
supports_responses_fallback: false, supports_responses_fallback,
user_agent: user_agent.map(ToString::to_string),
} }
} }
fn http_client(&self) -> Client { 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) crate::config::build_runtime_proxy_client_with_timeouts("provider.compatible", 120, 10)
} }

View file

@ -314,6 +314,9 @@ fn resolve_provider_credential(name: &str, credential_override: Option<&str>) ->
"perplexity" => vec!["PERPLEXITY_API_KEY"], "perplexity" => vec!["PERPLEXITY_API_KEY"],
"cohere" => vec!["COHERE_API_KEY"], "cohere" => vec!["COHERE_API_KEY"],
name if is_moonshot_alias(name) => vec!["MOONSHOT_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_glm_alias(name) => vec!["GLM_API_KEY"],
name if is_minimax_alias(name) => vec!["MINIMAX_API_KEY"], name if is_minimax_alias(name) => vec!["MINIMAX_API_KEY"],
name if is_qianfan_alias(name) => vec!["QIANFAN_API_KEY"], name if is_qianfan_alias(name) => vec!["QIANFAN_API_KEY"],
@ -432,6 +435,15 @@ pub fn create_provider_with_url(
key, key,
AuthStyle::Bearer, 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" => Ok(Box::new(OpenAiCompatibleProvider::new(
"Synthetic", "https://api.synthetic.com", key, AuthStyle::Bearer, "Synthetic", "https://api.synthetic.com", key, AuthStyle::Bearer,
))), ))),
@ -787,6 +799,12 @@ pub fn list_providers() -> Vec<ProviderInfo> {
aliases: &["kimi"], aliases: &["kimi"],
local: false, local: false,
}, },
ProviderInfo {
name: "kimi-code",
display_name: "Kimi Code",
aliases: &["kimi_coding", "kimi_for_coding"],
local: false,
},
ProviderInfo { ProviderInfo {
name: "synthetic", name: "synthetic",
display_name: "Synthetic", display_name: "Synthetic",
@ -1067,6 +1085,13 @@ mod tests {
assert!(create_provider("kimi-cn", Some("key")).is_ok()); 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] #[test]
fn factory_synthetic() { fn factory_synthetic() {
assert!(create_provider("synthetic", Some("key")).is_ok()); assert!(create_provider("synthetic", Some("key")).is_ok());
@ -1399,6 +1424,7 @@ mod tests {
"cloudflare", "cloudflare",
"moonshot", "moonshot",
"moonshot-intl", "moonshot-intl",
"kimi-code",
"moonshot-cn", "moonshot-cn",
"synthetic", "synthetic",
"opencode", "opencode",