diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f21e957..2d0f05ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- **Kimi Code API-key setup.** `codewhale config set providers.moonshot.*` + now writes the Moonshot/Kimi provider table, and Kimi Code API-key + endpoints default to `kimi-for-coding` without using the Kimi CLI OAuth path. + ## [0.8.45] - 2026-05-25 ### Added @@ -17,9 +23,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Command palette voice input.** The command palette can launch a configured speech-to-text helper and show footer status while transcription runs (#2047). -- **Moonshot/Kimi OAuth provider.** Moonshot/Kimi is now a first-class - provider, including Kimi CLI OAuth reuse, secure refresh writes, model - completion, CLI auth, and secret-store integration. +- **Moonshot/Kimi provider.** Moonshot/Kimi is now a first-class provider, + including API-key auth, model completion, CLI auth, secret-store + integration, and optional Kimi CLI credential reuse. - **Deterministic whale-species sub-agent names.** Sub-agents now get stable, human-readable whale-species nicknames (e.g. "Beluga", "Orca") while preserving the raw agent ID in the popup (#2035, #2016). diff --git a/README.md b/README.md index d1dd66a1..450ca64d 100644 --- a/README.md +++ b/README.md @@ -314,15 +314,23 @@ codewhale --provider novita --model deepseek/deepseek-v4-pro codewhale auth set --provider fireworks --api-key "YOUR_FIREWORKS_API_KEY" codewhale --provider fireworks --model deepseek-v4-pro -# Moonshot/Kimi -codewhale auth set --provider moonshot --api-key "YOUR_MOONSHOT_OR_KIMI_API_KEY" -codewhale --provider moonshot --model kimi-k2.6 +# Kimi Code plan API key +codewhale auth set --provider moonshot --api-key "YOUR_KIMI_CODE_API_KEY" +codewhale config set providers.moonshot.auth_mode api_key +codewhale config set providers.moonshot.base_url https://api.kimi.com/coding/v1 +codewhale config set providers.moonshot.model kimi-for-coding +codewhale --provider moonshot -# Moonshot/Kimi with Kimi CLI OAuth -kimi login -mkdir -p ~/.deepseek -printf 'provider = "moonshot"\n\n[providers.moonshot]\nauth_mode = "kimi_oauth"\n' >> ~/.deepseek/config.toml -codewhale --provider moonshot --model kimi-for-coding +# Kimi/Moonshot Platform API key +codewhale auth set --provider moonshot --api-key "YOUR_MOONSHOT_OR_KIMI_API_KEY" +codewhale config set providers.moonshot.auth_mode api_key +codewhale config set providers.moonshot.base_url https://api.moonshot.ai/v1 +codewhale config set providers.moonshot.model kimi-k2.6 +codewhale --provider moonshot + +# Kimi through OpenRouter's catalog +codewhale auth set --provider openrouter --api-key "YOUR_OPENROUTER_API_KEY" +codewhale --provider openrouter --model moonshotai/kimi-k2.6 # Self-hosted SGLang SGLANG_BASE_URL="http://localhost:30000/v1" codewhale --provider sglang --model deepseek-v4-flash @@ -512,7 +520,7 @@ Key environment variables: | `OPENAI_BASE_URL` / `OPENAI_MODEL` | Generic OpenAI-compatible endpoint and model ID | | `ATLASCLOUD_BASE_URL` / `ATLASCLOUD_MODEL` | AtlasCloud endpoint and model override | | `WANJIE_ARK_BASE_URL` / `WANJIE_BASE_URL` / `WANJIE_MAAS_BASE_URL` / `WANJIE_ARK_MODEL` / `WANJIE_MODEL` / `WANJIE_MAAS_MODEL` | Wanjie Ark endpoint and model override | -| `MOONSHOT_BASE_URL` / `KIMI_BASE_URL` / `MOONSHOT_MODEL` / `KIMI_MODEL_NAME` / `KIMI_MODEL` | Moonshot/Kimi endpoint and model override | +| `MOONSHOT_BASE_URL` / `KIMI_BASE_URL` / `MOONSHOT_MODEL` / `KIMI_MODEL_NAME` / `KIMI_MODEL` | Moonshot/Kimi endpoint and model override. For a Kimi Code plan API key, use `KIMI_BASE_URL=https://api.kimi.com/coding/v1` and `KIMI_MODEL=kimi-for-coding`. | | `OPENROUTER_BASE_URL` | OpenRouter endpoint override | | `NOVITA_BASE_URL` | Novita endpoint override | | `FIREWORKS_BASE_URL` | Fireworks endpoint override | diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 576de517..d9d72864 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -462,6 +462,13 @@ impl ConfigToml { "providers.fireworks.http_headers" => { serialize_http_headers(&self.providers.fireworks.http_headers) } + "providers.moonshot.api_key" => self.providers.moonshot.api_key.clone(), + "providers.moonshot.base_url" => self.providers.moonshot.base_url.clone(), + "providers.moonshot.model" => self.providers.moonshot.model.clone(), + "providers.moonshot.auth_mode" => self.providers.moonshot.auth_mode.clone(), + "providers.moonshot.http_headers" => { + serialize_http_headers(&self.providers.moonshot.http_headers) + } "providers.sglang.api_key" => self.providers.sglang.api_key.clone(), "providers.sglang.base_url" => self.providers.sglang.base_url.clone(), "providers.sglang.model" => self.providers.sglang.model.clone(), @@ -612,6 +619,21 @@ impl ConfigToml { "providers.fireworks.http_headers" => { self.providers.fireworks.http_headers = parse_http_headers(value)?; } + "providers.moonshot.api_key" => { + self.providers.moonshot.api_key = Some(value.to_string()); + } + "providers.moonshot.base_url" => { + self.providers.moonshot.base_url = Some(value.to_string()); + } + "providers.moonshot.model" => { + self.providers.moonshot.model = Some(value.to_string()); + } + "providers.moonshot.auth_mode" => { + self.providers.moonshot.auth_mode = Some(value.to_string()); + } + "providers.moonshot.http_headers" => { + self.providers.moonshot.http_headers = parse_http_headers(value)?; + } "providers.sglang.api_key" => { self.providers.sglang.api_key = Some(value.to_string()); } @@ -716,6 +738,11 @@ impl ConfigToml { "providers.fireworks.base_url" => self.providers.fireworks.base_url = None, "providers.fireworks.model" => self.providers.fireworks.model = None, "providers.fireworks.http_headers" => self.providers.fireworks.http_headers.clear(), + "providers.moonshot.api_key" => self.providers.moonshot.api_key = None, + "providers.moonshot.base_url" => self.providers.moonshot.base_url = None, + "providers.moonshot.model" => self.providers.moonshot.model = None, + "providers.moonshot.auth_mode" => self.providers.moonshot.auth_mode = None, + "providers.moonshot.http_headers" => self.providers.moonshot.http_headers.clear(), "providers.sglang.api_key" => self.providers.sglang.api_key = None, "providers.sglang.base_url" => self.providers.sglang.base_url = None, "providers.sglang.model" => self.providers.sglang.model = None, @@ -869,6 +896,21 @@ impl ConfigToml { if let Some(v) = serialize_http_headers(&self.providers.fireworks.http_headers) { out.insert("providers.fireworks.http_headers".to_string(), v); } + if let Some(v) = self.providers.moonshot.api_key.as_ref() { + out.insert("providers.moonshot.api_key".to_string(), redact_secret(v)); + } + if let Some(v) = self.providers.moonshot.base_url.as_ref() { + out.insert("providers.moonshot.base_url".to_string(), v.clone()); + } + if let Some(v) = self.providers.moonshot.model.as_ref() { + out.insert("providers.moonshot.model".to_string(), v.clone()); + } + if let Some(v) = self.providers.moonshot.auth_mode.as_ref() { + out.insert("providers.moonshot.auth_mode".to_string(), v.clone()); + } + if let Some(v) = serialize_http_headers(&self.providers.moonshot.http_headers) { + out.insert("providers.moonshot.http_headers".to_string(), v); + } if let Some(v) = self.providers.sglang.api_key.as_ref() { out.insert("providers.sglang.api_key".to_string(), redact_secret(v)); } @@ -1028,7 +1070,8 @@ impl ConfigToml { .or_else(|| self.model.clone()) .unwrap_or_else(|| { if provider == ProviderKind::Moonshot - && auth_mode.as_deref().is_some_and(auth_mode_uses_kimi_oauth) + && (auth_mode.as_deref().is_some_and(auth_mode_uses_kimi_oauth) + || moonshot_base_url_uses_kimi_code(&base_url)) { DEFAULT_KIMI_CODE_MODEL.to_string() } else { @@ -1257,6 +1300,13 @@ fn default_base_url_for_provider(provider: ProviderKind) -> &'static str { } } +fn moonshot_base_url_uses_kimi_code(base_url: &str) -> bool { + let normalized = base_url.trim_end_matches('/').to_ascii_lowercase(); + normalized == DEFAULT_KIMI_CODE_BASE_URL + || normalized == "https://api.kimi.com/coding" + || normalized.starts_with("https://api.kimi.com/coding/") +} + fn base_url_is_custom_for_provider(provider: ProviderKind, base_url: &str) -> bool { let actual = base_url.trim_end_matches('/'); let default = default_base_url_for_provider(provider).trim_end_matches('/'); @@ -2358,6 +2408,52 @@ mod tests { ); } + #[test] + fn moonshot_provider_config_values_round_trip() -> Result<()> { + let mut config = ConfigToml::default(); + + config.set_value("providers.moonshot.api_key", "moonshot-secret-value")?; + config.set_value("providers.moonshot.base_url", DEFAULT_KIMI_CODE_BASE_URL)?; + config.set_value("providers.moonshot.model", DEFAULT_KIMI_CODE_MODEL)?; + config.set_value("providers.moonshot.auth_mode", "api_key")?; + config.set_value("providers.moonshot.http_headers", "X-Test=ok")?; + + assert_eq!( + config + .get_display_value("providers.moonshot.api_key") + .as_deref(), + Some("moon***alue") + ); + assert_eq!( + config.get_value("providers.moonshot.base_url").as_deref(), + Some(DEFAULT_KIMI_CODE_BASE_URL) + ); + assert_eq!( + config.get_value("providers.moonshot.model").as_deref(), + Some(DEFAULT_KIMI_CODE_MODEL) + ); + assert_eq!( + config.get_value("providers.moonshot.auth_mode").as_deref(), + Some("api_key") + ); + assert_eq!( + config + .list_values() + .get("providers.moonshot.api_key") + .map(String::as_str), + Some("moon***alue") + ); + + config.unset_value("providers.moonshot.auth_mode")?; + config.unset_value("providers.moonshot.base_url")?; + config.unset_value("providers.moonshot.model")?; + + assert_eq!(config.get_value("providers.moonshot.auth_mode"), None); + assert_eq!(config.get_value("providers.moonshot.base_url"), None); + assert_eq!(config.get_value("providers.moonshot.model"), None); + Ok(()) + } + #[test] fn project_merge_denies_credentials_endpoints_and_provider_selection() { let mut base = ConfigToml { @@ -2637,6 +2733,30 @@ mod tests { assert_eq!(resolved.api_key_source, None); } + #[test] + fn moonshot_kimi_code_api_key_endpoint_defaults_to_kimi_for_coding() { + let _lock = env_lock(); + let _env = EnvGuard::without_deepseek_runtime_overrides(); + let mut config = ConfigToml { + provider: ProviderKind::Moonshot, + ..ConfigToml::default() + }; + config.providers.moonshot.api_key = Some("kimi-code-key".to_string()); + config.providers.moonshot.base_url = Some(DEFAULT_KIMI_CODE_BASE_URL.to_string()); + + let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default()); + + assert_eq!(resolved.provider, ProviderKind::Moonshot); + assert_eq!(resolved.auth_mode, None); + assert_eq!(resolved.base_url, DEFAULT_KIMI_CODE_BASE_URL); + assert_eq!(resolved.model, DEFAULT_KIMI_CODE_MODEL); + assert_eq!(resolved.api_key.as_deref(), Some("kimi-code-key")); + assert_eq!( + resolved.api_key_source, + Some(RuntimeApiKeySource::ConfigFile) + ); + } + #[test] fn wanjie_ark_provider_defaults_to_openai_compatible_endpoint_and_model() { let _lock = env_lock(); diff --git a/crates/tui/CHANGELOG.md b/crates/tui/CHANGELOG.md index 7f21e957..2d0f05ae 100644 --- a/crates/tui/CHANGELOG.md +++ b/crates/tui/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- **Kimi Code API-key setup.** `codewhale config set providers.moonshot.*` + now writes the Moonshot/Kimi provider table, and Kimi Code API-key + endpoints default to `kimi-for-coding` without using the Kimi CLI OAuth path. + ## [0.8.45] - 2026-05-25 ### Added @@ -17,9 +23,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Command palette voice input.** The command palette can launch a configured speech-to-text helper and show footer status while transcription runs (#2047). -- **Moonshot/Kimi OAuth provider.** Moonshot/Kimi is now a first-class - provider, including Kimi CLI OAuth reuse, secure refresh writes, model - completion, CLI auth, and secret-store integration. +- **Moonshot/Kimi provider.** Moonshot/Kimi is now a first-class provider, + including API-key auth, model completion, CLI auth, secret-store + integration, and optional Kimi CLI credential reuse. - **Deterministic whale-species sub-agent names.** Sub-agents now get stable, human-readable whale-species nicknames (e.g. "Beluga", "Orca") while preserving the raw agent ID in the popup (#2035, #2016). diff --git a/crates/tui/src/config.rs b/crates/tui/src/config.rs index ce15b29e..78975ee3 100644 --- a/crates/tui/src/config.rs +++ b/crates/tui/src/config.rs @@ -1570,11 +1570,17 @@ impl Config { { return model_for_provider(provider, normalized); } - if provider == ApiProvider::Moonshot - && self - .provider_config() - .is_some_and(provider_config_uses_kimi_oauth) - { + let moonshot_config = (provider == ApiProvider::Moonshot) + .then(|| self.provider_config()) + .flatten(); + let moonshot_uses_kimi_code = moonshot_config.is_some_and(|config| { + provider_config_uses_kimi_oauth(config) + || config + .base_url + .as_deref() + .is_some_and(moonshot_base_url_uses_kimi_code) + }); + if moonshot_uses_kimi_code { return DEFAULT_KIMI_CODE_MODEL.to_string(); } @@ -1771,8 +1777,9 @@ impl Config { ), ApiProvider::Moonshot => anyhow::bail!( "Moonshot/Kimi API key not found. Run 'codewhale auth set --provider moonshot', \ - set MOONSHOT_API_KEY/KIMI_API_KEY, add [providers.moonshot] api_key, \ - or run `kimi login` and set [providers.moonshot] auth_mode = \"kimi_oauth\"." + set MOONSHOT_API_KEY/KIMI_API_KEY, or add [providers.moonshot] api_key. \ + For a Kimi Code plan key, set [providers.moonshot] base_url = \ + \"https://api.kimi.com/coding/v1\" and model = \"kimi-for-coding\"." ), // Self-hosted deployments commonly run without auth on localhost. // Return an empty key and let the client omit the Authorization header. @@ -2880,6 +2887,13 @@ fn provider_preserves_custom_base_url_model(provider: ApiProvider, base_url: &st base_url_is_custom_for_provider(provider, base_url) } +fn moonshot_base_url_uses_kimi_code(base_url: &str) -> bool { + let normalized = normalize_base_url(base_url).to_ascii_lowercase(); + normalized == DEFAULT_KIMI_CODE_BASE_URL + || normalized == "https://api.kimi.com/coding" + || normalized.starts_with("https://api.kimi.com/coding/") +} + fn provider_config_uses_kimi_oauth(config: &ProviderConfig) -> bool { config .auth_mode @@ -6434,6 +6448,42 @@ api_key = "stale-api-key" Ok(()) } + #[test] + fn moonshot_kimi_code_api_key_uses_coding_model() -> Result<()> { + let _lock = lock_test_env(); + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let temp_root = env::temp_dir().join(format!( + "codewhale-tui-kimi-code-key-{}-{}", + std::process::id(), + nanos + )); + fs::create_dir_all(&temp_root)?; + let _guard = EnvGuard::new(&temp_root); + + let config_path = temp_root.join(".deepseek").join("config.toml"); + ensure_parent_dir(&config_path)?; + fs::write( + &config_path, + r#"provider = "moonshot" + +[providers.moonshot] +api_key = "kimi-code-key" +base_url = "https://api.kimi.com/coding/v1" +"#, + )?; + + let config = Config::load(None, None)?; + assert_eq!(config.api_provider(), ApiProvider::Moonshot); + assert_eq!(config.deepseek_base_url(), DEFAULT_KIMI_CODE_BASE_URL); + assert_eq!(config.default_model(), DEFAULT_KIMI_CODE_MODEL); + assert_eq!(config.deepseek_api_key()?, "kimi-code-key"); + assert!(has_api_key_for(&config, ApiProvider::Moonshot)); + Ok(()) + } + #[test] fn has_api_key_for_detects_env_and_config_per_provider() -> Result<()> { let _lock = lock_test_env(); diff --git a/web/app/[locale]/docs/page.tsx b/web/app/[locale]/docs/page.tsx index 78be0f71..5a639add 100644 --- a/web/app/[locale]/docs/page.tsx +++ b/web/app/[locale]/docs/page.tsx @@ -267,6 +267,11 @@ command = "~/.deepseek/hooks/pre.sh" # / message_submit / mode_change /
开放模型平台方向:CodeWhale 保持 DeepSeek 优先,同时内置 Moonshot/Kimi、OpenRouter、NVIDIA NIM、
AtlasCloud、Wanjie Ark、Novita、Fireworks 和自托管 SGLang/vLLM/Ollama 路径。
+ Kimi Code 会员 API Key 使用 providers.moonshot.base_url
+ 指向 https://api.kimi.com/coding/v1,模型为
+ kimi-for-coding;Kimi/Moonshot 平台 API Key 继续使用
+ https://api.moonshot.ai/v1 和
+ kimi-k2.6。
providers.moonshot.base_url
+ set to https://api.kimi.com/coding/v1 with
+ kimi-for-coding; Kimi/Moonshot Platform API keys use
+ https://api.moonshot.ai/v1 with
+ kimi-k2.6.
diff --git a/web/app/[locale]/faq/page.tsx b/web/app/[locale]/faq/page.tsx
index ec5f7dcd..2d9db748 100644
--- a/web/app/[locale]/faq/page.tsx
+++ b/web/app/[locale]/faq/page.tsx
@@ -112,7 +112,7 @@ codewhale doctor # full connectivity check`}
CodeWhale ships with these built-in providers:
CodeWhale 内建以下提供商: