Merge pull request #2467 from Hmbown/codex/codewhale-secret-path-migration

Migrate file secrets to CodeWhale home
This commit is contained in:
Hunter Bown
2026-05-31 15:37:19 -07:00
committed by GitHub
13 changed files with 345 additions and 70 deletions
+2 -2
View File
@@ -319,7 +319,7 @@ codewhale --provider openrouter --model arcee-ai/trinity-large-thinking
codewhale --provider openrouter --model qwen/qwen3.7-max
# Xiaomi MiMo
codewhale auth set --provider xiaomi-mimo --api-key "YOUR_XIAOMI_MIMO_API_KEY"
codewhale auth set --provider xiaomi-mimo --api-key "YOUR_XIAOMI_KEY"
codewhale --provider xiaomi-mimo --model mimo-v2.5-pro
# Novita
@@ -508,7 +508,7 @@ Key environment variables:
| `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` / `OPENAI_API_KEY` / `ATLASCLOUD_API_KEY` / `WANJIE_ARK_API_KEY` / `VOLCENGINE_API_KEY` / `OPENROUTER_API_KEY` / `XIAOMI_MIMO_API_KEY` / `MIMO_API_KEY` / `NOVITA_API_KEY` / `FIREWORKS_API_KEY` / `SILICONFLOW_API_KEY` / `MOONSHOT_API_KEY` / `KIMI_API_KEY` / `SGLANG_API_KEY` / `VLLM_API_KEY` / `OLLAMA_API_KEY` | Provider auth |
| `NVIDIA_API_KEY` / `OPENAI_API_KEY` / `ATLASCLOUD_API_KEY` / `WANJIE_ARK_API_KEY` / `VOLCENGINE_API_KEY` / `OPENROUTER_API_KEY` / `XIAOMI_MIMO_API_KEY` / `XIAOMI_API_KEY` / `MIMO_API_KEY` / `NOVITA_API_KEY` / `FIREWORKS_API_KEY` / `SILICONFLOW_API_KEY` / `MOONSHOT_API_KEY` / `KIMI_API_KEY` / `SGLANG_API_KEY` / `VLLM_API_KEY` / `OLLAMA_API_KEY` | Provider auth |
| `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_ARK_MODEL` | Wanjie Ark endpoint and model override |
+3 -3
View File
@@ -196,7 +196,7 @@ max_subagents = 10 # optional (1-20)
# OpenAI-compatible: OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_MODEL
# Wanjie Ark: WANJIE_ARK_API_KEY (or WANJIE_API_KEY), WANJIE_ARK_BASE_URL, WANJIE_ARK_MODEL
# OpenRouter: OPENROUTER_API_KEY, OPENROUTER_BASE_URL, OPENROUTER_MODEL
# Xiaomi MiMo: XIAOMI_MIMO_API_KEY (or MIMO_API_KEY), XIAOMI_MIMO_BASE_URL, XIAOMI_MIMO_MODEL
# Xiaomi MiMo: XIAOMI_MIMO_API_KEY (or XIAOMI_API_KEY / MIMO_API_KEY), XIAOMI_MIMO_BASE_URL, XIAOMI_MIMO_MODEL
# Novita: NOVITA_API_KEY, NOVITA_BASE_URL, NOVITA_MODEL
# Fireworks: FIREWORKS_API_KEY, FIREWORKS_BASE_URL
# SiliconFlow: SILICONFLOW_API_KEY, SILICONFLOW_BASE_URL, SILICONFLOW_MODEL
@@ -268,7 +268,7 @@ max_subagents = 10 # optional (1-20)
# Xiaomi MiMo OpenAI-compatible endpoint (https://platform.xiaomimimo.com)
[providers.xiaomi_mimo]
# api_key = "YOUR_XIAOMI_MIMO_API_KEY"
# api_key = "YOUR_XIAOMI_KEY"
# base_url = "https://api.xiaomimimo.com/v1"
# model = "mimo-v2.5-pro"
@@ -423,7 +423,7 @@ exec_policy = true
#
# Xiaomi MiMo image understanding can be configured through the same tool:
# model = "mimo-v2.5"
# api_key = "YOUR_XIAOMI_MIMO_API_KEY"
# api_key = "YOUR_XIAOMI_KEY"
# base_url = "https://api.xiaomimimo.com/v1"
# ─────────────────────────────────────────────────────────────────────────────────
+2 -2
View File
@@ -807,7 +807,7 @@ fn provider_env_vars(provider: ProviderKind) -> &'static [&'static str] {
match provider {
ProviderKind::Deepseek => &["DEEPSEEK_API_KEY"],
ProviderKind::Openrouter => &["OPENROUTER_API_KEY"],
ProviderKind::XiaomiMimo => &["XIAOMI_MIMO_API_KEY", "MIMO_API_KEY"],
ProviderKind::XiaomiMimo => &["XIAOMI_MIMO_API_KEY", "XIAOMI_API_KEY", "MIMO_API_KEY"],
ProviderKind::Novita => &["NOVITA_API_KEY"],
ProviderKind::NvidiaNim => &["NVIDIA_API_KEY", "NVIDIA_NIM_API_KEY", "DEEPSEEK_API_KEY"],
ProviderKind::Fireworks => &["FIREWORKS_API_KEY"],
@@ -2955,7 +2955,7 @@ mod tests {
(
ProviderKind::XiaomiMimo,
"xiaomi-mimo",
&["XIAOMI_MIMO_API_KEY", "MIMO_API_KEY"],
&["XIAOMI_MIMO_API_KEY", "XIAOMI_API_KEY", "MIMO_API_KEY"],
),
(ProviderKind::Novita, "novita", &["NOVITA_API_KEY"]),
(
+4
View File
@@ -2305,6 +2305,7 @@ mod tests {
openrouter_api_key: Option<OsString>,
openrouter_base_url: Option<OsString>,
xiaomi_mimo_api_key: Option<OsString>,
xiaomi_api_key: Option<OsString>,
mimo_api_key: Option<OsString>,
xiaomi_mimo_base_url: Option<OsString>,
mimo_base_url: Option<OsString>,
@@ -2364,6 +2365,7 @@ mod tests {
openrouter_api_key: env::var_os("OPENROUTER_API_KEY"),
openrouter_base_url: env::var_os("OPENROUTER_BASE_URL"),
xiaomi_mimo_api_key: env::var_os("XIAOMI_MIMO_API_KEY"),
xiaomi_api_key: env::var_os("XIAOMI_API_KEY"),
mimo_api_key: env::var_os("MIMO_API_KEY"),
xiaomi_mimo_base_url: env::var_os("XIAOMI_MIMO_BASE_URL"),
mimo_base_url: env::var_os("MIMO_BASE_URL"),
@@ -2418,6 +2420,7 @@ mod tests {
env::remove_var("OPENROUTER_API_KEY");
env::remove_var("OPENROUTER_BASE_URL");
env::remove_var("XIAOMI_MIMO_API_KEY");
env::remove_var("XIAOMI_API_KEY");
env::remove_var("MIMO_API_KEY");
env::remove_var("XIAOMI_MIMO_BASE_URL");
env::remove_var("MIMO_BASE_URL");
@@ -2488,6 +2491,7 @@ mod tests {
Self::restore_var("OPENROUTER_API_KEY", self.openrouter_api_key.take());
Self::restore_var("OPENROUTER_BASE_URL", self.openrouter_base_url.take());
Self::restore_var("XIAOMI_MIMO_API_KEY", self.xiaomi_mimo_api_key.take());
Self::restore_var("XIAOMI_API_KEY", self.xiaomi_api_key.take());
Self::restore_var("MIMO_API_KEY", self.mimo_api_key.take());
Self::restore_var("XIAOMI_MIMO_BASE_URL", self.xiaomi_mimo_base_url.take());
Self::restore_var("MIMO_BASE_URL", self.mimo_base_url.take());
+269 -26
View File
@@ -1,4 +1,4 @@
//! Secret storage for DeepSeek API keys.
//! Secret storage for CodeWhale API keys.
//!
//! Provides a small abstraction (`KeyringStore`) plus a default
//! file-based implementation (`FileKeyringStore`), an opt-in OS keyring
@@ -19,12 +19,16 @@ use std::sync::{Arc, Mutex};
use serde::{Deserialize, Serialize};
use thiserror::Error;
/// Default OS keychain service name. macOS users can verify entries with
/// `security find-generic-password -s deepseek -a <provider>`.
/// Default OS keychain service name. Kept as `deepseek` for compatibility
/// with credentials saved before the CodeWhale rename. macOS users can verify
/// entries with `security find-generic-password -s deepseek -a <provider>`.
pub const DEFAULT_SERVICE: &str = "deepseek";
/// Select the secret storage backend. Supported values are `file` (default)
/// and `system`/`keyring` for the OS credential store.
pub const SECRET_BACKEND_ENV: &str = "DEEPSEEK_SECRET_BACKEND";
pub const SECRET_BACKEND_ENV: &str = "CODEWHALE_SECRET_BACKEND";
/// Legacy alias for [`SECRET_BACKEND_ENV`].
pub const LEGACY_SECRET_BACKEND_ENV: &str = "DEEPSEEK_SECRET_BACKEND";
const FILE_BACKEND_LABEL: &str = "file-based (~/.codewhale/secrets/)";
/// Errors that may arise from a [`KeyringStore`] backend.
#[derive(Debug, Error)]
@@ -51,7 +55,7 @@ pub enum SecretsError {
/// Abstract secret store trait.
///
/// Concrete implementations may use the OS keyring ([`DefaultKeyringStore`]),
/// a JSON file under `~/.deepseek/secrets/` ([`FileKeyringStore`]), or an
/// a JSON file under `~/.codewhale/secrets/` ([`FileKeyringStore`]), or an
/// in-memory map for tests ([`InMemoryKeyringStore`]).
///
/// All implementations must be [`Send`] + [`Sync`] so they can be shared
@@ -78,7 +82,7 @@ pub trait KeyringStore: Send + Sync {
/// Short, human-readable label for this backend.
///
/// Used by diagnostic output (e.g. `doctor` command) to indicate which
/// storage backend is active. Examples: `"file-based (~/.deepseek/secrets/)"`,
/// storage backend is active. Examples: `"file-based (~/.codewhale/secrets/)"`,
/// `"system keyring"`, `"in-memory (test)"`.
fn backend_name(&self) -> &'static str;
}
@@ -267,7 +271,7 @@ impl KeyringStore for InMemoryKeyringStore {
/// JSON-on-disk secret store for headless environments.
///
/// This is the default backend. Secrets are serialised as a JSON object
/// at `<home>/.deepseek/secrets/secrets.json` with Unix file mode `0600`
/// at `<home>/.codewhale/secrets/secrets.json` with Unix file mode `0600`
/// (owner read/write only). The parent directory is created with mode `0700`
/// if it does not exist.
///
@@ -295,16 +299,68 @@ impl FileKeyringStore {
Self { path: path.into() }
}
/// Default path: `<home>/.deepseek/secrets/secrets.json`. Honours
/// `HOME` (Unix) and `USERPROFILE` (Windows) via the `dirs` crate.
/// Default path: `<home>/.codewhale/secrets/secrets.json`. Honours
/// `CODEWHALE_HOME`, then `HOME`, `USERPROFILE`, and finally the platform
/// home directory from the `dirs` crate. On first use, non-conflicting
/// entries from the legacy `<home>/.deepseek/secrets/secrets.json` file are
/// copied into the CodeWhale store.
pub fn default_path() -> Result<PathBuf, SecretsError> {
let home = dirs::home_dir().ok_or_else(|| {
let primary = default_codewhale_secrets_path()?;
let legacy = legacy_deepseek_secrets_path()?;
if let Err(err) = Self::migrate_legacy_file_if_needed(&primary, &legacy) {
tracing::warn!(
"could not migrate legacy secret store from {} to {}: {err}",
legacy.display(),
primary.display()
);
}
Ok(primary)
}
fn migrate_legacy_file_if_needed(primary: &Path, legacy: &Path) -> Result<(), SecretsError> {
if !legacy.exists() {
return Ok(());
}
let legacy_store = Self::new(legacy.to_path_buf());
let legacy_blob = legacy_store.load_unlocked()?;
if legacy_blob.entries.is_empty() {
return Ok(());
}
let primary_store = Self::new(primary.to_path_buf());
let mut primary_blob = primary_store.load_unlocked()?;
let mut changed = false;
for (key, value) in legacy_blob.entries {
if let std::collections::hash_map::Entry::Vacant(entry) =
primary_blob.entries.entry(key)
{
entry.insert(value);
changed = true;
}
}
if changed {
primary_store.store_unlocked(&primary_blob)?;
}
Ok(())
}
fn home_dir() -> Result<PathBuf, SecretsError> {
for var in ["HOME", "USERPROFILE"] {
if let Ok(value) = std::env::var(var) {
let trimmed = value.trim();
if !trimmed.is_empty() {
return Ok(PathBuf::from(trimmed));
}
}
}
dirs::home_dir().ok_or_else(|| {
SecretsError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
"could not resolve home directory for FileKeyringStore",
))
})?;
Ok(home.join(".deepseek").join("secrets").join("secrets.json"))
})
}
/// Path used for storage.
@@ -398,10 +454,30 @@ impl KeyringStore for FileKeyringStore {
}
fn backend_name(&self) -> &'static str {
"file-based (~/.deepseek/secrets/)"
FILE_BACKEND_LABEL
}
}
fn default_codewhale_secrets_path() -> Result<PathBuf, SecretsError> {
if let Ok(value) = std::env::var("CODEWHALE_HOME") {
let trimmed = value.trim();
if !trimmed.is_empty() {
return Ok(PathBuf::from(trimmed).join("secrets").join("secrets.json"));
}
}
Ok(FileKeyringStore::home_dir()?
.join(".codewhale")
.join("secrets")
.join("secrets.json"))
}
fn legacy_deepseek_secrets_path() -> Result<PathBuf, SecretsError> {
Ok(FileKeyringStore::home_dir()?
.join(".deepseek")
.join("secrets")
.join("secrets.json"))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SecretBackendSelection {
File,
@@ -420,6 +496,13 @@ fn secret_backend_selection(value: Option<&str>) -> SecretBackendSelection {
}
}
fn configured_secret_backend() -> Option<String> {
std::env::var(SECRET_BACKEND_ENV)
.ok()
.filter(|value| !value.trim().is_empty())
.or_else(|| std::env::var(LEGACY_SECRET_BACKEND_ENV).ok())
}
/// High-level facade combining a [`KeyringStore`] with environment variable fallbacks.
///
/// Lookup precedence: **secret store -> env -> none**. Callers that also
@@ -491,11 +574,11 @@ impl Secrets {
/// 3. If the env var is set to an unrecognised value, log a warning
/// and use the file-based store.
pub fn auto_detect() -> Self {
match secret_backend_selection(std::env::var(SECRET_BACKEND_ENV).ok().as_deref()) {
match secret_backend_selection(configured_secret_backend().as_deref()) {
SecretBackendSelection::File => Self::file_backed_default(),
SecretBackendSelection::Unknown => {
tracing::warn!(
"{SECRET_BACKEND_ENV} has an unsupported value; using file-backed secret store"
"{SECRET_BACKEND_ENV}/{LEGACY_SECRET_BACKEND_ENV} has an unsupported value; using file-backed secret store"
);
Self::file_backed_default()
}
@@ -516,7 +599,7 @@ impl Secrets {
fn file_backed_default() -> Self {
let path = FileKeyringStore::default_path()
.unwrap_or_else(|_| PathBuf::from(".deepseek-secrets.json"));
.unwrap_or_else(|_| PathBuf::from(".codewhale-secrets.json"));
Self::new(Arc::new(FileKeyringStore::new(path)))
}
@@ -594,7 +677,7 @@ impl Secrets {
/// |---|---|
/// | `deepseek` | `DEEPSEEK_API_KEY` |
/// | `openrouter` | `OPENROUTER_API_KEY` |
/// | `xiaomi-mimo` / `mimo` | `XIAOMI_MIMO_API_KEY`, `MIMO_API_KEY` |
/// | `xiaomi-mimo` / `mimo` | `XIAOMI_MIMO_API_KEY`, `XIAOMI_API_KEY`, `MIMO_API_KEY` |
/// | `novita` | `NOVITA_API_KEY` |
/// | `nvidia` / `nvidia-nim` / `nim` | `NVIDIA_API_KEY`, `NVIDIA_NIM_API_KEY`, `DEEPSEEK_API_KEY` |
/// | `fireworks` | `FIREWORKS_API_KEY` |
@@ -616,7 +699,7 @@ pub fn env_for(name: &str) -> Option<String> {
"deepseek" => &["DEEPSEEK_API_KEY"],
"openrouter" => &["OPENROUTER_API_KEY"],
"xiaomi-mimo" | "xiaomi_mimo" | "xiaomimimo" | "mimo" | "xiaomi" => {
&["XIAOMI_MIMO_API_KEY", "MIMO_API_KEY"]
&["XIAOMI_MIMO_API_KEY", "XIAOMI_API_KEY", "MIMO_API_KEY"]
}
"novita" => &["NOVITA_API_KEY"],
// NVIDIA NIM falls back to `DEEPSEEK_API_KEY` last because the
@@ -673,6 +756,7 @@ mod tests {
fn clear_known_envs() {
for var in [
"CODEWHALE_HOME",
"DEEPSEEK_API_KEY",
"OPENROUTER_API_KEY",
"NOVITA_API_KEY",
@@ -689,8 +773,10 @@ mod tests {
"WANJIE_API_KEY",
"WANJIE_MAAS_API_KEY",
"XIAOMI_MIMO_API_KEY",
"XIAOMI_API_KEY",
"MIMO_API_KEY",
SECRET_BACKEND_ENV,
LEGACY_SECRET_BACKEND_ENV,
] {
// Safety: tests serialise on env_lock(); the broader
// workspace has the same pattern in `crates/config`.
@@ -698,6 +784,28 @@ mod tests {
}
}
struct EnvVarGuard {
name: &'static str,
previous: Option<std::ffi::OsString>,
}
impl EnvVarGuard {
fn set(name: &'static str, value: impl AsRef<std::ffi::OsStr>) -> Self {
let previous = std::env::var_os(name);
unsafe { std::env::set_var(name, value) };
Self { name, previous }
}
}
impl Drop for EnvVarGuard {
fn drop(&mut self) {
match self.previous.take() {
Some(value) => unsafe { std::env::set_var(self.name, value) },
None => unsafe { std::env::remove_var(self.name) },
}
}
}
#[test]
fn backend_selection_defaults_to_file() {
assert_eq!(secret_backend_selection(None), SecretBackendSelection::File);
@@ -731,26 +839,151 @@ mod tests {
fn auto_detect_is_file_backed_by_default() {
let _lock = env_lock();
clear_known_envs();
let tmp = tempfile::tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp.path());
let _userprofile = EnvVarGuard::set("USERPROFILE", tmp.path());
let secrets = Secrets::auto_detect();
assert_eq!(secrets.backend_name(), "file-based (~/.deepseek/secrets/)");
assert_eq!(secrets.backend_name(), FILE_BACKEND_LABEL);
}
#[test]
fn auto_detect_honors_explicit_file_backend() {
let _lock = env_lock();
clear_known_envs();
let tmp = tempfile::tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp.path());
let _userprofile = EnvVarGuard::set("USERPROFILE", tmp.path());
// Safety: env mutation guarded by env_lock().
unsafe { std::env::set_var(SECRET_BACKEND_ENV, "local") };
let secrets = Secrets::auto_detect();
assert_eq!(secrets.backend_name(), "file-based (~/.deepseek/secrets/)");
assert_eq!(secrets.backend_name(), FILE_BACKEND_LABEL);
// Safety: env mutation guarded by env_lock().
unsafe { std::env::remove_var(SECRET_BACKEND_ENV) };
}
#[test]
fn auto_detect_honors_legacy_backend_env_alias() {
let _lock = env_lock();
clear_known_envs();
let tmp = tempfile::tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp.path());
let _userprofile = EnvVarGuard::set("USERPROFILE", tmp.path());
unsafe { std::env::set_var(LEGACY_SECRET_BACKEND_ENV, "local") };
let secrets = Secrets::auto_detect();
assert_eq!(secrets.backend_name(), FILE_BACKEND_LABEL);
clear_known_envs();
}
#[test]
fn file_default_path_uses_codewhale_home() {
let _lock = env_lock();
clear_known_envs();
let tmp = tempfile::tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp.path());
let _userprofile = EnvVarGuard::set("USERPROFILE", tmp.path());
let path = FileKeyringStore::default_path().unwrap();
assert_eq!(
path,
tmp.path()
.join(".codewhale")
.join("secrets")
.join("secrets.json")
);
}
#[test]
fn file_default_path_honors_codewhale_home() {
let _lock = env_lock();
clear_known_envs();
let tmp = tempfile::tempdir().unwrap();
let custom = tmp.path().join("custom-codewhale");
let _home = EnvVarGuard::set("HOME", tmp.path());
let _userprofile = EnvVarGuard::set("USERPROFILE", tmp.path());
let _codewhale_home = EnvVarGuard::set("CODEWHALE_HOME", &custom);
let path = FileKeyringStore::default_path().unwrap();
assert_eq!(path, custom.join("secrets").join("secrets.json"));
}
#[test]
fn file_default_path_migrates_legacy_entries_to_codewhale() {
let _lock = env_lock();
clear_known_envs();
let tmp = tempfile::tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp.path());
let _userprofile = EnvVarGuard::set("USERPROFILE", tmp.path());
let legacy = tmp
.path()
.join(".deepseek")
.join("secrets")
.join("secrets.json");
FileKeyringStore::new(legacy.clone())
.set("xiaomi-mimo", "legacy-mimo")
.unwrap();
let primary = FileKeyringStore::default_path().unwrap();
let primary_store = FileKeyringStore::new(primary.clone());
assert_eq!(
primary,
tmp.path()
.join(".codewhale")
.join("secrets")
.join("secrets.json")
);
assert_eq!(
primary_store.get("xiaomi-mimo").unwrap().as_deref(),
Some("legacy-mimo")
);
assert!(
legacy.exists(),
"migration copies; it does not delete legacy data"
);
}
#[test]
fn file_default_path_migration_preserves_primary_values() {
let _lock = env_lock();
clear_known_envs();
let tmp = tempfile::tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp.path());
let _userprofile = EnvVarGuard::set("USERPROFILE", tmp.path());
let legacy = tmp
.path()
.join(".deepseek")
.join("secrets")
.join("secrets.json");
let primary = tmp
.path()
.join(".codewhale")
.join("secrets")
.join("secrets.json");
FileKeyringStore::new(legacy)
.set("openrouter", "legacy-openrouter")
.unwrap();
let primary_store = FileKeyringStore::new(primary.clone());
primary_store
.set("openrouter", "primary-openrouter")
.unwrap();
let resolved = FileKeyringStore::default_path().unwrap();
assert_eq!(resolved, primary);
assert_eq!(
primary_store.get("openrouter").unwrap().as_deref(),
Some("primary-openrouter")
);
}
#[test]
fn in_memory_store_round_trips() {
let store = InMemoryKeyringStore::new();
@@ -879,6 +1112,10 @@ mod tests {
assert_eq!(env_for("xiaomi").as_deref(), Some("mimo-key"));
clear_known_envs();
unsafe { std::env::set_var("XIAOMI_API_KEY", "xiaomi-key") };
assert_eq!(env_for("xiaomi-mimo").as_deref(), Some("xiaomi-key"));
clear_known_envs();
}
#[test]
@@ -1102,13 +1339,19 @@ mod tests {
#[test]
fn file_store_default_path_uses_home() {
// We don't override HOME here (other tests do); we just check the
// shape of the path is `<home>/.deepseek/secrets/secrets.json`.
let _lock = env_lock();
clear_known_envs();
let tmp = tempfile::tempdir().unwrap();
let _home = EnvVarGuard::set("HOME", tmp.path());
let _userprofile = EnvVarGuard::set("USERPROFILE", tmp.path());
let path = FileKeyringStore::default_path().unwrap();
assert!(
path.ends_with("secrets/secrets.json") || path.ends_with("secrets\\secrets.json"),
"unexpected default path: {}",
path.display()
assert_eq!(
path,
tmp.path()
.join(".codewhale")
.join("secrets")
.join("secrets.json")
);
}
}
+44 -16
View File
@@ -2210,45 +2210,45 @@ impl Config {
export DEEPSEEK_API_KEY=<your-key> (current shell only;\n\
also note: zsh users exports in ~/.zshrc only reach interactive\n\
shells, prefer ~/.zshenv for everything)\n\
api_key = \"<your-key>\" in ~/.deepseek/config.toml"
api_key = \"<your-key>\" in ~/.codewhale/config.toml"
),
ApiProvider::NvidiaNim => anyhow::bail!(
"NVIDIA NIM API key not found. Run 'codewhale auth set --provider nvidia-nim', \
set NVIDIA_API_KEY/NVIDIA_NIM_API_KEY, or save api_key in ~/.deepseek/config.toml \
set NVIDIA_API_KEY/NVIDIA_NIM_API_KEY, or save api_key in ~/.codewhale/config.toml \
with provider = \"nvidia-nim\"."
),
ApiProvider::Openai => anyhow::bail!(
"OpenAI-compatible API key not found. Run 'codewhale auth set --provider openai', \
set OPENAI_API_KEY, or add [providers.openai] api_key in ~/.deepseek/config.toml."
set OPENAI_API_KEY, or add [providers.openai] api_key in ~/.codewhale/config.toml."
),
ApiProvider::Atlascloud => anyhow::bail!(
"AtlasCloud API key not found. Run 'codewhale auth set --provider atlascloud', \
set ATLASCLOUD_API_KEY, or add [providers.atlascloud] api_key in ~/.deepseek/config.toml."
set ATLASCLOUD_API_KEY, or add [providers.atlascloud] api_key in ~/.codewhale/config.toml."
),
ApiProvider::WanjieArk => anyhow::bail!(
"Wanjie Ark API key not found. Run 'codewhale auth set --provider wanjie-ark', \
set WANJIE_ARK_API_KEY/WANJIE_API_KEY/WANJIE_MAAS_API_KEY, or add \
[providers.wanjie_ark] api_key in ~/.deepseek/config.toml."
[providers.wanjie_ark] api_key in ~/.codewhale/config.toml."
),
ApiProvider::Openrouter => anyhow::bail!(
"OpenRouter API key not found. Run 'codewhale auth set --provider openrouter', \
set OPENROUTER_API_KEY, or add [providers.openrouter] api_key in ~/.deepseek/config.toml."
set OPENROUTER_API_KEY, or add [providers.openrouter] api_key in ~/.codewhale/config.toml."
),
ApiProvider::XiaomiMimo => anyhow::bail!(
"Xiaomi MiMo API key not found. Run 'codewhale auth set --provider xiaomi-mimo', \
set XIAOMI_MIMO_API_KEY/MIMO_API_KEY, or add [providers.xiaomi_mimo] api_key in ~/.deepseek/config.toml."
set XIAOMI_MIMO_API_KEY/XIAOMI_API_KEY/MIMO_API_KEY, or add [providers.xiaomi_mimo] api_key in ~/.codewhale/config.toml."
),
ApiProvider::Novita => anyhow::bail!(
"Novita API key not found. Run 'codewhale auth set --provider novita', \
set NOVITA_API_KEY, or add [providers.novita] api_key in ~/.deepseek/config.toml."
set NOVITA_API_KEY, or add [providers.novita] api_key in ~/.codewhale/config.toml."
),
ApiProvider::Fireworks => anyhow::bail!(
"Fireworks AI API key not found. Run 'codewhale auth set --provider fireworks', \
set FIREWORKS_API_KEY, or add [providers.fireworks] api_key in ~/.deepseek/config.toml."
set FIREWORKS_API_KEY, or add [providers.fireworks] api_key in ~/.codewhale/config.toml."
),
ApiProvider::Siliconflow => anyhow::bail!(
"SiliconFlow API key not found. Run 'codewhale auth set --provider siliconflow', \
set SILICONFLOW_API_KEY, or add [providers.siliconflow] api_key in ~/.deepseek/config.toml."
set SILICONFLOW_API_KEY, or add [providers.siliconflow] api_key in ~/.codewhale/config.toml."
),
ApiProvider::Moonshot => anyhow::bail!(
"Moonshot/Kimi API key not found. Run 'codewhale auth set --provider moonshot', \
@@ -3969,7 +3969,7 @@ pub enum SavedCredential {
/// install hide the freshly-entered key (#593). The `backend`
/// label is the value of [`codewhale_secrets::Secrets::backend_name`]
/// at write time so the toast text can name the actual backend
/// (`"system keyring"`, `"file-based (~/.deepseek/secrets/)"`).
/// (`"system keyring"`, `"file-based (~/.codewhale/secrets/)"`).
KeyringAndConfigFile {
/// `Secrets::backend_name()` at write time.
backend: String,
@@ -3998,7 +3998,7 @@ impl SavedCredential {
/// Save the active provider's API key.
///
/// **Dual-write strategy (#593):** writes to `~/.deepseek/config.toml`
/// **Dual-write strategy (#593):** writes to `~/.codewhale/config.toml`
/// (always) and to the OS keyring via [`codewhale_secrets::Secrets`]
/// (when a backend is reachable). The runtime resolves credentials in
/// `keyring → env → config-file` order; writing to the config file
@@ -4139,7 +4139,7 @@ reasoning_effort = "max"
/// Platform credential stores are intentionally not queried here.
/// Startup/onboarding checks must be cheap and prompt-free, so v0.8.8
/// keeps the default auth path to environment variables and
/// `~/.deepseek/config.toml`.
/// `~/.codewhale/config.toml`.
///
/// Used by [`crate::tui::app::App::new`] to decide whether to gate
/// the user behind the in-TUI api-key onboarding screen — getting
@@ -4200,6 +4200,7 @@ pub fn active_provider_has_env_api_key(config: &Config) -> bool {
}
ApiProvider::XiaomiMimo => {
std::env::var("XIAOMI_MIMO_API_KEY").is_ok_and(|k| !k.trim().is_empty())
|| std::env::var("XIAOMI_API_KEY").is_ok_and(|k| !k.trim().is_empty())
|| std::env::var("MIMO_API_KEY").is_ok_and(|k| !k.trim().is_empty())
}
ApiProvider::Novita => std::env::var("NOVITA_API_KEY").is_ok_and(|k| !k.trim().is_empty()),
@@ -4266,7 +4267,8 @@ pub fn has_api_key_for(config: &Config, provider: ApiProvider) -> bool {
return true;
}
if matches!(provider, ApiProvider::XiaomiMimo)
&& std::env::var("MIMO_API_KEY").is_ok_and(|k| !k.trim().is_empty())
&& (std::env::var("XIAOMI_API_KEY").is_ok_and(|k| !k.trim().is_empty())
|| std::env::var("MIMO_API_KEY").is_ok_and(|k| !k.trim().is_empty()))
{
return true;
}
@@ -4318,7 +4320,7 @@ pub fn has_api_key_for(config: &Config, provider: ApiProvider) -> bool {
/// Save an API key to the appropriate place for the given provider.
/// DeepSeek goes through [`save_api_key`]. Other providers write
/// `[providers.<name>] api_key = "..."` to `~/.deepseek/config.toml`.
/// `[providers.<name>] api_key = "..."` to `~/.codewhale/config.toml`.
/// Returns the config file path.
pub fn save_api_key_for(provider: ApiProvider, api_key: &str) -> Result<PathBuf> {
if matches!(provider, ApiProvider::Deepseek | ApiProvider::DeepseekCN) {
@@ -4927,7 +4929,10 @@ mod tests {
struct EnvGuard {
home: Option<OsString>,
userprofile: Option<OsString>,
codewhale_home: Option<OsString>,
deepseek_config_path: Option<OsString>,
codewhale_secret_backend: Option<OsString>,
deepseek_secret_backend: Option<OsString>,
deepseek_provider: Option<OsString>,
deepseek_api_key: Option<OsString>,
deepseek_base_url: Option<OsString>,
@@ -4961,6 +4966,7 @@ mod tests {
openrouter_api_key: Option<OsString>,
openrouter_base_url: Option<OsString>,
xiaomi_mimo_api_key: Option<OsString>,
xiaomi_api_key: Option<OsString>,
mimo_api_key: Option<OsString>,
xiaomi_mimo_base_url: Option<OsString>,
mimo_base_url: Option<OsString>,
@@ -5001,7 +5007,10 @@ mod tests {
let config_str = OsString::from(config_path.as_os_str());
let home_prev = env::var_os("HOME");
let userprofile_prev = env::var_os("USERPROFILE");
let codewhale_home_prev = env::var_os("CODEWHALE_HOME");
let deepseek_config_prev = env::var_os("DEEPSEEK_CONFIG_PATH");
let codewhale_secret_backend_prev = env::var_os("CODEWHALE_SECRET_BACKEND");
let deepseek_secret_backend_prev = env::var_os("DEEPSEEK_SECRET_BACKEND");
let deepseek_provider_prev = env::var_os("DEEPSEEK_PROVIDER");
let api_key_prev = env::var_os("DEEPSEEK_API_KEY");
let base_url_prev = env::var_os("DEEPSEEK_BASE_URL");
@@ -5035,6 +5044,7 @@ mod tests {
let openrouter_api_key_prev = env::var_os("OPENROUTER_API_KEY");
let openrouter_base_url_prev = env::var_os("OPENROUTER_BASE_URL");
let xiaomi_mimo_api_key_prev = env::var_os("XIAOMI_MIMO_API_KEY");
let xiaomi_api_key_prev = env::var_os("XIAOMI_API_KEY");
let mimo_api_key_prev = env::var_os("MIMO_API_KEY");
let xiaomi_mimo_base_url_prev = env::var_os("XIAOMI_MIMO_BASE_URL");
let mimo_base_url_prev = env::var_os("MIMO_BASE_URL");
@@ -5070,7 +5080,10 @@ mod tests {
unsafe {
env::set_var("HOME", &home_str);
env::set_var("USERPROFILE", &home_str);
env::remove_var("CODEWHALE_HOME");
env::set_var("DEEPSEEK_CONFIG_PATH", &config_str);
env::remove_var("CODEWHALE_SECRET_BACKEND");
env::remove_var("DEEPSEEK_SECRET_BACKEND");
env::remove_var("DEEPSEEK_PROVIDER");
env::remove_var("DEEPSEEK_API_KEY");
env::remove_var("DEEPSEEK_BASE_URL");
@@ -5104,6 +5117,7 @@ mod tests {
env::remove_var("OPENROUTER_API_KEY");
env::remove_var("OPENROUTER_BASE_URL");
env::remove_var("XIAOMI_MIMO_API_KEY");
env::remove_var("XIAOMI_API_KEY");
env::remove_var("MIMO_API_KEY");
env::remove_var("XIAOMI_MIMO_BASE_URL");
env::remove_var("MIMO_BASE_URL");
@@ -5139,7 +5153,10 @@ mod tests {
Self {
home: home_prev,
userprofile: userprofile_prev,
codewhale_home: codewhale_home_prev,
deepseek_config_path: deepseek_config_prev,
codewhale_secret_backend: codewhale_secret_backend_prev,
deepseek_secret_backend: deepseek_secret_backend_prev,
deepseek_provider: deepseek_provider_prev,
deepseek_api_key: api_key_prev,
deepseek_base_url: base_url_prev,
@@ -5173,6 +5190,7 @@ mod tests {
openrouter_api_key: openrouter_api_key_prev,
openrouter_base_url: openrouter_base_url_prev,
xiaomi_mimo_api_key: xiaomi_mimo_api_key_prev,
xiaomi_api_key: xiaomi_api_key_prev,
mimo_api_key: mimo_api_key_prev,
xiaomi_mimo_base_url: xiaomi_mimo_base_url_prev,
mimo_base_url: mimo_base_url_prev,
@@ -5214,7 +5232,16 @@ mod tests {
unsafe {
Self::restore_var("HOME", self.home.take());
Self::restore_var("USERPROFILE", self.userprofile.take());
Self::restore_var("CODEWHALE_HOME", self.codewhale_home.take());
Self::restore_var("DEEPSEEK_CONFIG_PATH", self.deepseek_config_path.take());
Self::restore_var(
"CODEWHALE_SECRET_BACKEND",
self.codewhale_secret_backend.take(),
);
Self::restore_var(
"DEEPSEEK_SECRET_BACKEND",
self.deepseek_secret_backend.take(),
);
Self::restore_var("DEEPSEEK_PROVIDER", self.deepseek_provider.take());
Self::restore_var("DEEPSEEK_API_KEY", self.deepseek_api_key.take());
Self::restore_var("DEEPSEEK_BASE_URL", self.deepseek_base_url.take());
@@ -5251,6 +5278,7 @@ mod tests {
Self::restore_var("OPENROUTER_API_KEY", self.openrouter_api_key.take());
Self::restore_var("OPENROUTER_BASE_URL", self.openrouter_base_url.take());
Self::restore_var("XIAOMI_MIMO_API_KEY", self.xiaomi_mimo_api_key.take());
Self::restore_var("XIAOMI_API_KEY", self.xiaomi_api_key.take());
Self::restore_var("MIMO_API_KEY", self.mimo_api_key.take());
Self::restore_var("XIAOMI_MIMO_BASE_URL", self.xiaomi_mimo_base_url.take());
Self::restore_var("MIMO_BASE_URL", self.mimo_base_url.take());
@@ -7999,7 +8027,7 @@ api_key = "moonshot-platform-key"
));
fs::create_dir_all(&temp_root)?;
let _guard = EnvGuard::new(&temp_root);
unsafe { std::env::set_var("DEEPSEEK_SECRET_BACKEND", "local") };
unsafe { std::env::set_var("CODEWHALE_SECRET_BACKEND", "local") };
let path = save_api_key_for(ApiProvider::Openrouter, "or-saved-key")?;
let contents = fs::read_to_string(&path)?;
+2 -2
View File
@@ -407,7 +407,7 @@ impl Engine {
ApiProvider::WanjieArk => "WANJIE_ARK_API_KEY/WANJIE_API_KEY/WANJIE_MAAS_API_KEY",
ApiProvider::Volcengine => "VOLCENGINE_API_KEY/VOLCENGINE_ARK_API_KEY/ARK_API_KEY",
ApiProvider::Openrouter => "OPENROUTER_API_KEY",
ApiProvider::XiaomiMimo => "XIAOMI_MIMO_API_KEY/MIMO_API_KEY",
ApiProvider::XiaomiMimo => "XIAOMI_MIMO_API_KEY/XIAOMI_API_KEY/MIMO_API_KEY",
ApiProvider::Novita => "NOVITA_API_KEY",
ApiProvider::Fireworks => "FIREWORKS_API_KEY",
ApiProvider::Siliconflow => "SILICONFLOW_API_KEY",
@@ -420,7 +420,7 @@ impl Engine {
Some(format!(
"The rejected key came from {env_var}; no saved config key is present.\n\
Run `codewhale auth status` to inspect credential sources, then \
`codewhale auth set --provider {provider}` to save a valid key in ~/.deepseek/config.toml, \
`codewhale auth set --provider {provider}` to save a valid key in ~/.codewhale/config.toml, \
or remove the stale export and open a fresh shell.",
provider = provider.as_str()
))
+5 -5
View File
@@ -1256,7 +1256,7 @@ fn english(id: MessageId) -> &'static str {
}
MessageId::OnboardApiKeyStep2 => "Step 2. Paste it below and press Enter.",
MessageId::OnboardApiKeySavedHint => {
"Saved to ~/.deepseek/config.toml so it works from any folder."
"Saved to ~/.codewhale/config.toml so it works from any folder."
}
MessageId::OnboardApiKeyFormatHint => {
"Paste the full key exactly as issued (no spaces or newlines)."
@@ -2100,7 +2100,7 @@ fn japanese(id: MessageId) -> Option<&'static str> {
}
MessageId::OnboardApiKeyStep2 => "ステップ 2. 下に貼り付けて Enter を押してください。",
MessageId::OnboardApiKeySavedHint => {
"~/.deepseek/config.toml に保存されるので、どのフォルダからでも有効になります。"
"~/.codewhale/config.toml に保存されるので、どのフォルダからでも有効になります。"
}
MessageId::OnboardApiKeyFormatHint => {
"発行されたキーをそのまま貼り付けてください(空白や改行を含めない)。"
@@ -2447,7 +2447,7 @@ fn chinese_simplified(id: MessageId) -> Option<&'static str> {
}
MessageId::OnboardApiKeyStep2 => "步骤 2. 把密钥粘贴到下方并按 Enter。",
MessageId::OnboardApiKeySavedHint => {
"保存到 ~/.deepseek/config.toml,因此在任何目录下都生效。"
"保存到 ~/.codewhale/config.toml,因此在任何目录下都生效。"
}
MessageId::OnboardApiKeyFormatHint => "请完整粘贴密钥(不要含空格或换行)。",
MessageId::OnboardApiKeyPlaceholder => "(在此粘贴密钥)",
@@ -2856,7 +2856,7 @@ fn portuguese_brazil(id: MessageId) -> Option<&'static str> {
}
MessageId::OnboardApiKeyStep2 => "Passo 2. Cole abaixo e pressione Enter.",
MessageId::OnboardApiKeySavedHint => {
"Salvo em ~/.deepseek/config.toml para funcionar em qualquer pasta."
"Salvo em ~/.codewhale/config.toml para funcionar em qualquer pasta."
}
MessageId::OnboardApiKeyFormatHint => {
"Cole a chave inteira como foi emitida (sem espaços ou quebras de linha)."
@@ -3283,7 +3283,7 @@ fn spanish_latin_america(id: MessageId) -> Option<&'static str> {
}
MessageId::OnboardApiKeyStep2 => "Paso 2. Pégala abajo y presiona Enter.",
MessageId::OnboardApiKeySavedHint => {
"Guardada en ~/.deepseek/config.toml para funcionar en cualquier carpeta."
"Guardada en ~/.codewhale/config.toml para funcionar en cualquier carpeta."
}
MessageId::OnboardApiKeyFormatHint => {
"Pega la clave completa tal como fue emitida (sin espacios ni saltos de línea)."
+7 -7
View File
@@ -1932,7 +1932,7 @@ fn run_setup_status(config: &Config, workspace: &Path) -> Result<()> {
"codewhale auth set --provider openrouter --api-key \"...\"",
),
crate::config::ApiProvider::XiaomiMimo => (
"XIAOMI_MIMO_API_KEY/MIMO_API_KEY",
"XIAOMI_MIMO_API_KEY/XIAOMI_API_KEY/MIMO_API_KEY",
"codewhale auth set --provider xiaomi-mimo --api-key \"...\"",
),
crate::config::ApiProvider::Novita => (
@@ -1971,7 +1971,7 @@ fn run_setup_status(config: &Config, workspace: &Path) -> Result<()> {
}
};
println!(
" {} api_key: missing (set {env_var} or `[providers.{}].api_key` in ~/.deepseek/config.toml; or run `{login_hint}`)",
" {} api_key: missing (set {env_var} or `[providers.{}].api_key` in ~/.codewhale/config.toml; or run `{login_hint}`)",
"".truecolor(red_r, red_g, red_b),
match config.api_provider() {
crate::config::ApiProvider::NvidiaNim => "nvidia_nim",
@@ -2291,7 +2291,7 @@ async fn run_doctor(config: &Config, workspace: &Path, config_path_override: Opt
(
crate::config::ApiProvider::XiaomiMimo,
"xiaomi-mimo",
&["XIAOMI_MIMO_API_KEY", "MIMO_API_KEY"][..],
&["XIAOMI_MIMO_API_KEY", "XIAOMI_API_KEY", "MIMO_API_KEY"][..],
),
(
crate::config::ApiProvider::Novita,
@@ -2361,7 +2361,7 @@ async fn run_doctor(config: &Config, workspace: &Path, config_path_override: Opt
if in_config { "yes" } else { "no" }
);
}
println!(" · credential precedence: ~/.deepseek/config.toml, OS keyring, then env");
println!(" · credential precedence: ~/.codewhale/config.toml, OS keyring, then env");
let api_key_source = resolve_api_key_source(config);
let has_api_key = if config.deepseek_api_key().is_ok() {
@@ -2392,7 +2392,7 @@ async fn run_doctor(config: &Config, workspace: &Path, config_path_override: Opt
"".truecolor(red_r, red_g, red_b)
);
println!(
" Run 'codewhale auth set --provider <name>' to save a key to ~/.deepseek/config.toml."
" Run 'codewhale auth set --provider <name>' to save a key to ~/.codewhale/config.toml."
);
false
};
@@ -3435,7 +3435,7 @@ fn doctor_timeout_recovery_lines(config: &Config) -> Vec<String> {
&& !target.base_url.contains("api.deepseeki.com") =>
{
lines.push(
"If this is a custom DeepSeek-compatible endpoint, set its HTTPS base URL in ~/.deepseek/config.toml and rerun `codewhale doctor`."
"If this is a custom DeepSeek-compatible endpoint, set its HTTPS base URL in ~/.codewhale/config.toml and rerun `codewhale doctor`."
.to_string(),
);
}
@@ -4870,7 +4870,7 @@ fn merge_project_config(config: &mut Config, workspace: &Path) {
if table.contains_key(*key) {
eprintln!(
"warning: project-scope config key `{key}` is ignored — \
set it in `~/.deepseek/config.toml` instead. \
set it in `~/.codewhale/config.toml` instead. \
(See #417 for the deny-list rationale.)"
);
}
+1 -1
View File
@@ -103,7 +103,7 @@ impl ProviderPickerView {
ApiProvider::WanjieArk => "WANJIE_ARK_API_KEY",
ApiProvider::Volcengine => "VOLCENGINE_API_KEY",
ApiProvider::Openrouter => "OPENROUTER_API_KEY",
ApiProvider::XiaomiMimo => "XIAOMI_MIMO_API_KEY / MIMO_API_KEY",
ApiProvider::XiaomiMimo => "XIAOMI_MIMO_API_KEY / XIAOMI_API_KEY / MIMO_API_KEY",
ApiProvider::Novita => "NOVITA_API_KEY",
ApiProvider::Fireworks => "FIREWORKS_API_KEY",
ApiProvider::Siliconflow => "SILICONFLOW_API_KEY",
+2 -2
View File
@@ -4216,7 +4216,7 @@ pub(crate) fn apply_engine_error_to_app(
app.onboarding_needs_api_key = true;
app.onboarding = OnboardingState::ApiKey;
app.status_message = Some(
"The API key from DEEPSEEK_API_KEY was rejected. Paste a valid key to save it to ~/.deepseek/config.toml, or update the environment variable.".to_string(),
"The API key from DEEPSEEK_API_KEY was rejected. Paste a valid key to save it to ~/.codewhale/config.toml, or update the environment variable.".to_string(),
);
return;
}
@@ -7045,7 +7045,7 @@ fn apply_backtrack(app: &mut App, depth: usize) {
app.needs_redraw = true;
}
/// Persist the typed API key to `~/.deepseek/config.toml`, refresh the
/// Persist the typed API key to `~/.codewhale/config.toml`, refresh the
/// in-memory config so the engine can see it, then switch to the provider.
async fn apply_provider_picker_api_key(
app: &mut App,
+3 -3
View File
@@ -78,7 +78,7 @@ the resolved key, base URL, provider, and model to the TUI process. Use
`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 xiaomi-mimo --api-key "YOUR_XIAOMI_MIMO_API_KEY"` or
`codewhale auth set --provider xiaomi-mimo --api-key "YOUR_XIAOMI_KEY"` or
`codewhale auth set --provider fireworks --api-key "YOUR_FIREWORKS_API_KEY"` or
`codewhale auth set --provider siliconflow --api-key "YOUR_SILICONFLOW_API_KEY"`
to save provider keys through the facade. The generic `openai` provider defaults
@@ -155,7 +155,7 @@ vision_model = true
[vision_model]
model = "mimo-v2.5"
api_key = "YOUR_XIAOMI_MIMO_API_KEY"
api_key = "YOUR_XIAOMI_KEY"
base_url = "https://api.xiaomimimo.com/v1"
```
@@ -269,7 +269,7 @@ Remaining variables:
- `WANJIE_ARK_MODEL`, `WANJIE_MODEL`, or `WANJIE_MAAS_MODEL`
- `OPENROUTER_API_KEY`
- `OPENROUTER_BASE_URL`
- `XIAOMI_MIMO_API_KEY` or `MIMO_API_KEY`
- `XIAOMI_MIMO_API_KEY`, `XIAOMI_API_KEY`, or `MIMO_API_KEY`
- `XIAOMI_MIMO_BASE_URL` or `MIMO_BASE_URL`
- `XIAOMI_MIMO_MODEL` or `MIMO_MODEL`
- `NOVITA_API_KEY`
+1 -1
View File
@@ -118,7 +118,7 @@ endpoint.
| `wanjie-ark` | `[providers.wanjie_ark]` | `WANJIE_ARK_API_KEY`, `WANJIE_API_KEY`, `WANJIE_MAAS_API_KEY` | `WANJIE_ARK_BASE_URL`, `WANJIE_BASE_URL`, `WANJIE_MAAS_BASE_URL`; default `https://maas-openapi.wanjiedata.com/api/v1` | `deepseek-reasoner` | OpenAI-compatible hosted route. `WANJIE_ARK_MODEL`, `WANJIE_MODEL`, and `WANJIE_MAAS_MODEL` are accepted. |
| `volcengine` | `[providers.volcengine]` | `VOLCENGINE_API_KEY`, `VOLCENGINE_ARK_API_KEY`, `ARK_API_KEY` | `VOLCENGINE_BASE_URL`, `VOLCENGINE_ARK_BASE_URL`, `ARK_BASE_URL`; default `https://ark.cn-beijing.volces.com/api/coding/v3` | `DeepSeek-V4-Pro`, `DeepSeek-V4-Flash` | Volcengine/Volcano Engine Ark OpenAI-compatible coding endpoint. `VOLCENGINE_MODEL` and `VOLCENGINE_ARK_MODEL` are accepted. |
| `openrouter` | `[providers.openrouter]` | `OPENROUTER_API_KEY` | `OPENROUTER_BASE_URL`; default `https://openrouter.ai/api/v1` | `deepseek/deepseek-v4-pro`, `deepseek/deepseek-v4-flash`; recent large IDs include `arcee-ai/trinity-large-thinking`, `qwen/qwen3.7-max`, `xiaomi/mimo-v2.5-pro`, `qwen/qwen3.6-35b-a3b`, `google/gemma-4-31b-it`, `z-ai/glm-5.1`, `moonshotai/kimi-k2.6` | Additive open-model routing layer. It does not replace DeepSeek; it lets users route supported model IDs through OpenRouter when they choose it. |
| `xiaomi-mimo` | `[providers.xiaomi_mimo]` | `XIAOMI_MIMO_API_KEY`, `MIMO_API_KEY` | `XIAOMI_MIMO_BASE_URL`, `MIMO_BASE_URL`; default `https://api.xiaomimimo.com/v1` | `mimo-v2.5-pro`, `mimo-v2.5` | Xiaomi MiMo OpenAI-compatible chat completions route. It sends `max_completion_tokens` and uses MiMo's `thinking` field for reasoning control. |
| `xiaomi-mimo` | `[providers.xiaomi_mimo]` | `XIAOMI_MIMO_API_KEY`, `XIAOMI_API_KEY`, `MIMO_API_KEY` | `XIAOMI_MIMO_BASE_URL`, `MIMO_BASE_URL`; default `https://api.xiaomimimo.com/v1` | `mimo-v2.5-pro`, `mimo-v2.5` | Xiaomi MiMo OpenAI-compatible chat completions route. It sends `max_completion_tokens` and uses MiMo's `thinking` field for reasoning control. |
| `novita` | `[providers.novita]` | `NOVITA_API_KEY` | `NOVITA_BASE_URL`; default `https://api.novita.ai/v1` | `deepseek/deepseek-v4-pro`, `deepseek/deepseek-v4-flash` | OpenAI-compatible hosted route for DeepSeek model IDs. Use config or `CODEWHALE_MODEL` / `DEEPSEEK_MODEL` for model overrides. |
| `fireworks` | `[providers.fireworks]` | `FIREWORKS_API_KEY` | `FIREWORKS_BASE_URL`; default `https://api.fireworks.ai/inference/v1` | `accounts/fireworks/models/deepseek-v4-pro` | OpenAI-compatible hosted route. Use config or `CODEWHALE_MODEL` / `DEEPSEEK_MODEL` for model overrides. |
| `siliconflow` | `[providers.siliconflow]` | `SILICONFLOW_API_KEY` | `SILICONFLOW_BASE_URL`; default `https://api.siliconflow.com/v1` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | OpenAI-compatible hosted route. Official docs use the `.com` endpoint; users who need the regional endpoint can set `https://api.siliconflow.cn/v1` explicitly. `SILICONFLOW_MODEL` is accepted. Reasoning aliases `deepseek-reasoner` and `deepseek-r1` map to Pro; `deepseek-chat` and `deepseek-v3` map to Flash. |