Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **`CODEWHALE_*` env aliases.** `CODEWHALE_PROVIDER`, `CODEWHALE_MODEL`,
and `CODEWHALE_BASE_URL` are public 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>` — never `DEEPSEEK_PROVIDER=<name>` in
user-facing docs.

### 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.
- **Kimi Code model precedence.** When `[providers.moonshot]` points at the
Kimi Code endpoint, the resolver now returns `kimi-for-coding` even when
a root DeepSeek `default_text_model` (e.g. `deepseek-v4-pro`) is still in
the config from a previous setup.

## [0.8.45] - 2026-05-25

Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -506,12 +506,15 @@ Key environment variables:

| Variable | Purpose |
|---|---|
| `CODEWHALE_PROVIDER` | Active provider. Same value set as `DEEPSEEK_PROVIDER`; this is the public alias and 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`, `moonshot`, `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 |
Expand Down
183 changes: 180 additions & 3 deletions crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand All @@ -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")
Expand Down Expand Up @@ -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>,
Expand Down Expand Up @@ -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 {
Expand All @@ -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"),
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
}
Comment on lines +2886 to +2918
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The test codewhale_model_env_alias_overrides_default_for_active_provider sets CODEWHALE_MODEL to "kimi-k2.6", which is already the default model for Moonshot (DEFAULT_MOONSHOT_MODEL). As a result, this test would pass even if the CODEWHALE_MODEL environment variable was completely ignored by the resolver. To properly test that the environment variable overrides the default model, you should set it to a non-default value (e.g., "custom-model") and assert that the resolved model matches that custom value.

Suggested change
#[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", "kimi-k2.6");
}
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 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-model");
}
let config = ConfigToml::default();
let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.provider, ProviderKind::Moonshot);
assert_eq!(resolved.model, "custom-model");
}

Comment thread
greptile-apps[bot] marked this conversation as resolved.

#[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();
Expand Down
14 changes: 14 additions & 0 deletions crates/tui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **`CODEWHALE_*` env aliases.** `CODEWHALE_PROVIDER`, `CODEWHALE_MODEL`,
and `CODEWHALE_BASE_URL` are public 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>` — never `DEEPSEEK_PROVIDER=<name>` in
user-facing docs.

### 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.
- **Kimi Code model precedence.** When `[providers.moonshot]` points at the
Kimi Code endpoint, the resolver now returns `kimi-for-coding` even when
a root DeepSeek `default_text_model` (e.g. `deepseek-v4-pro`) is still in
the config from a previous setup.

## [0.8.45] - 2026-05-25

Expand Down
Loading
Loading