From 77536dbe9c686047c5719d19c6921b386df488b0 Mon Sep 17 00:00:00 2001 From: Bram Ruttens Date: Mon, 9 Mar 2026 21:37:07 +0100 Subject: [PATCH] Add Kimi for Code provider support Adds support for the Kimi for Code API endpoint (https://api.kimi.com/coding/v1), an Anthropic-compatible coding agent API with 262K context window. Changes: - Add KIMI_CODING_BASE_URL constant in openfang-types - Add kimi_coding provider with Anthropic driver compatibility - Add kimi-for-coding model entry (262K context, 32K output) - Update tests and provider counts API Key: KIMI_API_KEY Usage: provider = "kimi_coding", model = "kimi-for-coding" --- crates/openfang-runtime/src/drivers/mod.rs | 44 +++++++++++++++--- crates/openfang-runtime/src/model_catalog.rs | 47 ++++++++++++++------ crates/openfang-types/src/model_catalog.rs | 1 + 3 files changed, 72 insertions(+), 20 deletions(-) diff --git a/crates/openfang-runtime/src/drivers/mod.rs b/crates/openfang-runtime/src/drivers/mod.rs index be20ee263..225f20c12 100644 --- a/crates/openfang-runtime/src/drivers/mod.rs +++ b/crates/openfang-runtime/src/drivers/mod.rs @@ -14,12 +14,11 @@ pub mod openai; use crate::llm_driver::{DriverConfig, LlmDriver, LlmError}; use openfang_types::model_catalog::{ AI21_BASE_URL, ANTHROPIC_BASE_URL, CEREBRAS_BASE_URL, COHERE_BASE_URL, DEEPSEEK_BASE_URL, - FIREWORKS_BASE_URL, GEMINI_BASE_URL, GROQ_BASE_URL, HUGGINGFACE_BASE_URL, LEMONADE_BASE_URL, - LMSTUDIO_BASE_URL, - MINIMAX_BASE_URL, MISTRAL_BASE_URL, MOONSHOT_BASE_URL, OLLAMA_BASE_URL, OPENAI_BASE_URL, - OPENROUTER_BASE_URL, PERPLEXITY_BASE_URL, QIANFAN_BASE_URL, QWEN_BASE_URL, - REPLICATE_BASE_URL, SAMBANOVA_BASE_URL, TOGETHER_BASE_URL, VENICE_BASE_URL, VLLM_BASE_URL, - VOLCENGINE_BASE_URL, VOLCENGINE_CODING_BASE_URL, XAI_BASE_URL, ZAI_BASE_URL, + FIREWORKS_BASE_URL, GEMINI_BASE_URL, GROQ_BASE_URL, HUGGINGFACE_BASE_URL, KIMI_CODING_BASE_URL, + LEMONADE_BASE_URL, LMSTUDIO_BASE_URL, MINIMAX_BASE_URL, MISTRAL_BASE_URL, MOONSHOT_BASE_URL, + OLLAMA_BASE_URL, OPENAI_BASE_URL, OPENROUTER_BASE_URL, PERPLEXITY_BASE_URL, QIANFAN_BASE_URL, + QWEN_BASE_URL, REPLICATE_BASE_URL, SAMBANOVA_BASE_URL, TOGETHER_BASE_URL, VENICE_BASE_URL, + VLLM_BASE_URL, VOLCENGINE_BASE_URL, VOLCENGINE_CODING_BASE_URL, XAI_BASE_URL, ZAI_BASE_URL, ZAI_CODING_BASE_URL, ZHIPU_BASE_URL, ZHIPU_CODING_BASE_URL, }; use std::sync::Arc; @@ -155,6 +154,11 @@ fn provider_defaults(provider: &str) -> Option { api_key_env: "MOONSHOT_API_KEY", key_required: true, }), + "kimi_coding" => Some(ProviderDefaults { + base_url: KIMI_CODING_BASE_URL, + api_key_env: "KIMI_API_KEY", + key_required: true, + }), "qwen" | "dashscope" | "model_studio" => Some(ProviderDefaults { base_url: QWEN_BASE_URL, api_key_env: "DASHSCOPE_API_KEY", @@ -251,6 +255,22 @@ pub fn create_driver(config: &DriverConfig) -> Result, LlmErr return Ok(Arc::new(anthropic::AnthropicDriver::new(api_key, base_url))); } + // Kimi for Code — Anthropic-compatible endpoint + if provider == "kimi_coding" { + let api_key = config + .api_key + .clone() + .or_else(|| std::env::var("KIMI_API_KEY").ok()) + .ok_or_else(|| { + LlmError::MissingApiKey("Set KIMI_API_KEY environment variable".to_string()) + })?; + let base_url = config + .base_url + .clone() + .unwrap_or_else(|| KIMI_CODING_BASE_URL.to_string()); + return Ok(Arc::new(anthropic::AnthropicDriver::new(api_key, base_url))); + } + // Gemini uses a different API format — special case if provider == "gemini" || provider == "google" { let api_key = config @@ -444,6 +464,7 @@ pub fn known_providers() -> &'static [&'static str] { "replicate", "github-copilot", "moonshot", + "kimi_coding", "qwen", "minimax", "zhipu", @@ -545,11 +566,12 @@ mod tests { assert!(providers.contains(&"minimax")); assert!(providers.contains(&"zhipu")); assert!(providers.contains(&"zhipu_coding")); + assert!(providers.contains(&"kimi_coding")); assert!(providers.contains(&"qianfan")); assert!(providers.contains(&"volcengine")); assert!(providers.contains(&"codex")); assert!(providers.contains(&"claude-code")); - assert_eq!(providers.len(), 31); + assert_eq!(providers.len(), 32); } #[test] @@ -635,6 +657,14 @@ mod tests { std::env::remove_var("NVIDIA_API_KEY"); } + #[test] + fn test_provider_defaults_kimi_coding() { + let d = provider_defaults("kimi_coding").unwrap(); + assert_eq!(d.base_url, "https://api.kimi.com/coding"); + assert_eq!(d.api_key_env, "KIMI_API_KEY"); + assert!(d.key_required); + } + #[test] fn test_custom_provider_explicit_key_with_url() { // When api_key is explicitly passed, it should be used regardless of env var. diff --git a/crates/openfang-runtime/src/model_catalog.rs b/crates/openfang-runtime/src/model_catalog.rs index 67dd66fd3..ba62ea592 100644 --- a/crates/openfang-runtime/src/model_catalog.rs +++ b/crates/openfang-runtime/src/model_catalog.rs @@ -7,12 +7,11 @@ use openfang_types::model_catalog::{ AuthStatus, ModelCatalogEntry, ModelTier, ProviderInfo, AI21_BASE_URL, ANTHROPIC_BASE_URL, BEDROCK_BASE_URL, CEREBRAS_BASE_URL, COHERE_BASE_URL, DEEPSEEK_BASE_URL, FIREWORKS_BASE_URL, GEMINI_BASE_URL, GITHUB_COPILOT_BASE_URL, GROQ_BASE_URL, HUGGINGFACE_BASE_URL, - LMSTUDIO_BASE_URL, MINIMAX_BASE_URL, MISTRAL_BASE_URL, MOONSHOT_BASE_URL, OLLAMA_BASE_URL, - LEMONADE_BASE_URL, OPENAI_BASE_URL, OPENROUTER_BASE_URL, PERPLEXITY_BASE_URL, - QIANFAN_BASE_URL, QWEN_BASE_URL, - REPLICATE_BASE_URL, SAMBANOVA_BASE_URL, TOGETHER_BASE_URL, VENICE_BASE_URL, VLLM_BASE_URL, - VOLCENGINE_BASE_URL, VOLCENGINE_CODING_BASE_URL, XAI_BASE_URL, ZAI_BASE_URL, - ZAI_CODING_BASE_URL, ZHIPU_BASE_URL, ZHIPU_CODING_BASE_URL, + KIMI_CODING_BASE_URL, LEMONADE_BASE_URL, LMSTUDIO_BASE_URL, MINIMAX_BASE_URL, MISTRAL_BASE_URL, + MOONSHOT_BASE_URL, OLLAMA_BASE_URL, OPENAI_BASE_URL, OPENROUTER_BASE_URL, PERPLEXITY_BASE_URL, + QIANFAN_BASE_URL, QWEN_BASE_URL, REPLICATE_BASE_URL, SAMBANOVA_BASE_URL, TOGETHER_BASE_URL, + VENICE_BASE_URL, VLLM_BASE_URL, VOLCENGINE_BASE_URL, VOLCENGINE_CODING_BASE_URL, XAI_BASE_URL, + ZAI_BASE_URL, ZAI_CODING_BASE_URL, ZHIPU_BASE_URL, ZHIPU_CODING_BASE_URL, }; use std::collections::HashMap; @@ -68,8 +67,7 @@ impl ModelCatalog { let has_fallback = match provider.id.as_str() { "gemini" => std::env::var("GOOGLE_API_KEY").is_ok(), "codex" => { - std::env::var("OPENAI_API_KEY").is_ok() - || read_codex_credential().is_some() + std::env::var("OPENAI_API_KEY").is_ok() || read_codex_credential().is_some() } "claude-code" => crate::drivers::claude_code::claude_code_available(), _ => false, @@ -666,6 +664,15 @@ fn builtin_providers() -> Vec { auth_status: AuthStatus::Missing, model_count: 0, }, + ProviderInfo { + id: "kimi_coding".into(), + display_name: "Kimi for Code".into(), + api_key_env: "KIMI_API_KEY".into(), + base_url: KIMI_CODING_BASE_URL.into(), + key_required: true, + auth_status: AuthStatus::Missing, + model_count: 0, + }, ProviderInfo { id: "qianfan".into(), display_name: "Baidu Qianfan".into(), @@ -2926,6 +2933,23 @@ fn builtin_models() -> Vec { aliases: vec!["kimi-k2.5".into()], }, // ══════════════════════════════════════════════════════════════ + // Kimi for Code (1) + // ══════════════════════════════════════════════════════════════ + ModelCatalogEntry { + id: "kimi-for-coding".into(), + display_name: "Kimi For Coding".into(), + provider: "kimi_coding".into(), + tier: ModelTier::Frontier, + context_window: 262_144, + max_output_tokens: 32_768, + input_cost_per_m: 0.0, // Subscription-based (Coding Plan) + output_cost_per_m: 0.0, // Subscription-based (Coding Plan) + supports_tools: true, + supports_vision: true, + supports_streaming: true, + aliases: vec![], + }, + // ══════════════════════════════════════════════════════════════ // Baidu Qianfan / ERNIE (3) // ══════════════════════════════════════════════════════════════ ModelCatalogEntry { @@ -3281,7 +3305,7 @@ mod tests { #[test] fn test_catalog_has_providers() { let catalog = ModelCatalog::new(); - assert_eq!(catalog.list_providers().len(), 36); + assert_eq!(catalog.list_providers().len(), 37); } #[test] @@ -3316,10 +3340,7 @@ mod tests { #[test] fn test_resolve_alias() { let catalog = ModelCatalog::new(); - assert_eq!( - catalog.resolve_alias("sonnet"), - Some("claude-sonnet-4-6") - ); + assert_eq!(catalog.resolve_alias("sonnet"), Some("claude-sonnet-4-6")); assert_eq!( catalog.resolve_alias("haiku"), Some("claude-haiku-4-5-20251001") diff --git a/crates/openfang-types/src/model_catalog.rs b/crates/openfang-types/src/model_catalog.rs index 92019518d..b0594b5ed 100644 --- a/crates/openfang-types/src/model_catalog.rs +++ b/crates/openfang-types/src/model_catalog.rs @@ -43,6 +43,7 @@ pub const ZHIPU_CODING_BASE_URL: &str = "https://open.bigmodel.cn/api/coding/paa pub const ZAI_BASE_URL: &str = "https://api.z.ai/api/paas/v4"; pub const ZAI_CODING_BASE_URL: &str = "https://api.z.ai/api/coding/paas/v4"; pub const MOONSHOT_BASE_URL: &str = "https://api.moonshot.cn/v1"; +pub const KIMI_CODING_BASE_URL: &str = "https://api.kimi.com/coding"; pub const QIANFAN_BASE_URL: &str = "https://qianfan.baidubce.com/v2"; pub const VOLCENGINE_BASE_URL: &str = "https://ark.cn-beijing.volces.com/api/v3"; pub const VOLCENGINE_CODING_BASE_URL: &str = "https://ark.cn-beijing.volces.com/api/coding/v3";