From c47ed896dc2ccb8145ad6f35b8d06864f1e3076c Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Tue, 26 May 2026 06:03:10 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20DeepSeek-first=20v0.8.45=20=E2=80=94=20C?= =?UTF-8?q?ODEWHALE=5F*=20env=20aliases,=20remove=20public=20Kimi/Moonshot?= =?UTF-8?q?=20promotion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #2164 (superseded). --- CHANGELOG.md | 19 ++- README.md | 41 ++---- crates/config/src/lib.rs | 183 +++++++++++++++++++++++++- crates/tui/CHANGELOG.md | 19 ++- crates/tui/src/config.rs | 269 ++++++++++++++++++++++++++++++++++++--- docs/CONFIGURATION.md | 32 +++-- 6 files changed, 494 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d0f05ae..7b8fee06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Fixed +### Added -- **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. +- **`CODEWHALE_*` env aliases.** `CODEWHALE_PROVIDER`, `CODEWHALE_MODEL`, + and `CODEWHALE_BASE_URL` are public product-scoped aliases that take + precedence over the legacy `DEEPSEEK_*` forms. The `DEEPSEEK_*` names + remain accepted for back-compat. Recommended setup paths are + `codewhale --provider `, `provider = ""` in + `~/.codewhale/config.toml`, or `CODEWHALE_PROVIDER=`. + +### Changed + +- **DeepSeek-first focus.** v0.8.45.x refocuses on delivering the + highest-quality experience on DeepSeek first. The project's broader + goal remains to become a strong harness for open-source and open-weight + coding models, but additional first-class provider paths are planned + for v0.9.0 after the core DeepSeek workflow is solid. ## [0.8.45] - 2026-05-25 diff --git a/README.md b/README.md index 450ca64d..b5e27147 100644 --- a/README.md +++ b/README.md @@ -275,11 +275,12 @@ Both binaries are required. Cross-compilation and platform-specific notes: [docs ### Providers -Official DeepSeek remains the default and first-class path. v0.8.45 supports -all 12 provider IDs in this order: `deepseek`, `nvidia-nim`, `openai`, -`atlascloud`, `wanjie-ark`, `openrouter`, `novita`, `fireworks`, `moonshot`, -`sglang`, `vllm`, and `ollama`. Other providers are additive, with OpenRouter -starting from DeepSeek Pro/Flash before broader open-model catalogs are enabled. +CodeWhale v0.8.45 focuses on delivering the highest-quality experience on +DeepSeek first. The project's broader goal remains to become a strong harness +for open-source and open-weight coding models — additional first-class +provider paths are planned for v0.9.0. Backend provider infrastructure for +other OpenAI-compatible endpoints and self-hosted runtimes is available under +the same `--provider` flag for advanced users who need it today. ```bash # DeepSeek (default) @@ -314,24 +315,6 @@ 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 -# 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 - -# 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 @@ -506,21 +489,23 @@ Key environment variables: | Variable | Purpose | |---|---| +| `CODEWHALE_PROVIDER` | Active provider. Public alias for `DEEPSEEK_PROVIDER`; wins when both are set. | +| `CODEWHALE_MODEL` | Default model for the active provider. Public alias for `DEEPSEEK_MODEL`. | +| `CODEWHALE_BASE_URL` | Base URL for the active provider. Public alias for `DEEPSEEK_BASE_URL`. | | `DEEPSEEK_API_KEY` | API key | -| `DEEPSEEK_BASE_URL` | API base URL | +| `DEEPSEEK_BASE_URL` | API base URL (legacy alias of `CODEWHALE_BASE_URL`) | | `DEEPSEEK_HTTP_HEADERS` | Optional custom model request headers, e.g. `X-Model-Provider-Id=your-model-provider` | -| `DEEPSEEK_MODEL` | Default model | +| `DEEPSEEK_MODEL` | Default model (legacy alias of `CODEWHALE_MODEL`) | | `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` | Stream idle timeout in seconds, default `300`, clamped to `1..=3600` | -| `DEEPSEEK_PROVIDER` | `deepseek` (default), `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `openrouter`, `novita`, `fireworks`, `moonshot`, `sglang`, `vllm`, `ollama` | +| `DEEPSEEK_PROVIDER` | Legacy alias of `CODEWHALE_PROVIDER`. Accepts `deepseek` (default), `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `openrouter`, `novita`, `fireworks`, `sglang`, `vllm`, `ollama`. | | `DEEPSEEK_PROFILE` | Config profile name | | `DEEPSEEK_MEMORY` | Set to `on` to enable user memory | | `DEEPSEEK_ALLOW_INSECURE_HTTP=1` | Allow non-local `http://` API base URLs on trusted networks | -| `NVIDIA_API_KEY` / `NVIDIA_NIM_API_KEY` / `OPENAI_API_KEY` / `ATLASCLOUD_API_KEY` / `WANJIE_ARK_API_KEY` / `WANJIE_API_KEY` / `WANJIE_MAAS_API_KEY` / `OPENROUTER_API_KEY` / `NOVITA_API_KEY` / `FIREWORKS_API_KEY` / `MOONSHOT_API_KEY` / `KIMI_API_KEY` / `SGLANG_API_KEY` / `VLLM_API_KEY` / `OLLAMA_API_KEY` | Provider auth | +| `NVIDIA_API_KEY` / `NVIDIA_NIM_API_KEY` / `OPENAI_API_KEY` / `ATLASCLOUD_API_KEY` / `WANJIE_ARK_API_KEY` / `WANJIE_API_KEY` / `WANJIE_MAAS_API_KEY` / `OPENROUTER_API_KEY` / `NOVITA_API_KEY` / `FIREWORKS_API_KEY` / `SGLANG_API_KEY` / `VLLM_API_KEY` / `OLLAMA_API_KEY` | Provider auth | | `NVIDIA_NIM_BASE_URL` / `NIM_BASE_URL` / `NVIDIA_BASE_URL` | NVIDIA NIM endpoint override | | `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. 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 d9d72864..039c28c7 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -1787,10 +1787,15 @@ struct EnvRuntimeOverrides { impl EnvRuntimeOverrides { fn load() -> Self { Self { - provider: std::env::var("DEEPSEEK_PROVIDER") + provider: std::env::var("CODEWHALE_PROVIDER") + .or_else(|_| std::env::var("DEEPSEEK_PROVIDER")) .ok() .and_then(|v| ProviderKind::parse(&v)), - model: std::env::var("DEEPSEEK_MODEL").ok(), + model: std::env::var("CODEWHALE_MODEL") + .or_else(|_| std::env::var("DEEPSEEK_MODEL")) + .or_else(|_| std::env::var("DEEPSEEK_DEFAULT_TEXT_MODEL")) + .ok() + .filter(|v| !v.trim().is_empty()), wanjie_ark_model: std::env::var("WANJIE_ARK_MODEL") .or_else(|_| std::env::var("WANJIE_MODEL")) .or_else(|_| std::env::var("WANJIE_MAAS_MODEL")) @@ -1816,7 +1821,8 @@ impl EnvRuntimeOverrides { .ok() .and_then(|value| parse_http_headers(&value).ok()) .filter(|headers| !headers.is_empty()), - deepseek_base_url: std::env::var("DEEPSEEK_BASE_URL") + deepseek_base_url: std::env::var("CODEWHALE_BASE_URL") + .or_else(|_| std::env::var("DEEPSEEK_BASE_URL")) .ok() .filter(|v| !v.trim().is_empty()), nvidia_base_url: std::env::var("NVIDIA_NIM_BASE_URL") @@ -1921,6 +1927,7 @@ mod tests { deepseek_base_url: Option, deepseek_http_headers: Option, deepseek_model: Option, + deepseek_default_text_model: Option, deepseek_provider: Option, deepseek_auth_mode: Option, nvidia_api_key: Option, @@ -1954,6 +1961,9 @@ mod tests { vllm_base_url: Option, ollama_api_key: Option, ollama_base_url: Option, + codewhale_provider: Option, + codewhale_model: Option, + codewhale_base_url: Option, } impl EnvGuard { @@ -1963,8 +1973,12 @@ mod tests { deepseek_base_url: env::var_os("DEEPSEEK_BASE_URL"), deepseek_http_headers: env::var_os("DEEPSEEK_HTTP_HEADERS"), deepseek_model: env::var_os("DEEPSEEK_MODEL"), + deepseek_default_text_model: env::var_os("DEEPSEEK_DEFAULT_TEXT_MODEL"), deepseek_provider: env::var_os("DEEPSEEK_PROVIDER"), deepseek_auth_mode: env::var_os("DEEPSEEK_AUTH_MODE"), + codewhale_provider: env::var_os("CODEWHALE_PROVIDER"), + codewhale_model: env::var_os("CODEWHALE_MODEL"), + codewhale_base_url: env::var_os("CODEWHALE_BASE_URL"), nvidia_api_key: env::var_os("NVIDIA_API_KEY"), nvidia_nim_api_key: env::var_os("NVIDIA_NIM_API_KEY"), nim_base_url: env::var_os("NIM_BASE_URL"), @@ -2003,8 +2017,12 @@ mod tests { env::remove_var("DEEPSEEK_BASE_URL"); env::remove_var("DEEPSEEK_HTTP_HEADERS"); env::remove_var("DEEPSEEK_MODEL"); + env::remove_var("DEEPSEEK_DEFAULT_TEXT_MODEL"); env::remove_var("DEEPSEEK_PROVIDER"); env::remove_var("DEEPSEEK_AUTH_MODE"); + env::remove_var("CODEWHALE_PROVIDER"); + env::remove_var("CODEWHALE_MODEL"); + env::remove_var("CODEWHALE_BASE_URL"); env::remove_var("NVIDIA_API_KEY"); env::remove_var("NVIDIA_NIM_API_KEY"); env::remove_var("NIM_BASE_URL"); @@ -2057,8 +2075,15 @@ mod tests { Self::restore_var("DEEPSEEK_BASE_URL", self.deepseek_base_url.take()); Self::restore_var("DEEPSEEK_HTTP_HEADERS", self.deepseek_http_headers.take()); Self::restore_var("DEEPSEEK_MODEL", self.deepseek_model.take()); + Self::restore_var( + "DEEPSEEK_DEFAULT_TEXT_MODEL", + self.deepseek_default_text_model.take(), + ); Self::restore_var("DEEPSEEK_PROVIDER", self.deepseek_provider.take()); Self::restore_var("DEEPSEEK_AUTH_MODE", self.deepseek_auth_mode.take()); + Self::restore_var("CODEWHALE_PROVIDER", self.codewhale_provider.take()); + Self::restore_var("CODEWHALE_MODEL", self.codewhale_model.take()); + Self::restore_var("CODEWHALE_BASE_URL", self.codewhale_base_url.take()); Self::restore_var("NVIDIA_API_KEY", self.nvidia_api_key.take()); Self::restore_var("NVIDIA_NIM_API_KEY", self.nvidia_nim_api_key.take()); Self::restore_var("NIM_BASE_URL", self.nim_base_url.take()); @@ -2408,6 +2433,55 @@ mod tests { ); } + /// End-to-end smoke for the preferred Kimi Code setup path: + /// 1. Start from a fresh root config that uses DeepSeek defaults. + /// 2. Mutate it through the same key-value setters the + /// `codewhale config set providers.moonshot.*` CLI invokes. + /// 3. Switch the active provider through `CODEWHALE_PROVIDER` — + /// the public env alias — without ever touching the legacy + /// `DEEPSEEK_PROVIDER` name. + /// 4. Resolve the runtime and confirm the doctor/runtime values. + /// + /// No real API key is required; the `api_key` here is just a + /// non-empty placeholder. + #[test] + fn moonshot_kimi_code_smoke_config_set_then_resolve() -> Result<()> { + let _lock = env_lock(); + let _env = EnvGuard::without_deepseek_runtime_overrides(); + + let mut config = ConfigToml { + provider: ProviderKind::Deepseek, + default_text_model: Some("deepseek-v4-pro".to_string()), + ..ConfigToml::default() + }; + + // Same key paths a user would run via `codewhale config set`. + config.set_value("providers.moonshot.api_key", "kimi-code-key-placeholder")?; + config.set_value("providers.moonshot.auth_mode", "api_key")?; + config.set_value("providers.moonshot.base_url", DEFAULT_KIMI_CODE_BASE_URL)?; + config.set_value("providers.moonshot.model", DEFAULT_KIMI_CODE_MODEL)?; + + // Public env alias for the active-provider switch. + // Safety: test-only env mutation guarded by env_lock(). + unsafe { env::set_var("CODEWHALE_PROVIDER", "moonshot") }; + + let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default()); + + assert_eq!(resolved.provider, ProviderKind::Moonshot); + assert_eq!(resolved.base_url, DEFAULT_KIMI_CODE_BASE_URL); + assert_eq!(resolved.model, DEFAULT_KIMI_CODE_MODEL); + assert_eq!(resolved.auth_mode.as_deref(), Some("api_key")); + assert_eq!( + resolved.api_key.as_deref(), + Some("kimi-code-key-placeholder") + ); + assert_eq!( + resolved.api_key_source, + Some(RuntimeApiKeySource::ConfigFile) + ); + Ok(()) + } + #[test] fn moonshot_provider_config_values_round_trip() -> Result<()> { let mut config = ConfigToml::default(); @@ -2757,6 +2831,109 @@ mod tests { ); } + /// `CODEWHALE_PROVIDER` is the user-facing env alias for switching the + /// active provider. It must be honored by the runtime resolver and win + /// over a root `provider = "deepseek"` config entry. + #[test] + fn codewhale_provider_env_switches_active_provider() { + let _lock = env_lock(); + let _env = EnvGuard::without_deepseek_runtime_overrides(); + // Safety: test-only env mutation guarded by env_lock(). + unsafe { + env::set_var("CODEWHALE_PROVIDER", "moonshot"); + } + let mut config = ConfigToml { + provider: ProviderKind::Deepseek, + ..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.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")); + } + + /// When both `CODEWHALE_PROVIDER` and the legacy `DEEPSEEK_PROVIDER` + /// are set, the public alias wins — a user adopting `CODEWHALE_*` in a + /// fresh shell config is not tripped up by a stale legacy export still + /// living in their dotfiles. + #[test] + fn codewhale_provider_env_wins_over_deepseek_provider_env() { + let _lock = env_lock(); + let _env = EnvGuard::without_deepseek_runtime_overrides(); + // Safety: test-only env mutation guarded by env_lock(). + unsafe { + env::set_var("CODEWHALE_PROVIDER", "moonshot"); + env::set_var("DEEPSEEK_PROVIDER", "openrouter"); + } + let config = ConfigToml { + provider: ProviderKind::Deepseek, + ..ConfigToml::default() + }; + + let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default()); + + assert_eq!(resolved.provider, ProviderKind::Moonshot); + } + + /// `CODEWHALE_MODEL` is the user-facing env alias for picking a model + /// against the active provider. It must be honored by the runtime + /// resolver in place of `DEEPSEEK_MODEL`. + #[test] + fn codewhale_model_env_alias_overrides_default_for_active_provider() { + let _lock = env_lock(); + let _env = EnvGuard::without_deepseek_runtime_overrides(); + // Safety: test-only env mutation guarded by env_lock(). + unsafe { + env::set_var("CODEWHALE_PROVIDER", "moonshot"); + env::set_var("CODEWHALE_MODEL", "custom-kimi-test-model"); + } + let config = ConfigToml::default(); + + let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default()); + + assert_eq!(resolved.provider, ProviderKind::Moonshot); + assert_eq!(resolved.model, "custom-kimi-test-model"); + } + + #[test] + fn blank_codewhale_model_env_alias_does_not_override_default_for_active_provider() { + let _lock = env_lock(); + let _env = EnvGuard::without_deepseek_runtime_overrides(); + // Safety: test-only env mutation guarded by env_lock(). + unsafe { + env::set_var("CODEWHALE_PROVIDER", "moonshot"); + env::set_var("CODEWHALE_MODEL", " "); + } + let config = ConfigToml::default(); + + let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default()); + + assert_eq!(resolved.provider, ProviderKind::Moonshot); + assert_eq!(resolved.model, DEFAULT_MOONSHOT_MODEL); + } + + #[test] + fn deepseek_default_text_model_legacy_alias_still_overrides_active_provider_model() { + let _lock = env_lock(); + let _env = EnvGuard::without_deepseek_runtime_overrides(); + // Safety: test-only env mutation guarded by env_lock(). + unsafe { + env::set_var("CODEWHALE_PROVIDER", "moonshot"); + env::set_var("DEEPSEEK_DEFAULT_TEXT_MODEL", "legacy-env-model"); + } + let config = ConfigToml::default(); + + let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default()); + + assert_eq!(resolved.provider, ProviderKind::Moonshot); + assert_eq!(resolved.model, "legacy-env-model"); + } + #[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 2d0f05ae..7b8fee06 100644 --- a/crates/tui/CHANGELOG.md +++ b/crates/tui/CHANGELOG.md @@ -7,11 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Fixed +### Added -- **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. +- **`CODEWHALE_*` env aliases.** `CODEWHALE_PROVIDER`, `CODEWHALE_MODEL`, + and `CODEWHALE_BASE_URL` are public product-scoped aliases that take + precedence over the legacy `DEEPSEEK_*` forms. The `DEEPSEEK_*` names + remain accepted for back-compat. Recommended setup paths are + `codewhale --provider `, `provider = ""` in + `~/.codewhale/config.toml`, or `CODEWHALE_PROVIDER=`. + +### Changed + +- **DeepSeek-first focus.** v0.8.45.x refocuses on delivering the + highest-quality experience on DeepSeek first. The project's broader + goal remains to become a strong harness for open-source and open-weight + coding models, but additional first-class provider paths are planned + for v0.9.0 after the core DeepSeek workflow is solid. ## [0.8.45] - 2026-05-25 diff --git a/crates/tui/src/config.rs b/crates/tui/src/config.rs index 78975ee3..a51a3c8b 100644 --- a/crates/tui/src/config.rs +++ b/crates/tui/src/config.rs @@ -1554,6 +1554,19 @@ impl Config { } } } + 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(); + } if let Some(model) = self.default_text_model.as_deref() && (provider_passes_model_through(provider) || self.active_provider_preserves_custom_base_url_model()) @@ -1570,19 +1583,6 @@ impl Config { { return model_for_provider(provider, normalized); } - 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(); - } match provider { ApiProvider::Deepseek | ApiProvider::DeepseekCN => DEFAULT_TEXT_MODEL, @@ -2271,11 +2271,29 @@ fn default_memory_path() -> Option { // === Environment Overrides === +/// Read a CodeWhale env var, preferring the `CODEWHALE_*` form over the +/// legacy `DEEPSEEK_*` form. Empty values are ignored so a blank shell export +/// does not erase configured provider settings. +fn codewhale_env_var( + codewhale_name: &str, + legacy_name: &str, +) -> Result { + std::env::var(codewhale_name) + .ok() + .filter(|value| !value.trim().is_empty()) + .or_else(|| { + std::env::var(legacy_name) + .ok() + .filter(|value| !value.trim().is_empty()) + }) + .ok_or(std::env::VarError::NotPresent) +} + fn apply_env_overrides(config: &mut Config) { - if let Ok(value) = std::env::var("DEEPSEEK_PROVIDER") { + if let Ok(value) = codewhale_env_var("CODEWHALE_PROVIDER", "DEEPSEEK_PROVIDER") { config.provider = Some(value); } - if let Ok(value) = std::env::var("DEEPSEEK_BASE_URL") { + if let Ok(value) = codewhale_env_var("CODEWHALE_BASE_URL", "DEEPSEEK_BASE_URL") { match config.api_provider() { ApiProvider::Deepseek | ApiProvider::DeepseekCN => { config.base_url = Some(value); @@ -2558,8 +2576,13 @@ fn apply_env_overrides(config: &mut Config) { .moonshot .model = Some(value); } - if let Ok(value) = - std::env::var("DEEPSEEK_MODEL").or_else(|_| std::env::var("DEEPSEEK_DEFAULT_TEXT_MODEL")) + if let Some(value) = codewhale_env_var("CODEWHALE_MODEL", "DEEPSEEK_MODEL") + .ok() + .or_else(|| { + std::env::var("DEEPSEEK_DEFAULT_TEXT_MODEL") + .ok() + .filter(|value| !value.trim().is_empty()) + }) { // The CLI `--model` handoff always sets DEEPSEEK_MODEL, never the // provider-specific *_MODEL var. The legacy root `default_text_model` @@ -4075,6 +4098,9 @@ mod tests { deepseek_http_headers: Option, deepseek_model: Option, deepseek_default_text_model: Option, + codewhale_provider: Option, + codewhale_model: Option, + codewhale_base_url: Option, nvidia_api_key: Option, nvidia_nim_api_key: Option, nim_base_url: Option, @@ -4137,6 +4163,9 @@ mod tests { let http_headers_prev = env::var_os("DEEPSEEK_HTTP_HEADERS"); let model_prev = env::var_os("DEEPSEEK_MODEL"); let default_text_model_prev = env::var_os("DEEPSEEK_DEFAULT_TEXT_MODEL"); + let codewhale_provider_prev = env::var_os("CODEWHALE_PROVIDER"); + let codewhale_model_prev = env::var_os("CODEWHALE_MODEL"); + let codewhale_base_url_prev = env::var_os("CODEWHALE_BASE_URL"); let nvidia_api_key_prev = env::var_os("NVIDIA_API_KEY"); let nvidia_nim_api_key_prev = env::var_os("NVIDIA_NIM_API_KEY"); let nim_base_url_prev = env::var_os("NIM_BASE_URL"); @@ -4194,6 +4223,9 @@ mod tests { env::remove_var("DEEPSEEK_HTTP_HEADERS"); env::remove_var("DEEPSEEK_MODEL"); env::remove_var("DEEPSEEK_DEFAULT_TEXT_MODEL"); + env::remove_var("CODEWHALE_PROVIDER"); + env::remove_var("CODEWHALE_MODEL"); + env::remove_var("CODEWHALE_BASE_URL"); env::remove_var("NVIDIA_API_KEY"); env::remove_var("NVIDIA_NIM_API_KEY"); env::remove_var("NIM_BASE_URL"); @@ -4251,6 +4283,9 @@ mod tests { deepseek_http_headers: http_headers_prev, deepseek_model: model_prev, deepseek_default_text_model: default_text_model_prev, + codewhale_provider: codewhale_provider_prev, + codewhale_model: codewhale_model_prev, + codewhale_base_url: codewhale_base_url_prev, nvidia_api_key: nvidia_api_key_prev, nvidia_nim_api_key: nvidia_nim_api_key_prev, nim_base_url: nim_base_url_prev, @@ -4317,6 +4352,9 @@ mod tests { "DEEPSEEK_DEFAULT_TEXT_MODEL", self.deepseek_default_text_model.take(), ); + Self::restore_var("CODEWHALE_PROVIDER", self.codewhale_provider.take()); + Self::restore_var("CODEWHALE_MODEL", self.codewhale_model.take()); + Self::restore_var("CODEWHALE_BASE_URL", self.codewhale_base_url.take()); Self::restore_var("NVIDIA_API_KEY", self.nvidia_api_key.take()); Self::restore_var("NVIDIA_NIM_API_KEY", self.nvidia_nim_api_key.take()); Self::restore_var("NIM_BASE_URL", self.nim_base_url.take()); @@ -6484,6 +6522,203 @@ base_url = "https://api.kimi.com/coding/v1" Ok(()) } + /// Env-var-only path: `CODEWHALE_BASE_URL=https://api.kimi.com/coding/v1` + /// combined with `CODEWHALE_PROVIDER=moonshot` must trigger Kimi Code + /// model selection even when the TOML has no `base_url`. + #[test] + fn moonshot_kimi_code_env_base_url_selects_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-env-url-{}-{}", + 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#"[providers.moonshot] +api_key = "kimi-code-env-key" +"#, + )?; + // Safety: test-only env mutation guarded by lock_test_env(). + unsafe { + env::set_var("CODEWHALE_PROVIDER", "moonshot"); + env::set_var("CODEWHALE_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-env-key"); + assert!(has_api_key_for(&config, ApiProvider::Moonshot)); + Ok(()) + } + + /// Regression for issue #2160: a stale root `default_text_model` carried + /// over from a DeepSeek setup must not steer the Kimi Code endpoint to + /// `deepseek-v4-pro`. The user-facing trigger here is the legacy + /// `DEEPSEEK_PROVIDER` env var (still produced by the `codewhale + /// --provider moonshot` dispatcher for compat); the test also has a + /// `CODEWHALE_PROVIDER` twin below for the public env path. + #[test] + fn moonshot_kimi_code_model_overrides_root_deepseek_default() -> 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-root-model-{}-{}", + 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 = "deepseek" +default_text_model = "deepseek-v4-pro" + +[providers.moonshot] +api_key = "kimi-code-key" +base_url = "https://api.kimi.com/coding/v1" +"#, + )?; + // Safety: test-only env mutation guarded by lock_test_env(). + unsafe { env::set_var("DEEPSEEK_PROVIDER", "moonshot") }; + + 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); + Ok(()) + } + + /// Same regression as above, but driven by the public `CODEWHALE_PROVIDER` + /// env var. Documents the recommended user-facing setup path: never + /// `DEEPSEEK_PROVIDER=moonshot`, always `CODEWHALE_PROVIDER=moonshot` + /// (or `codewhale --provider moonshot`, which also resolves through + /// this code path internally). + #[test] + fn moonshot_kimi_code_model_resolves_via_codewhale_provider_env() -> 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-cw-env-{}-{}", + 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 = "deepseek" +default_text_model = "deepseek-v4-pro" + +[providers.moonshot] +api_key = "kimi-code-key" +base_url = "https://api.kimi.com/coding/v1" +"#, + )?; + // Safety: test-only env mutation guarded by lock_test_env(). + unsafe { env::set_var("CODEWHALE_PROVIDER", "moonshot") }; + + 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); + Ok(()) + } + + /// `CODEWHALE_PROVIDER` wins when both it and the legacy + /// `DEEPSEEK_PROVIDER` are set, so a user adding the new alias to their + /// shell isn't surprised by a stale legacy export. + #[test] + fn codewhale_provider_env_takes_precedence_over_deepseek_provider() -> 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-cw-vs-ds-provider-{}-{}", + 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, "provider = \"deepseek\"\n")?; + // Safety: test-only env mutation guarded by lock_test_env(). + unsafe { + env::set_var("CODEWHALE_PROVIDER", "moonshot"); + env::set_var("DEEPSEEK_PROVIDER", "openrouter"); + } + + let config = Config::load(None, None)?; + assert_eq!(config.api_provider(), ApiProvider::Moonshot); + Ok(()) + } + + /// Moonshot Platform path: when [providers.moonshot] is empty (or + /// missing) and no Kimi Code endpoint is configured, the resolver + /// defaults to the Moonshot Platform base URL and the `kimi-k2.6` + /// model. This is the "I have a Moonshot Platform API key, not a + /// Kimi Code plan key" path. + #[test] + fn moonshot_platform_defaults_to_kimi_k26() -> 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-moonshot-platform-{}-{}", + 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 = "moonshot-platform-key" +"#, + )?; + + let config = Config::load(None, None)?; + assert_eq!(config.api_provider(), ApiProvider::Moonshot); + assert_eq!(config.deepseek_base_url(), DEFAULT_MOONSHOT_BASE_URL); + assert_eq!(config.default_model(), DEFAULT_MOONSHOT_MODEL); + assert_eq!(config.deepseek_api_key()?, "moonshot-platform-key"); + Ok(()) + } + #[test] fn has_api_key_for_detects_env_and_config_per_provider() -> Result<()> { let _lock = lock_test_env(); diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index c63e5b9b..ed15c9d6 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -63,15 +63,14 @@ provider's keyring entry. For hosted, generic OpenAI-compatible, or self-hosted providers, set `provider = "nvidia-nim"`, `"openai"`, `"atlascloud"`, `"wanjie-ark"`, `"fireworks"`, -`"moonshot"`, `"sglang"`, `"vllm"`, or `"ollama"` or pass `codewhale --provider `. +`"sglang"`, `"vllm"`, or `"ollama"` or pass `codewhale --provider `. The facade saves provider credentials to the shared user config and forwards the resolved key, base URL, provider, and model to the TUI process. Use `codewhale auth set --provider nvidia-nim --api-key "YOUR_NVIDIA_API_KEY"` or `codewhale auth set --provider openai --api-key "YOUR_OPENAI_COMPATIBLE_API_KEY"` or `codewhale auth set --provider atlascloud --api-key "YOUR_ATLASCLOUD_API_KEY"` or `codewhale auth set --provider wanjie-ark --api-key "YOUR_WANJIE_API_KEY"` or -`codewhale auth set --provider fireworks --api-key "YOUR_FIREWORKS_API_KEY"` or -`codewhale auth set --provider moonshot --api-key "YOUR_MOONSHOT_OR_KIMI_API_KEY"` +`codewhale auth set --provider fireworks --api-key "YOUR_FIREWORKS_API_KEY"` to save provider keys through the facade. The generic `openai` provider defaults to `https://api.openai.com/v1`, accepts `OPENAI_BASE_URL`, and defaults to `deepseek-v4-pro` for OpenAI-compatible gateways. `atlascloud` defaults to @@ -80,9 +79,7 @@ to `https://api.openai.com/v1`, accepts `OPENAI_BASE_URL`, and defaults to Wanjie Ark's OpenAI-compatible endpoint at `https://maas-openapi.wanjiedata.com/api/v1`, defaults to `deepseek-reasoner`, and passes model IDs through unchanged because Wanjie model access is -account-scoped. `moonshot` targets Moonshot/Kimi, defaults to `kimi-k2.6`, -and can use `KIMI_API_KEY` or `auth_mode = "kimi_oauth"` with local Kimi CLI -credentials. SGLang, vLLM, and Ollama are +account-scoped. SGLang, vLLM, and Ollama are self-hosted and can run without an API key by default. Ollama defaults to `http://localhost:11434/v1` and sends model tags such as `codewhale-coder:1.3b` or `qwen2.5-coder:7b` unchanged. Self-hosted providers and loopback custom @@ -200,13 +197,22 @@ If a profile is selected but missing, codewhale exits with an error listing avai ## Environment Variables Most runtime environment variables override config values. API-key variables are -fallbacks after saved config and keyring credentials: +fallbacks after saved config and keyring credentials. + +The three user-facing slots — provider, model, base URL — expose `CODEWHALE_*` +aliases. When both forms are set the `CODEWHALE_*` value wins; the +`DEEPSEEK_*` form is kept for older shells: + +- `CODEWHALE_PROVIDER` (preferred) / `DEEPSEEK_PROVIDER` (legacy alias) — + `deepseek|nvidia-nim|openai|atlascloud|wanjie-ark|openrouter|novita|fireworks|sglang|vllm|ollama` +- `CODEWHALE_MODEL` (preferred) / `DEEPSEEK_MODEL` (legacy alias) — default model for the active provider +- `CODEWHALE_BASE_URL` (preferred) / `DEEPSEEK_BASE_URL` (legacy alias) — base URL for the active provider + +Remaining variables: - `DEEPSEEK_API_KEY` -- `DEEPSEEK_BASE_URL` - `DEEPSEEK_HTTP_HEADERS` (custom model request headers, comma-separated `name=value` pairs) -- `DEEPSEEK_PROVIDER` (`codewhale|nvidia-nim|openai|atlascloud|wanjie-ark|openrouter|novita|fireworks|moonshot|sglang|vllm|ollama`) -- `DEEPSEEK_MODEL` or `DEEPSEEK_DEFAULT_TEXT_MODEL` +- `DEEPSEEK_DEFAULT_TEXT_MODEL` (extra legacy alias of `DEEPSEEK_MODEL`) - `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` (stream idle timeout in seconds; default `300`, clamped to `1..=3600`) - `DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS` (connection setup + response-header wait in seconds; default `45`, clamped to `5..=300`; distinct from the per-chunk idle timeout) - `NVIDIA_API_KEY` or `NVIDIA_NIM_API_KEY` (preferred when provider is `nvidia-nim`; falls back to `DEEPSEEK_API_KEY`) @@ -429,10 +435,10 @@ If you are upgrading from older releases: ### Core keys (used by the TUI/engine) -- `provider` (string, optional): `codewhale` (default), `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `openrouter`, `novita`, `fireworks`, `moonshot`, `sglang`, `vllm`, or `ollama`. Legacy `deepseek-cn` configs are still accepted as an alias for `codewhale`; DeepSeek uses the same official host [`https://api.deepseek.com`](https://api-docs.deepseek.com/) worldwide. `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `atlascloud` targets AtlasCloud's OpenAI-compatible endpoint at `https://api.atlascloud.ai/v1`; `wanjie-ark` targets Wanjie Ark's OpenAI-compatible endpoint at `https://maas-openapi.wanjiedata.com/api/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `moonshot` targets `https://api.moonshot.ai/v1` by default, with Kimi CLI OAuth mode using `https://api.kimi.com/coding/v1`; `sglang` targets a self-hosted OpenAI-compatible endpoint, defaulting to `http://localhost:30000/v1`; `vllm` targets a self-hosted vLLM OpenAI-compatible endpoint, defaulting to `http://localhost:8000/v1`; `ollama` targets Ollama's OpenAI-compatible endpoint, defaulting to `http://localhost:11434/v1`. +- `provider` (string, optional): `codewhale` (default), `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `openrouter`, `novita`, `fireworks`, `sglang`, `vllm`, or `ollama`. Legacy `deepseek-cn` configs are still accepted as an alias for `codewhale`; DeepSeek uses the same official host [`https://api.deepseek.com`](https://api-docs.deepseek.com/) worldwide. `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `atlascloud` targets AtlasCloud's OpenAI-compatible endpoint at `https://api.atlascloud.ai/v1`; `wanjie-ark` targets Wanjie Ark's OpenAI-compatible endpoint at `https://maas-openapi.wanjiedata.com/api/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `sglang` targets a self-hosted OpenAI-compatible endpoint, defaulting to `http://localhost:30000/v1`; `vllm` targets a self-hosted vLLM OpenAI-compatible endpoint, defaulting to `http://localhost:8000/v1`; `ollama` targets Ollama's OpenAI-compatible endpoint, defaulting to `http://localhost:11434/v1`. - `api_key` (string, required for hosted providers): must be non-empty for DeepSeek/hosted providers (or set the provider API key env var). Self-hosted SGLang, vLLM, and Ollama can omit it. -- `base_url` (string, optional): defaults to `https://api.deepseek.com/beta` for DeepSeek's OpenAI-compatible Chat Completions API, including legacy `provider = "deepseek-cn"` configs, `https://api.openai.com/v1` for `provider = "openai"`, `https://api.atlascloud.ai/v1` for `provider = "atlascloud"`, `https://maas-openapi.wanjiedata.com/api/v1` for `provider = "wanjie-ark"`, `https://api.moonshot.ai/v1` for `provider = "moonshot"` API-key mode, or the provider-specific endpoint for hosted/self-hosted providers. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features. -- `default_text_model` (string, optional): defaults to `deepseek-v4-pro` for DeepSeek and generic OpenAI-compatible endpoints, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `deepseek-reasoner` for Wanjie Ark, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `kimi-k2.6` for Moonshot/Kimi API-key mode, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, and `codewhale-coder:1.3b` for Ollama. Current public DeepSeek IDs are `deepseek-v4-pro` and `deepseek-v4-flash`, both with 1M context windows, 384K max output, and thinking mode enabled by default. Legacy `deepseek-chat` and `deepseek-reasoner` remain compatibility aliases for `deepseek-v4-flash` until July 24, 2026. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. Generic `openai`, `atlascloud`, `wanjie-ark`, and Ollama model IDs are passed through unchanged. OpenRouter provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `codewhale models` to discover live IDs from your configured endpoint. `DEEPSEEK_MODEL` overrides this for a single process. +- `base_url` (string, optional): defaults to `https://api.deepseek.com/beta` for DeepSeek's OpenAI-compatible Chat Completions API, including legacy `provider = "deepseek-cn"` configs, `https://api.openai.com/v1` for `provider = "openai"`, `https://api.atlascloud.ai/v1` for `provider = "atlascloud"`, `https://maas-openapi.wanjiedata.com/api/v1` for `provider = "wanjie-ark"`, or the provider-specific endpoint for hosted/self-hosted providers. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features. +- `default_text_model` (string, optional): defaults to `deepseek-v4-pro` for DeepSeek and generic OpenAI-compatible endpoints, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `deepseek-reasoner` for Wanjie Ark, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, and `codewhale-coder:1.3b` for Ollama. Current public DeepSeek IDs are `deepseek-v4-pro` and `deepseek-v4-flash`, both with 1M context windows, 384K max output, and thinking mode enabled by default. Legacy `deepseek-chat` and `deepseek-reasoner` remain compatibility aliases for `deepseek-v4-flash` until July 24, 2026. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. Generic `openai`, `atlascloud`, `wanjie-ark`, and Ollama model IDs are passed through unchanged. OpenRouter provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `codewhale models` to discover live IDs from your configured endpoint. `DEEPSEEK_MODEL` overrides this for a single process. - `reasoning_effort` (string, optional): `off`, `low`, `medium`, `high`, or `max`; defaults to the configured UI tier. DeepSeek Platform receives top-level `thinking` / `reasoning_effort` fields. NVIDIA NIM receives equivalent settings through `chat_template_kwargs`. - `allow_shell` (bool, optional): defaults to `true` (sandboxed). - `approval_policy` (string, optional): `on-request`, `untrusted`, or `never`. Runtime `approval_mode` editing in `/config` also accepts `on-request` and `untrusted` aliases.