fix: DeepSeek-first v0.8.45 — CODEWHALE_* env aliases, remove public Kimi/Moonshot promotion

Closes #2164 (superseded).
This commit is contained in:
Hunter Bown
2026-05-26 06:03:10 -05:00
committed by GitHub
parent 6011888cfd
commit c47ed896dc
6 changed files with 494 additions and 69 deletions
+15 -4
View File
@@ -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 <name>`, `provider = "<name>"` in
`~/.codewhale/config.toml`, or `CODEWHALE_PROVIDER=<name>`.
### 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
+13 -28
View File
@@ -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 |
+180 -3
View File
@@ -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<OsString>,
deepseek_http_headers: Option<OsString>,
deepseek_model: Option<OsString>,
deepseek_default_text_model: Option<OsString>,
deepseek_provider: Option<OsString>,
deepseek_auth_mode: Option<OsString>,
nvidia_api_key: Option<OsString>,
@@ -1954,6 +1961,9 @@ mod tests {
vllm_base_url: Option<OsString>,
ollama_api_key: Option<OsString>,
ollama_base_url: Option<OsString>,
codewhale_provider: Option<OsString>,
codewhale_model: Option<OsString>,
codewhale_base_url: Option<OsString>,
}
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();
+15 -4
View File
@@ -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 <name>`, `provider = "<name>"` in
`~/.codewhale/config.toml`, or `CODEWHALE_PROVIDER=<name>`.
### 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
+252 -17
View File
@@ -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<PathBuf> {
// === 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<String, std::env::VarError> {
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<OsString>,
deepseek_model: Option<OsString>,
deepseek_default_text_model: Option<OsString>,
codewhale_provider: Option<OsString>,
codewhale_model: Option<OsString>,
codewhale_base_url: Option<OsString>,
nvidia_api_key: Option<OsString>,
nvidia_nim_api_key: Option<OsString>,
nim_base_url: Option<OsString>,
@@ -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();
+19 -13
View File
@@ -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 <name>`.
`"sglang"`, `"vllm"`, or `"ollama"` or pass `codewhale --provider <name>`.
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.