Migrate file secrets to codewhale home
This commit is contained in:
@@ -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
@@ -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"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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"]),
|
||||
(
|
||||
|
||||
@@ -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
@@ -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
@@ -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)?;
|
||||
|
||||
@@ -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()
|
||||
))
|
||||
|
||||
@@ -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)."
|
||||
|
||||
@@ -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.)"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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. |
|
||||
|
||||
Reference in New Issue
Block a user