feat(provider): add direct arcee support

This commit is contained in:
Hunter B
2026-06-02 08:51:39 -07:00
parent 0072209d12
commit e54a0a500b
16 changed files with 589 additions and 21 deletions
+7
View File
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Added Arcee AI as a direct OpenAI-compatible provider with `[providers.arcee]`,
`ARCEE_API_KEY` / `ARCEE_BASE_URL` / `ARCEE_MODEL`, `trinity-mini` as the
default model, and `trinity-large-preview` as the documented direct API model.
OpenRouter's `arcee-ai/trinity-large-thinking` route remains separate.
## [0.8.50] - 2026-06-02
### Added
+8 -2
View File
@@ -320,6 +320,11 @@ codewhale --provider openrouter --model deepseek/deepseek-v4-pro
codewhale --provider openrouter --model arcee-ai/trinity-large-thinking
codewhale --provider openrouter --model minimax/minimax-m3
# Arcee AI direct API
codewhale auth set --provider arcee --api-key "YOUR_ARCEE_API_KEY"
codewhale --provider arcee --model trinity-mini
codewhale --provider arcee --model trinity-large-preview
# Xiaomi MiMo
codewhale auth set --provider xiaomi-mimo --api-key "YOUR_XIAOMI_KEY"
codewhale --provider xiaomi-mimo --model mimo-v2.5-pro
@@ -524,11 +529,11 @@ Key environment variables:
| `DEEPSEEK_HTTP_HEADERS` | Optional custom model request headers, e.g. `X-Model-Provider-Id=your-model-provider` |
| `DEEPSEEK_MODEL` | Default model |
| `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` | Stream idle timeout in seconds, default `300`, clamped to `1..=3600` |
| `CODEWHALE_PROVIDER` / `DEEPSEEK_PROVIDER` | `deepseek` (default), `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `moonshot`, `sglang`, `vllm`, `ollama` |
| `CODEWHALE_PROVIDER` / `DEEPSEEK_PROVIDER` | `deepseek` (default), `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `arcee`, `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 |
| `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 |
| `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` / `ARCEE_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 |
@@ -537,6 +542,7 @@ Key environment variables:
| `NOVITA_BASE_URL` | Novita endpoint override |
| `FIREWORKS_BASE_URL` | Fireworks endpoint override |
| `SILICONFLOW_BASE_URL` / `SILICONFLOW_MODEL` | SiliconFlow endpoint and model override |
| `ARCEE_BASE_URL` / `ARCEE_MODEL` | Arcee AI endpoint and model override |
| `SGLANG_BASE_URL` | Self-hosted SGLang endpoint |
| `SGLANG_MODEL` | Self-hosted SGLang model ID |
| `VLLM_BASE_URL` | Self-hosted vLLM endpoint |
+16 -7
View File
@@ -14,12 +14,12 @@
# this file — keeping both stored at once means `/provider deepseek` and
# `/provider nvidia-nim` (or `--provider openai`, `--provider wanjie-ark`,
# `--provider volcengine`, `--provider xiaomi-mimo`, `--provider fireworks`,
# `--provider siliconflow`, `/provider moonshot`, `/provider sglang`,
# `--provider siliconflow`, `/provider arcee`, `/provider moonshot`, `/provider sglang`,
# `/provider vllm`, `/provider ollama`) toggle without having to re-enter keys. Top-level
# `api_key` / `base_url` are
# still read as DeepSeek defaults when `[providers.deepseek]` is absent
# (backward compatibility).
provider = "deepseek" # deepseek | deepseek-cn | nvidia-nim | openai | atlascloud | wanjie-ark | volcengine | openrouter | xiaomi-mimo | novita | fireworks | siliconflow | moonshot | sglang | vllm | ollama
provider = "deepseek" # deepseek | deepseek-cn | nvidia-nim | openai | atlascloud | wanjie-ark | volcengine | openrouter | xiaomi-mimo | novita | fireworks | siliconflow | arcee | moonshot | sglang | vllm | ollama
api_key = "YOUR_DEEPSEEK_API_KEY" # must be non-empty
base_url = "https://api.deepseek.com/beta"
# provider = "deepseek-cn" # legacy alias (official host is still https://api.deepseek.com)
@@ -49,10 +49,12 @@ base_url = "https://api.deepseek.com/beta"
# mimo-v2.5-tts-voicedesign ? Xiaomi MiMo voice-design TTS model ID
# mimo-v2.5-tts-voiceclone ? Xiaomi MiMo voice-clone TTS model ID
# accounts/fireworks/models/deepseek-v4-pro — Fireworks AI Pro model ID
# deepseek-ai/DeepSeek-V4-Pro — SiliconFlow hosted Pro model ID
# deepseek-ai/DeepSeek-V4-Flash — SiliconFlow hosted Flash model ID
# deepseek-ai/DeepSeek-V4-Pro — SGLang self-hosted Pro model ID
# deepseek-ai/DeepSeek-V4-Flash — SGLang self-hosted Flash model ID
# deepseek-ai/DeepSeek-V4-Pro — SiliconFlow hosted Pro model ID
# deepseek-ai/DeepSeek-V4-Flash — SiliconFlow hosted Flash model ID
# trinity-mini — default direct Arcee AI API model ID
# trinity-large-preview — direct Arcee AI API model ID
# deepseek-ai/DeepSeek-V4-Pro — SGLang self-hosted Pro model ID
# deepseek-ai/DeepSeek-V4-Flash — SGLang self-hosted Flash model ID
default_text_model = "deepseek-v4-pro"
# ─────────────────────────────────────────────────────────────────────────────────
@@ -225,7 +227,7 @@ max_subagents = 10 # optional (1-20)
# Providers can be stored at once; `provider = "..."` (top of file) or
# `/provider deepseek` / `/provider nvidia-nim` / `--provider openai` /
# `--provider wanjie-ark` / `/provider volcengine` / `/provider fireworks` /
# `--provider siliconflow` / `/provider moonshot`
# `--provider siliconflow` / `/provider arcee` / `/provider moonshot`
# switches between them without having to re-enter keys. Env vars override anything set here:
# DeepSeek: DEEPSEEK_API_KEY, DEEPSEEK_BASE_URL, DEEPSEEK_MODEL
# NIM: NVIDIA_API_KEY (or NVIDIA_NIM_API_KEY), NIM_BASE_URL
@@ -238,6 +240,7 @@ max_subagents = 10 # optional (1-20)
# 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
# Arcee: ARCEE_API_KEY, ARCEE_BASE_URL, ARCEE_MODEL
# Moonshot/Kimi: MOONSHOT_API_KEY (or KIMI_API_KEY), MOONSHOT_BASE_URL, MOONSHOT_MODEL
# SGLang: SGLANG_BASE_URL, SGLANG_MODEL, optional SGLANG_API_KEY
# vLLM: VLLM_BASE_URL, VLLM_MODEL, optional VLLM_API_KEY
@@ -331,6 +334,12 @@ max_subagents = 10 # optional (1-20)
# base_url = "https://api.siliconflow.com/v1"
# model = "deepseek-ai/DeepSeek-V4-Pro" # or deepseek-ai/DeepSeek-V4-Flash
# Arcee AI direct OpenAI-compatible endpoint (https://docs.arcee.ai)
[providers.arcee]
# api_key = "YOUR_ARCEE_API_KEY"
# base_url = "https://api.arcee.ai/api/v1"
# model = "trinity-mini" # or trinity-large-preview
# Moonshot/Kimi OpenAI-compatible endpoint (https://platform.moonshot.ai)
[providers.moonshot]
# api_key = "YOUR_MOONSHOT_API_KEY" # or KIMI_API_KEY
+74
View File
@@ -403,6 +403,24 @@ impl Default for ModelRegistry {
supports_tools: true,
supports_reasoning: true,
},
ModelInfo {
id: "trinity-mini".to_string(),
provider: ProviderKind::Arcee,
aliases: vec![
"trinity".to_string(),
"arcee-trinity".to_string(),
"arcee-trinity-mini".to_string(),
],
supports_tools: true,
supports_reasoning: false,
},
ModelInfo {
id: "trinity-large-preview".to_string(),
provider: ProviderKind::Arcee,
aliases: vec!["arcee-trinity-large-preview".to_string()],
supports_tools: true,
supports_reasoning: false,
},
ModelInfo {
id: "kimi-k2.6".to_string(),
provider: ProviderKind::Moonshot,
@@ -553,6 +571,16 @@ impl ModelRegistry {
fallback_chain,
};
}
if provider_hint == Some(ProviderKind::Arcee)
&& let Some(model) = arcee_passthrough_model(name)
{
return ModelResolution {
requested: Some(name.to_string()),
resolved: model,
used_fallback: false,
fallback_chain,
};
}
if let Some(idx) = self.alias_map.get(&normalize(name)) {
return ModelResolution {
requested: Some(name.to_string()),
@@ -627,6 +655,22 @@ fn atlascloud_passthrough_model(requested: &str) -> Option<ModelInfo> {
})
}
fn arcee_passthrough_model(requested: &str) -> Option<ModelInfo> {
let requested = requested.trim();
if requested.is_empty() {
return None;
}
let supports_reasoning = requested.to_ascii_lowercase().contains("thinking");
Some(ModelInfo {
id: requested.to_string(),
provider: ProviderKind::Arcee,
aliases: Vec::new(),
supports_tools: true,
supports_reasoning,
})
}
#[cfg(test)]
mod tests {
use super::*;
@@ -804,6 +848,36 @@ mod tests {
assert!(resolved.resolved.supports_reasoning);
}
#[test]
fn arcee_default_uses_direct_trinity_mini_model_id() {
let registry = ModelRegistry::default();
let resolved = registry.resolve(None, Some(ProviderKind::Arcee));
assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
assert_eq!(resolved.resolved.id, "trinity-mini");
}
#[test]
fn arcee_trinity_alias_resolves_to_direct_provider_not_openrouter() {
let registry = ModelRegistry::default();
let resolved = registry.resolve(Some("trinity"), Some(ProviderKind::Arcee));
assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
assert_eq!(resolved.resolved.id, "trinity-mini");
assert!(!resolved.resolved.supports_reasoning);
}
#[test]
fn arcee_provider_hint_preserves_explicit_future_model_id() {
let registry = ModelRegistry::default();
let resolved = registry.resolve(Some("trinity-large-thinking"), Some(ProviderKind::Arcee));
assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
assert_eq!(resolved.resolved.id, "trinity-large-thinking");
assert!(resolved.resolved.supports_reasoning);
assert!(!resolved.used_fallback);
}
#[test]
fn deepseek_reasoner_alias_resolves_to_siliconflow_pro_when_provider_hinted() {
let registry = ModelRegistry::default();
+136
View File
@@ -53,6 +53,8 @@ const DEFAULT_NOVITA_FLASH_MODEL: &str = "deepseek/deepseek-v4-flash";
const DEFAULT_FIREWORKS_MODEL: &str = "accounts/fireworks/models/deepseek-v4-pro";
const DEFAULT_SILICONFLOW_MODEL: &str = "deepseek-ai/DeepSeek-V4-Pro";
const DEFAULT_SILICONFLOW_FLASH_MODEL: &str = "deepseek-ai/DeepSeek-V4-Flash";
const DEFAULT_ARCEE_MODEL: &str = "trinity-mini";
const ARCEE_TRINITY_LARGE_PREVIEW_MODEL: &str = "trinity-large-preview";
const DEFAULT_MOONSHOT_MODEL: &str = "kimi-k2.6";
const DEFAULT_MOONSHOT_BASE_URL: &str = "https://api.moonshot.ai/v1";
const DEFAULT_KIMI_CODE_MODEL: &str = "kimi-for-coding";
@@ -64,6 +66,7 @@ const DEFAULT_XIAOMI_MIMO_BASE_URL: &str = "https://api.xiaomimimo.com/v1";
const DEFAULT_NOVITA_BASE_URL: &str = "https://api.novita.ai/v1";
const DEFAULT_FIREWORKS_BASE_URL: &str = "https://api.fireworks.ai/inference/v1";
const DEFAULT_SILICONFLOW_BASE_URL: &str = "https://api.siliconflow.com/v1";
const DEFAULT_ARCEE_BASE_URL: &str = "https://api.arcee.ai/api/v1";
const DEFAULT_SGLANG_BASE_URL: &str = "http://localhost:30000/v1";
const DEFAULT_VLLM_MODEL: &str = "deepseek-ai/DeepSeek-V4-Pro";
const DEFAULT_VLLM_FLASH_MODEL: &str = "deepseek-ai/DeepSeek-V4-Flash";
@@ -104,6 +107,8 @@ pub enum ProviderKind {
Fireworks,
#[serde(alias = "silicon-flow", alias = "silicon_flow")]
Siliconflow,
#[serde(alias = "arcee-ai", alias = "arcee_ai")]
Arcee,
Moonshot,
Sglang,
Vllm,
@@ -125,6 +130,7 @@ impl ProviderKind {
Self::Novita => "novita",
Self::Fireworks => "fireworks",
Self::Siliconflow => "siliconflow",
Self::Arcee => "arcee",
Self::Moonshot => "moonshot",
Self::Sglang => "sglang",
Self::Vllm => "vllm",
@@ -151,6 +157,7 @@ impl ProviderKind {
"novita" => Some(Self::Novita),
"fireworks" | "fireworks-ai" => Some(Self::Fireworks),
"siliconflow" | "silicon-flow" | "silicon_flow" => Some(Self::Siliconflow),
"arcee" | "arcee-ai" | "arcee_ai" => Some(Self::Arcee),
"moonshot" | "moonshot-ai" | "kimi" | "kimi-k2" => Some(Self::Moonshot),
"sglang" | "sg-lang" => Some(Self::Sglang),
"vllm" | "v-llm" => Some(Self::Vllm),
@@ -195,6 +202,8 @@ pub struct ProvidersToml {
#[serde(default)]
pub siliconflow: ProviderConfigToml,
#[serde(default)]
pub arcee: ProviderConfigToml,
#[serde(default)]
pub moonshot: ProviderConfigToml,
#[serde(default)]
pub sglang: ProviderConfigToml,
@@ -238,6 +247,7 @@ impl ProvidersToml {
ProviderKind::Novita => &self.novita,
ProviderKind::Fireworks => &self.fireworks,
ProviderKind::Siliconflow => &self.siliconflow,
ProviderKind::Arcee => &self.arcee,
ProviderKind::Moonshot => &self.moonshot,
ProviderKind::Sglang => &self.sglang,
ProviderKind::Vllm => &self.vllm,
@@ -258,6 +268,7 @@ impl ProvidersToml {
ProviderKind::Novita => &mut self.novita,
ProviderKind::Fireworks => &mut self.fireworks,
ProviderKind::Siliconflow => &mut self.siliconflow,
ProviderKind::Arcee => &mut self.arcee,
ProviderKind::Moonshot => &mut self.moonshot,
ProviderKind::Sglang => &mut self.sglang,
ProviderKind::Vllm => &mut self.vllm,
@@ -501,6 +512,7 @@ impl ConfigToml {
&mut self.providers.siliconflow,
&project.providers.siliconflow,
);
merge_project_provider_config(&mut self.providers.arcee, &project.providers.arcee);
merge_project_provider_config(&mut self.providers.sglang, &project.providers.sglang);
merge_project_provider_config(&mut self.providers.vllm, &project.providers.vllm);
merge_project_provider_config(&mut self.providers.ollama, &project.providers.ollama);
@@ -590,6 +602,12 @@ impl ConfigToml {
"providers.siliconflow.http_headers" => {
serialize_http_headers(&self.providers.siliconflow.http_headers)
}
"providers.arcee.api_key" => self.providers.arcee.api_key.clone(),
"providers.arcee.base_url" => self.providers.arcee.base_url.clone(),
"providers.arcee.model" => self.providers.arcee.model.clone(),
"providers.arcee.http_headers" => {
serialize_http_headers(&self.providers.arcee.http_headers)
}
"providers.moonshot.api_key" => self.providers.moonshot.api_key.clone(),
"providers.moonshot.base_url" => self.providers.moonshot.base_url.clone(),
"providers.moonshot.model" => self.providers.moonshot.model.clone(),
@@ -785,6 +803,18 @@ impl ConfigToml {
"providers.siliconflow.http_headers" => {
self.providers.siliconflow.http_headers = parse_http_headers(value)?;
}
"providers.arcee.api_key" => {
self.providers.arcee.api_key = Some(value.to_string());
}
"providers.arcee.base_url" => {
self.providers.arcee.base_url = Some(value.to_string());
}
"providers.arcee.model" => {
self.providers.arcee.model = Some(value.to_string());
}
"providers.arcee.http_headers" => {
self.providers.arcee.http_headers = parse_http_headers(value)?;
}
"providers.moonshot.api_key" => {
self.providers.moonshot.api_key = Some(value.to_string());
}
@@ -924,6 +954,12 @@ impl ConfigToml {
"providers.siliconflow.http_headers" => {
self.providers.siliconflow.http_headers.clear();
}
"providers.arcee.api_key" => self.providers.arcee.api_key = None,
"providers.arcee.base_url" => self.providers.arcee.base_url = None,
"providers.arcee.model" => self.providers.arcee.model = None,
"providers.arcee.http_headers" => {
self.providers.arcee.http_headers.clear();
}
"providers.moonshot.api_key" => self.providers.moonshot.api_key = None,
"providers.moonshot.base_url" => self.providers.moonshot.base_url = None,
"providers.moonshot.model" => self.providers.moonshot.model = None,
@@ -1134,6 +1170,18 @@ impl ConfigToml {
if let Some(v) = serialize_http_headers(&self.providers.siliconflow.http_headers) {
out.insert("providers.siliconflow.http_headers".to_string(), v);
}
if let Some(v) = self.providers.arcee.api_key.as_ref() {
out.insert("providers.arcee.api_key".to_string(), redact_secret(v));
}
if let Some(v) = self.providers.arcee.base_url.as_ref() {
out.insert("providers.arcee.base_url".to_string(), v.clone());
}
if let Some(v) = self.providers.arcee.model.as_ref() {
out.insert("providers.arcee.model".to_string(), v.clone());
}
if let Some(v) = serialize_http_headers(&self.providers.arcee.http_headers) {
out.insert("providers.arcee.http_headers".to_string(), v);
}
if let Some(v) = self.providers.moonshot.api_key.as_ref() {
out.insert("providers.moonshot.api_key".to_string(), redact_secret(v));
}
@@ -1252,6 +1300,7 @@ impl ConfigToml {
ProviderKind::Novita => DEFAULT_NOVITA_BASE_URL.to_string(),
ProviderKind::Fireworks => DEFAULT_FIREWORKS_BASE_URL.to_string(),
ProviderKind::Siliconflow => DEFAULT_SILICONFLOW_BASE_URL.to_string(),
ProviderKind::Arcee => DEFAULT_ARCEE_BASE_URL.to_string(),
ProviderKind::Moonshot => {
if auth_mode.as_deref().is_some_and(auth_mode_uses_kimi_oauth) {
DEFAULT_KIMI_CODE_BASE_URL.to_string()
@@ -1510,6 +1559,12 @@ fn normalize_model_for_provider(provider: ProviderKind, model: &str) -> String {
ProviderKind::Siliconflow,
"deepseek-v4-flash" | "deepseek-v4flash" | "deepseek-chat" | "deepseek-v3",
) => DEFAULT_SILICONFLOW_FLASH_MODEL.to_string(),
(ProviderKind::Arcee, "trinity" | "arcee-trinity" | "arcee-trinity-mini") => {
DEFAULT_ARCEE_MODEL.to_string()
}
(ProviderKind::Arcee, "arcee-trinity-large-preview") => {
ARCEE_TRINITY_LARGE_PREVIEW_MODEL.to_string()
}
(ProviderKind::Moonshot, "kimi-k2.6" | "kimi-k2") => DEFAULT_MOONSHOT_MODEL.to_string(),
(ProviderKind::Sglang, "deepseek-v4-pro" | "deepseek-v4pro") => {
DEFAULT_SGLANG_MODEL.to_string()
@@ -1624,6 +1679,7 @@ fn default_model_for_provider(provider: ProviderKind) -> &'static str {
ProviderKind::Novita => DEFAULT_NOVITA_MODEL,
ProviderKind::Fireworks => DEFAULT_FIREWORKS_MODEL,
ProviderKind::Siliconflow => DEFAULT_SILICONFLOW_MODEL,
ProviderKind::Arcee => DEFAULT_ARCEE_MODEL,
ProviderKind::Moonshot => DEFAULT_MOONSHOT_MODEL,
ProviderKind::Sglang => DEFAULT_SGLANG_MODEL,
ProviderKind::Vllm => DEFAULT_VLLM_MODEL,
@@ -1644,6 +1700,7 @@ fn default_base_url_for_provider(provider: ProviderKind) -> &'static str {
ProviderKind::Novita => DEFAULT_NOVITA_BASE_URL,
ProviderKind::Fireworks => DEFAULT_FIREWORKS_BASE_URL,
ProviderKind::Siliconflow => DEFAULT_SILICONFLOW_BASE_URL,
ProviderKind::Arcee => DEFAULT_ARCEE_BASE_URL,
ProviderKind::Moonshot => DEFAULT_MOONSHOT_BASE_URL,
ProviderKind::Sglang => DEFAULT_SGLANG_BASE_URL,
ProviderKind::Vllm => DEFAULT_VLLM_BASE_URL,
@@ -2209,6 +2266,7 @@ struct EnvRuntimeOverrides {
wanjie_ark_model: Option<String>,
moonshot_model: Option<String>,
xiaomi_mimo_model: Option<String>,
arcee_model: Option<String>,
output_mode: Option<String>,
auth_mode: Option<String>,
log_level: Option<String>,
@@ -2229,6 +2287,7 @@ struct EnvRuntimeOverrides {
fireworks_base_url: Option<String>,
siliconflow_base_url: Option<String>,
siliconflow_model: Option<String>,
arcee_base_url: Option<String>,
moonshot_base_url: Option<String>,
sglang_base_url: Option<String>,
vllm_base_url: Option<String>,
@@ -2265,6 +2324,9 @@ impl EnvRuntimeOverrides {
.or_else(|_| std::env::var("MIMO_MODEL"))
.ok()
.filter(|v| !v.trim().is_empty()),
arcee_model: std::env::var("ARCEE_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
output_mode: std::env::var("DEEPSEEK_OUTPUT_MODE").ok(),
auth_mode: std::env::var("DEEPSEEK_AUTH_MODE").ok(),
log_level: std::env::var("DEEPSEEK_LOG_LEVEL").ok(),
@@ -2324,6 +2386,9 @@ impl EnvRuntimeOverrides {
siliconflow_model: std::env::var("SILICONFLOW_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
arcee_base_url: std::env::var("ARCEE_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
moonshot_base_url: std::env::var("MOONSHOT_BASE_URL")
.or_else(|_| std::env::var("KIMI_BASE_URL"))
.ok()
@@ -2355,6 +2420,7 @@ impl EnvRuntimeOverrides {
ProviderKind::Novita => self.novita_base_url.clone(),
ProviderKind::Fireworks => self.fireworks_base_url.clone(),
ProviderKind::Siliconflow => self.siliconflow_base_url.clone(),
ProviderKind::Arcee => self.arcee_base_url.clone(),
ProviderKind::Moonshot => self.moonshot_base_url.clone(),
ProviderKind::Sglang => self.sglang_base_url.clone(),
ProviderKind::Vllm => self.vllm_base_url.clone(),
@@ -2367,6 +2433,7 @@ impl EnvRuntimeOverrides {
ProviderKind::WanjieArk => self.wanjie_ark_model.clone(),
ProviderKind::Volcengine => self.volcengine_model.clone(),
ProviderKind::Siliconflow => self.siliconflow_model.clone(),
ProviderKind::Arcee => self.arcee_model.clone(),
ProviderKind::Moonshot => self.moonshot_model.clone(),
ProviderKind::XiaomiMimo => self.xiaomi_mimo_model.clone(),
_ => None,
@@ -2566,6 +2633,9 @@ mod tests {
siliconflow_api_key: Option<OsString>,
siliconflow_base_url: Option<OsString>,
siliconflow_model: Option<OsString>,
arcee_api_key: Option<OsString>,
arcee_base_url: Option<OsString>,
arcee_model: Option<OsString>,
moonshot_api_key: Option<OsString>,
moonshot_base_url: Option<OsString>,
moonshot_model: Option<OsString>,
@@ -2626,6 +2696,9 @@ mod tests {
siliconflow_api_key: env::var_os("SILICONFLOW_API_KEY"),
siliconflow_base_url: env::var_os("SILICONFLOW_BASE_URL"),
siliconflow_model: env::var_os("SILICONFLOW_MODEL"),
arcee_api_key: env::var_os("ARCEE_API_KEY"),
arcee_base_url: env::var_os("ARCEE_BASE_URL"),
arcee_model: env::var_os("ARCEE_MODEL"),
moonshot_api_key: env::var_os("MOONSHOT_API_KEY"),
moonshot_base_url: env::var_os("MOONSHOT_BASE_URL"),
moonshot_model: env::var_os("MOONSHOT_MODEL"),
@@ -2680,6 +2753,9 @@ mod tests {
env::remove_var("SILICONFLOW_API_KEY");
env::remove_var("SILICONFLOW_BASE_URL");
env::remove_var("SILICONFLOW_MODEL");
env::remove_var("ARCEE_API_KEY");
env::remove_var("ARCEE_BASE_URL");
env::remove_var("ARCEE_MODEL");
env::remove_var("MOONSHOT_API_KEY");
env::remove_var("MOONSHOT_BASE_URL");
env::remove_var("MOONSHOT_MODEL");
@@ -2752,6 +2828,9 @@ mod tests {
Self::restore_var("SILICONFLOW_API_KEY", self.siliconflow_api_key.take());
Self::restore_var("SILICONFLOW_BASE_URL", self.siliconflow_base_url.take());
Self::restore_var("SILICONFLOW_MODEL", self.siliconflow_model.take());
Self::restore_var("ARCEE_API_KEY", self.arcee_api_key.take());
Self::restore_var("ARCEE_BASE_URL", self.arcee_base_url.take());
Self::restore_var("ARCEE_MODEL", self.arcee_model.take());
Self::restore_var("MOONSHOT_API_KEY", self.moonshot_api_key.take());
Self::restore_var("MOONSHOT_BASE_URL", self.moonshot_base_url.take());
Self::restore_var("MOONSHOT_MODEL", self.moonshot_model.take());
@@ -4131,6 +4210,63 @@ unix_socket_path = "/tmp/cw-hooks.sock"
assert_eq!(resolved.model, "deepseek-v4-flash");
}
#[test]
fn arcee_provider_defaults_to_direct_api_endpoint_and_model() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
let config = ConfigToml {
provider: ProviderKind::Arcee,
..ConfigToml::default()
};
let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.provider, ProviderKind::Arcee);
assert_eq!(resolved.base_url, DEFAULT_ARCEE_BASE_URL);
assert_eq!(resolved.model, DEFAULT_ARCEE_MODEL);
}
#[test]
fn arcee_env_overrides_key_base_url_and_model() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
// Safety: test-only environment mutation guarded by a module mutex.
unsafe {
env::set_var("CODEWHALE_PROVIDER", "arcee");
env::set_var("ARCEE_API_KEY", "arcee-env-key");
env::set_var("ARCEE_BASE_URL", "https://arcee-mirror.example/api/v1");
env::set_var("ARCEE_MODEL", "trinity-large-preview");
}
let resolved =
ConfigToml::default().resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.provider, ProviderKind::Arcee);
assert_eq!(resolved.api_key.as_deref(), Some("arcee-env-key"));
assert_eq!(resolved.base_url, "https://arcee-mirror.example/api/v1");
assert_eq!(resolved.model, "trinity-large-preview");
}
#[test]
fn arcee_provider_config_overrides_runtime_defaults() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
let mut config = ConfigToml {
provider: ProviderKind::Arcee,
..ConfigToml::default()
};
config.providers.arcee.api_key = Some("arcee-file-key".to_string());
config.providers.arcee.base_url = Some(DEFAULT_ARCEE_BASE_URL.to_string());
config.providers.arcee.model = Some("arcee-trinity-large-preview".to_string());
let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.provider, ProviderKind::Arcee);
assert_eq!(resolved.api_key.as_deref(), Some("arcee-file-key"));
assert_eq!(resolved.base_url, DEFAULT_ARCEE_BASE_URL);
assert_eq!(resolved.model, ARCEE_TRINITY_LARGE_PREVIEW_MODEL);
}
#[test]
fn siliconflow_cn_base_url_env_normalizes_model_aliases() {
let _lock = env_lock();
+17
View File
@@ -682,6 +682,7 @@ impl Secrets {
/// | `nvidia` / `nvidia-nim` / `nim` | `NVIDIA_API_KEY`, `NVIDIA_NIM_API_KEY`, `DEEPSEEK_API_KEY` |
/// | `fireworks` | `FIREWORKS_API_KEY` |
/// | `siliconflow` | `SILICONFLOW_API_KEY` |
/// | `arcee` / `arcee-ai` | `ARCEE_API_KEY` |
/// | `moonshot` / `kimi` | `MOONSHOT_API_KEY`, `KIMI_API_KEY` |
/// | `sglang` | `SGLANG_API_KEY` |
/// | `vllm` | `VLLM_API_KEY` |
@@ -710,6 +711,7 @@ pub fn env_for(name: &str) -> Option<String> {
}
"fireworks" | "fireworks-ai" => &["FIREWORKS_API_KEY"],
"siliconflow" | "silicon-flow" | "silicon_flow" => &["SILICONFLOW_API_KEY"],
"arcee" | "arcee-ai" | "arcee_ai" => &["ARCEE_API_KEY"],
"moonshot" | "moonshot-ai" | "kimi" | "kimi-k2" => &["MOONSHOT_API_KEY", "KIMI_API_KEY"],
"sglang" | "sg-lang" => &["SGLANG_API_KEY"],
"vllm" | "v-llm" => &["VLLM_API_KEY"],
@@ -764,6 +766,7 @@ mod tests {
"NVIDIA_NIM_API_KEY",
"FIREWORKS_API_KEY",
"SILICONFLOW_API_KEY",
"ARCEE_API_KEY",
"SGLANG_API_KEY",
"VLLM_API_KEY",
"OLLAMA_API_KEY",
@@ -1145,6 +1148,20 @@ mod tests {
unsafe { std::env::remove_var("SILICONFLOW_API_KEY") };
}
#[test]
fn arcee_env_aliases_resolve() {
let _lock = env_lock();
clear_known_envs();
// Safety: env mutation guarded by env_lock().
unsafe { std::env::set_var("ARCEE_API_KEY", "arcee-key") };
assert_eq!(env_for("arcee").as_deref(), Some("arcee-key"));
assert_eq!(env_for("arcee-ai").as_deref(), Some("arcee-key"));
assert_eq!(env_for("arcee_ai").as_deref(), Some("arcee-key"));
// Safety: env mutation guarded by env_lock().
unsafe { std::env::remove_var("ARCEE_API_KEY") };
}
#[test]
fn moonshot_kimi_env_aliases_resolve() {
let _lock = env_lock();
+7
View File
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Added Arcee AI as a direct OpenAI-compatible provider with `[providers.arcee]`,
`ARCEE_API_KEY` / `ARCEE_BASE_URL` / `ARCEE_MODEL`, `trinity-mini` as the
default model, and `trinity-large-preview` as the documented direct API model.
OpenRouter's `arcee-ai/trinity-large-thinking` route remains separate.
## [0.8.50] - 2026-06-02
### Added
+37
View File
@@ -1092,6 +1092,7 @@ pub(super) fn apply_reasoning_effort(
ApiProvider::Openai
| ApiProvider::Atlascloud
| ApiProvider::WanjieArk
| ApiProvider::Arcee
| ApiProvider::Moonshot
| ApiProvider::Ollama => {}
ApiProvider::NvidiaNim => {
@@ -1125,6 +1126,15 @@ pub(super) fn apply_reasoning_effort(
ApiProvider::XiaomiMimo => {
body["thinking"] = json!({ "type": "enabled" });
}
ApiProvider::Arcee => {
let value = match normalized.as_str() {
"minimal" => "minimal",
"low" => "low",
"medium" | "mid" => "medium",
_ => "high",
};
body["reasoning_effort"] = json!(value);
}
ApiProvider::Fireworks => {
body["reasoning_effort"] = json!("high");
}
@@ -1169,6 +1179,9 @@ pub(super) fn apply_reasoning_effort(
ApiProvider::XiaomiMimo => {
body["thinking"] = json!({ "type": "enabled" });
}
ApiProvider::Arcee => {
body["reasoning_effort"] = json!("high");
}
ApiProvider::Fireworks => {
body["reasoning_effort"] = json!("max");
}
@@ -2301,6 +2314,30 @@ mod tests {
);
}
#[test]
fn reasoning_effort_uses_arcee_reasoning_effort_without_thinking_object() {
for (input, expected) in [
("minimal", "minimal"),
("low", "low"),
("mid", "medium"),
("medium", "medium"),
("high", "high"),
("max", "high"),
] {
let mut body = json!({});
apply_reasoning_effort(&mut body, Some(input), ApiProvider::Arcee);
assert_eq!(
body.get("reasoning_effort").and_then(Value::as_str),
Some(expected)
);
assert!(
body.get("thinking").is_none(),
"Arcee documents reasoning_effort rather than a DeepSeek thinking object"
);
}
}
#[test]
fn reasoning_effort_maps_openrouter_scale_without_deepseek_max_label() {
for (input, expected) in [
+238 -1
View File
@@ -90,6 +90,9 @@ pub const DEFAULT_FIREWORKS_BASE_URL: &str = "https://api.fireworks.ai/inference
pub const DEFAULT_SILICONFLOW_MODEL: &str = "deepseek-ai/DeepSeek-V4-Pro";
pub const DEFAULT_SILICONFLOW_FLASH_MODEL: &str = "deepseek-ai/DeepSeek-V4-Flash";
pub const DEFAULT_SILICONFLOW_BASE_URL: &str = "https://api.siliconflow.com/v1";
pub const DEFAULT_ARCEE_MODEL: &str = "trinity-mini";
pub const ARCEE_TRINITY_LARGE_PREVIEW_MODEL: &str = "trinity-large-preview";
pub const DEFAULT_ARCEE_BASE_URL: &str = "https://api.arcee.ai/api/v1";
pub const DEFAULT_MOONSHOT_MODEL: &str = "kimi-k2.6";
pub const DEFAULT_MOONSHOT_BASE_URL: &str = "https://api.moonshot.ai/v1";
pub const DEFAULT_KIMI_CODE_MODEL: &str = "kimi-for-coding";
@@ -135,6 +138,7 @@ pub enum ApiProvider {
Novita,
Fireworks,
Siliconflow,
Arcee,
Moonshot,
Sglang,
Vllm,
@@ -163,6 +167,7 @@ impl ApiProvider {
"novita" => Some(Self::Novita),
"fireworks" | "fireworks-ai" => Some(Self::Fireworks),
"siliconflow" | "silicon-flow" | "silicon_flow" => Some(Self::Siliconflow),
"arcee" | "arcee-ai" | "arcee_ai" => Some(Self::Arcee),
"moonshot" | "moonshot-ai" | "kimi" | "kimi-k2" => Some(Self::Moonshot),
"sglang" | "sg-lang" => Some(Self::Sglang),
"vllm" | "v-llm" => Some(Self::Vllm),
@@ -186,6 +191,7 @@ impl ApiProvider {
Self::Novita => "novita",
Self::Fireworks => "fireworks",
Self::Siliconflow => "siliconflow",
Self::Arcee => "arcee",
Self::Moonshot => "moonshot",
Self::Sglang => "sglang",
Self::Vllm => "vllm",
@@ -209,6 +215,7 @@ impl ApiProvider {
Self::Novita => "Novita AI",
Self::Fireworks => "Fireworks AI",
Self::Siliconflow => "SiliconFlow",
Self::Arcee => "Arcee AI",
Self::Moonshot => "Moonshot/Kimi",
Self::Sglang => "SGLang",
Self::Vllm => "vLLM",
@@ -231,6 +238,7 @@ impl ApiProvider {
Self::Novita,
Self::Fireworks,
Self::Siliconflow,
Self::Arcee,
Self::Moonshot,
Self::Sglang,
Self::Vllm,
@@ -342,6 +350,20 @@ pub fn provider_capability(provider: ApiProvider, resolved_model: &str) -> Provi
};
}
if matches!(provider, ApiProvider::Arcee) {
return ProviderCapability {
provider,
resolved_model: resolved_model.to_string(),
context_window: crate::models::context_window_for_model(resolved_model)
.unwrap_or(crate::models::LEGACY_DEEPSEEK_CONTEXT_WINDOW_TOKENS),
max_output: crate::models::max_output_tokens_for_model(resolved_model).unwrap_or(4096),
thinking_supported: crate::models::model_supports_reasoning(resolved_model),
cache_telemetry_supported: false,
request_payload_mode: RequestPayloadMode::ChatCompletions,
alias_deprecation: None,
};
}
let model_lower = resolved_model.to_ascii_lowercase();
let alias_deprecation = if matches!(provider, ApiProvider::Deepseek | ApiProvider::DeepseekCN) {
deepseek_alias_deprecation(&model_lower)
@@ -574,6 +596,20 @@ fn canonical_xiaomi_mimo_model_id(model: &str) -> Option<&'static str> {
}
}
fn canonical_arcee_model_id(model: &str) -> Option<&'static str> {
let normalized = model.trim().to_ascii_lowercase();
let normalized = normalized.replace(['_', ' '], "-");
match normalized.as_str() {
"trinity" | "arcee-trinity" | "arcee-trinity-mini" | DEFAULT_ARCEE_MODEL => {
Some(DEFAULT_ARCEE_MODEL)
}
"arcee-trinity-large-preview" | ARCEE_TRINITY_LARGE_PREVIEW_MODEL => {
Some(ARCEE_TRINITY_LARGE_PREVIEW_MODEL)
}
_ => None,
}
}
/// Normalize a model selected through the TUI for the active provider.
///
/// Official DeepSeek endpoints require bare model IDs. Provider-prefixed
@@ -598,6 +634,12 @@ pub fn normalize_model_name_for_provider(provider: ApiProvider, model: &str) ->
return Some(canonical.to_string());
}
if matches!(provider, ApiProvider::Arcee) {
return canonical_arcee_model_id(model)
.map(ToString::to_string)
.or_else(|| normalize_custom_model_id(model));
}
let normalized = normalize_model_name(model)?;
if matches!(provider, ApiProvider::Deepseek | ApiProvider::DeepseekCN)
&& let Some(canonical) = canonical_official_deepseek_model_id(&normalized)
@@ -663,6 +705,7 @@ pub fn model_completion_names_for_provider(provider: ApiProvider) -> Vec<&'stati
ApiProvider::Siliconflow => {
vec![DEFAULT_SILICONFLOW_MODEL, DEFAULT_SILICONFLOW_FLASH_MODEL]
}
ApiProvider::Arcee => vec![DEFAULT_ARCEE_MODEL, ARCEE_TRINITY_LARGE_PREVIEW_MODEL],
ApiProvider::Moonshot => vec![DEFAULT_MOONSHOT_MODEL],
ApiProvider::WanjieArk => vec![DEFAULT_WANJIE_ARK_MODEL],
ApiProvider::Sglang => vec![DEFAULT_SGLANG_MODEL, DEFAULT_SGLANG_FLASH_MODEL],
@@ -1764,6 +1807,8 @@ pub struct ProvidersConfig {
#[serde(default)]
pub siliconflow: ProviderConfig,
#[serde(default)]
pub arcee: ProviderConfig,
#[serde(default)]
pub moonshot: ProviderConfig,
#[serde(default)]
pub sglang: ProviderConfig,
@@ -1923,6 +1968,7 @@ impl Config {
ApiProvider::Novita => "providers.novita",
ApiProvider::Fireworks => "providers.fireworks",
ApiProvider::Siliconflow => "providers.siliconflow",
ApiProvider::Arcee => "providers.arcee",
ApiProvider::Moonshot => "providers.moonshot",
ApiProvider::Sglang => "providers.sglang",
ApiProvider::Vllm => "providers.vllm",
@@ -1944,7 +1990,7 @@ impl Config {
&& ApiProvider::parse(provider).is_none()
{
anyhow::bail!(
"Invalid provider '{provider}': expected deepseek, deepseek-cn, nvidia-nim, openai, atlascloud, wanjie-ark, openrouter, xiaomi-mimo, novita, fireworks, siliconflow, moonshot, sglang, vllm, or ollama."
"Invalid provider '{provider}': expected deepseek, deepseek-cn, nvidia-nim, openai, atlascloud, wanjie-ark, volcengine, openrouter, xiaomi-mimo, novita, fireworks, siliconflow, arcee, moonshot, sglang, vllm, or ollama."
);
}
if let Some(ref key) = self.api_key
@@ -2068,6 +2114,7 @@ impl Config {
ApiProvider::Novita => &providers.novita,
ApiProvider::Fireworks => &providers.fireworks,
ApiProvider::Siliconflow => &providers.siliconflow,
ApiProvider::Arcee => &providers.arcee,
ApiProvider::Moonshot => &providers.moonshot,
ApiProvider::Sglang => &providers.sglang,
ApiProvider::Vllm => &providers.vllm,
@@ -2161,6 +2208,7 @@ impl Config {
ApiProvider::Novita => DEFAULT_NOVITA_MODEL,
ApiProvider::Fireworks => DEFAULT_FIREWORKS_MODEL,
ApiProvider::Siliconflow => DEFAULT_SILICONFLOW_MODEL,
ApiProvider::Arcee => DEFAULT_ARCEE_MODEL,
ApiProvider::Moonshot => DEFAULT_MOONSHOT_MODEL,
ApiProvider::Sglang => DEFAULT_SGLANG_MODEL,
ApiProvider::Vllm => DEFAULT_VLLM_MODEL,
@@ -2196,6 +2244,7 @@ impl Config {
| ApiProvider::Novita
| ApiProvider::Fireworks
| ApiProvider::Siliconflow
| ApiProvider::Arcee
| ApiProvider::Moonshot
| ApiProvider::Sglang
| ApiProvider::Vllm
@@ -2215,6 +2264,7 @@ impl Config {
ApiProvider::Novita => DEFAULT_NOVITA_BASE_URL,
ApiProvider::Fireworks => DEFAULT_FIREWORKS_BASE_URL,
ApiProvider::Siliconflow => DEFAULT_SILICONFLOW_BASE_URL,
ApiProvider::Arcee => DEFAULT_ARCEE_BASE_URL,
ApiProvider::Moonshot => {
if self
.provider_config()
@@ -2267,6 +2317,7 @@ impl Config {
ApiProvider::Novita => "novita",
ApiProvider::Fireworks => "fireworks",
ApiProvider::Siliconflow => "siliconflow",
ApiProvider::Arcee => "arcee",
ApiProvider::Moonshot => "moonshot",
ApiProvider::Sglang => "sglang",
ApiProvider::Vllm => "vllm",
@@ -2367,6 +2418,10 @@ impl Config {
"SiliconFlow API key not found. Run 'codewhale auth set --provider siliconflow', \
set SILICONFLOW_API_KEY, or add [providers.siliconflow] api_key in ~/.codewhale/config.toml."
),
ApiProvider::Arcee => anyhow::bail!(
"Arcee AI API key not found. Run 'codewhale auth set --provider arcee', \
set ARCEE_API_KEY, or add [providers.arcee] api_key in ~/.codewhale/config.toml."
),
ApiProvider::Moonshot => anyhow::bail!(
"Moonshot/Kimi API key not found. Run 'codewhale auth set --provider moonshot', \
set MOONSHOT_API_KEY/KIMI_API_KEY, or add [providers.moonshot] api_key. \
@@ -3038,6 +3093,13 @@ fn apply_env_overrides(config: &mut Config) {
.siliconflow
.base_url = Some(value);
}
ApiProvider::Arcee => {
config
.providers
.get_or_insert_with(ProvidersConfig::default)
.arcee
.base_url = Some(value);
}
ApiProvider::Moonshot => {
config
.providers
@@ -3179,6 +3241,16 @@ fn apply_env_overrides(config: &mut Config) {
.siliconflow
.base_url = Some(value);
}
if matches!(config.api_provider(), ApiProvider::Arcee)
&& let Ok(value) = std::env::var("ARCEE_BASE_URL")
&& !value.trim().is_empty()
{
config
.providers
.get_or_insert_with(ProvidersConfig::default)
.arcee
.base_url = Some(value);
}
if matches!(config.api_provider(), ApiProvider::Moonshot)
&& let Ok(value) =
std::env::var("MOONSHOT_BASE_URL").or_else(|_| std::env::var("KIMI_BASE_URL"))
@@ -3234,6 +3306,7 @@ fn apply_env_overrides(config: &mut Config) {
ApiProvider::Novita => &mut providers.novita,
ApiProvider::Fireworks => &mut providers.fireworks,
ApiProvider::Siliconflow => &mut providers.siliconflow,
ApiProvider::Arcee => &mut providers.arcee,
ApiProvider::Moonshot => &mut providers.moonshot,
ApiProvider::Sglang => &mut providers.sglang,
ApiProvider::Vllm => &mut providers.vllm,
@@ -3325,6 +3398,16 @@ fn apply_env_overrides(config: &mut Config) {
.siliconflow
.model = Some(value);
}
if matches!(config.api_provider(), ApiProvider::Arcee)
&& let Ok(value) = std::env::var("ARCEE_MODEL")
&& !value.trim().is_empty()
{
config
.providers
.get_or_insert_with(ProvidersConfig::default)
.arcee
.model = Some(value);
}
if let Some(value) = codewhale_env_var("CODEWHALE_MODEL", "DEEPSEEK_MODEL")
.ok()
.or_else(|| {
@@ -3361,6 +3444,7 @@ fn apply_env_overrides(config: &mut Config) {
ApiProvider::Novita => &mut providers.novita,
ApiProvider::Fireworks => &mut providers.fireworks,
ApiProvider::Siliconflow => &mut providers.siliconflow,
ApiProvider::Arcee => &mut providers.arcee,
ApiProvider::Moonshot => &mut providers.moonshot,
ApiProvider::Sglang => &mut providers.sglang,
ApiProvider::Vllm => &mut providers.vllm,
@@ -3673,6 +3757,7 @@ fn default_base_url_for_provider(provider: ApiProvider) -> &'static str {
ApiProvider::Novita => DEFAULT_NOVITA_BASE_URL,
ApiProvider::Fireworks => DEFAULT_FIREWORKS_BASE_URL,
ApiProvider::Siliconflow => DEFAULT_SILICONFLOW_BASE_URL,
ApiProvider::Arcee => DEFAULT_ARCEE_BASE_URL,
ApiProvider::Moonshot => DEFAULT_MOONSHOT_BASE_URL,
ApiProvider::Sglang => DEFAULT_SGLANG_BASE_URL,
ApiProvider::Vllm => DEFAULT_VLLM_BASE_URL,
@@ -3951,6 +4036,7 @@ fn merge_providers(
novita: merge_provider_config(base.novita, override_cfg.novita),
fireworks: merge_provider_config(base.fireworks, override_cfg.fireworks),
siliconflow: merge_provider_config(base.siliconflow, override_cfg.siliconflow),
arcee: merge_provider_config(base.arcee, override_cfg.arcee),
moonshot: merge_provider_config(base.moonshot, override_cfg.moonshot),
sglang: merge_provider_config(base.sglang, override_cfg.sglang),
vllm: merge_provider_config(base.vllm, override_cfg.vllm),
@@ -4382,6 +4468,7 @@ pub fn active_provider_has_env_api_key(config: &Config) -> bool {
ApiProvider::Siliconflow => {
std::env::var("SILICONFLOW_API_KEY").is_ok_and(|k| !k.trim().is_empty())
}
ApiProvider::Arcee => std::env::var("ARCEE_API_KEY").is_ok_and(|k| !k.trim().is_empty()),
ApiProvider::Moonshot => {
std::env::var("MOONSHOT_API_KEY").is_ok_and(|k| !k.trim().is_empty())
|| std::env::var("KIMI_API_KEY").is_ok_and(|k| !k.trim().is_empty())
@@ -4418,6 +4505,7 @@ pub fn has_api_key_for(config: &Config, provider: ApiProvider) -> bool {
ApiProvider::Novita => "NOVITA_API_KEY",
ApiProvider::Fireworks => "FIREWORKS_API_KEY",
ApiProvider::Siliconflow => "SILICONFLOW_API_KEY",
ApiProvider::Arcee => "ARCEE_API_KEY",
ApiProvider::Moonshot => "MOONSHOT_API_KEY",
ApiProvider::Sglang => "SGLANG_API_KEY",
ApiProvider::Vllm => "VLLM_API_KEY",
@@ -4521,6 +4609,7 @@ pub fn save_api_key_for(provider: ApiProvider, api_key: &str) -> Result<PathBuf>
ApiProvider::Novita => "providers.novita",
ApiProvider::Fireworks => "providers.fireworks",
ApiProvider::Siliconflow => "providers.siliconflow",
ApiProvider::Arcee => "providers.arcee",
ApiProvider::Moonshot => "providers.moonshot",
ApiProvider::Sglang => "providers.sglang",
ApiProvider::Vllm => "providers.vllm",
@@ -4561,6 +4650,7 @@ pub fn save_api_key_for(provider: ApiProvider, api_key: &str) -> Result<PathBuf>
ApiProvider::Novita => "novita",
ApiProvider::Fireworks => "fireworks",
ApiProvider::Siliconflow => "siliconflow",
ApiProvider::Arcee => "arcee",
ApiProvider::Moonshot => "moonshot",
ApiProvider::Sglang => "sglang",
ApiProvider::Vllm => "vllm",
@@ -4654,6 +4744,7 @@ fn provider_config_key(provider: ApiProvider) -> Result<&'static str> {
ApiProvider::Novita => Ok("novita"),
ApiProvider::Fireworks => Ok("fireworks"),
ApiProvider::Siliconflow => Ok("siliconflow"),
ApiProvider::Arcee => Ok("arcee"),
ApiProvider::Moonshot => Ok("moonshot"),
ApiProvider::Sglang => Ok("sglang"),
ApiProvider::Vllm => Ok("vllm"),
@@ -5180,6 +5271,9 @@ mod tests {
siliconflow_api_key: Option<OsString>,
siliconflow_base_url: Option<OsString>,
siliconflow_model: Option<OsString>,
arcee_api_key: Option<OsString>,
arcee_base_url: Option<OsString>,
arcee_model: Option<OsString>,
moonshot_api_key: Option<OsString>,
moonshot_base_url: Option<OsString>,
moonshot_model: Option<OsString>,
@@ -5259,6 +5353,9 @@ mod tests {
let siliconflow_api_key_prev = env::var_os("SILICONFLOW_API_KEY");
let siliconflow_base_url_prev = env::var_os("SILICONFLOW_BASE_URL");
let siliconflow_model_prev = env::var_os("SILICONFLOW_MODEL");
let arcee_api_key_prev = env::var_os("ARCEE_API_KEY");
let arcee_base_url_prev = env::var_os("ARCEE_BASE_URL");
let arcee_model_prev = env::var_os("ARCEE_MODEL");
let moonshot_api_key_prev = env::var_os("MOONSHOT_API_KEY");
let moonshot_base_url_prev = env::var_os("MOONSHOT_BASE_URL");
let moonshot_model_prev = env::var_os("MOONSHOT_MODEL");
@@ -5333,6 +5430,9 @@ mod tests {
env::remove_var("SILICONFLOW_API_KEY");
env::remove_var("SILICONFLOW_BASE_URL");
env::remove_var("SILICONFLOW_MODEL");
env::remove_var("ARCEE_API_KEY");
env::remove_var("ARCEE_BASE_URL");
env::remove_var("ARCEE_MODEL");
env::remove_var("MOONSHOT_API_KEY");
env::remove_var("MOONSHOT_BASE_URL");
env::remove_var("MOONSHOT_MODEL");
@@ -5407,6 +5507,9 @@ mod tests {
siliconflow_api_key: siliconflow_api_key_prev,
siliconflow_base_url: siliconflow_base_url_prev,
siliconflow_model: siliconflow_model_prev,
arcee_api_key: arcee_api_key_prev,
arcee_base_url: arcee_base_url_prev,
arcee_model: arcee_model_prev,
moonshot_api_key: moonshot_api_key_prev,
moonshot_base_url: moonshot_base_url_prev,
moonshot_model: moonshot_model_prev,
@@ -5496,6 +5599,9 @@ mod tests {
Self::restore_var("SILICONFLOW_API_KEY", self.siliconflow_api_key.take());
Self::restore_var("SILICONFLOW_BASE_URL", self.siliconflow_base_url.take());
Self::restore_var("SILICONFLOW_MODEL", self.siliconflow_model.take());
Self::restore_var("ARCEE_API_KEY", self.arcee_api_key.take());
Self::restore_var("ARCEE_BASE_URL", self.arcee_base_url.take());
Self::restore_var("ARCEE_MODEL", self.arcee_model.take());
Self::restore_var("MOONSHOT_API_KEY", self.moonshot_api_key.take());
Self::restore_var("MOONSHOT_BASE_URL", self.moonshot_base_url.take());
Self::restore_var("MOONSHOT_MODEL", self.moonshot_model.take());
@@ -6605,6 +6711,25 @@ api_key = "old-openrouter-key"
}
}
#[test]
fn normalize_model_name_for_provider_maps_arcee_direct_aliases() {
for (alias, expected) in [
("trinity", DEFAULT_ARCEE_MODEL),
("arcee-trinity", DEFAULT_ARCEE_MODEL),
("arcee-trinity-mini", DEFAULT_ARCEE_MODEL),
(
"arcee-trinity-large-preview",
ARCEE_TRINITY_LARGE_PREVIEW_MODEL,
),
("TRINITY_LARGE_PREVIEW", ARCEE_TRINITY_LARGE_PREVIEW_MODEL),
] {
assert_eq!(
normalize_model_name_for_provider(ApiProvider::Arcee, alias).as_deref(),
Some(expected)
);
}
}
#[test]
fn normalize_xiaomi_mimo_tts_aliases_for_provider() {
assert_eq!(
@@ -7812,6 +7937,102 @@ model = "qwen2.5-coder:7b"
Ok(())
}
#[test]
fn arcee_provider_uses_direct_defaults() -> Result<()> {
let _lock = lock_test_env();
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let temp_root = env::temp_dir().join(format!(
"codewhale-tui-arcee-defaults-test-{}-{}",
std::process::id(),
nanos
));
fs::create_dir_all(&temp_root)?;
let _guard = EnvGuard::new(&temp_root);
unsafe {
env::set_var("CODEWHALE_PROVIDER", "arcee");
env::set_var("ARCEE_API_KEY", "arcee-env-key");
}
let config = Config::load(None, None)?;
assert_eq!(config.api_provider(), ApiProvider::Arcee);
assert_eq!(config.deepseek_api_key()?, "arcee-env-key");
assert_eq!(config.deepseek_base_url(), DEFAULT_ARCEE_BASE_URL);
assert_eq!(config.default_model(), DEFAULT_ARCEE_MODEL);
Ok(())
}
#[test]
fn arcee_env_overrides_key_base_url_and_model() -> Result<()> {
let _lock = lock_test_env();
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let temp_root = env::temp_dir().join(format!(
"codewhale-tui-arcee-env-test-{}-{}",
std::process::id(),
nanos
));
fs::create_dir_all(&temp_root)?;
let _guard = EnvGuard::new(&temp_root);
unsafe {
env::set_var("CODEWHALE_PROVIDER", "arcee");
env::set_var("ARCEE_API_KEY", "arcee-env-key");
env::set_var("ARCEE_BASE_URL", "https://arcee-mirror.example/api/v1");
env::set_var("ARCEE_MODEL", "arcee-trinity-large-preview");
}
let config = Config::load(None, None)?;
assert_eq!(config.api_provider(), ApiProvider::Arcee);
assert_eq!(config.deepseek_api_key()?, "arcee-env-key");
assert_eq!(
config.deepseek_base_url(),
"https://arcee-mirror.example/api/v1"
);
assert_eq!(config.default_model(), "arcee-trinity-large-preview");
Ok(())
}
#[test]
fn arcee_provider_table_configures_direct_route() -> Result<()> {
let _lock = lock_test_env();
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let temp_root = env::temp_dir().join(format!(
"codewhale-tui-arcee-table-test-{}-{}",
std::process::id(),
nanos
));
let config_dir = temp_root.join(".deepseek");
fs::create_dir_all(&config_dir)?;
let _guard = EnvGuard::new(&temp_root);
fs::write(
config_dir.join("config.toml"),
r#"
provider = "arcee"
[providers.arcee]
api_key = "arcee-file-key"
base_url = "https://api.arcee.ai/api/v1"
model = "arcee-trinity-large-preview"
"#,
)?;
let config = Config::load(None, None)?;
assert_eq!(config.api_provider(), ApiProvider::Arcee);
assert_eq!(config.deepseek_api_key()?, "arcee-file-key");
assert_eq!(config.deepseek_base_url(), DEFAULT_ARCEE_BASE_URL);
assert_eq!(config.default_model(), ARCEE_TRINITY_LARGE_PREVIEW_MODEL);
Ok(())
}
#[test]
fn siliconflow_cn_base_url_env_normalizes_model_aliases() -> Result<()> {
let _lock = lock_test_env();
@@ -8759,6 +8980,22 @@ model = "deepseek-ai/deepseek-v4-pro"
}
}
#[test]
fn provider_capability_arcee_direct_models_use_api_docs_shape() {
for model in [DEFAULT_ARCEE_MODEL, ARCEE_TRINITY_LARGE_PREVIEW_MODEL] {
let cap = provider_capability(ApiProvider::Arcee, model);
assert_eq!(cap.context_window, 128_000);
assert_eq!(cap.max_output, 4096);
assert!(!cap.thinking_supported);
assert!(!cap.cache_telemetry_supported);
assert_eq!(
cap.request_payload_mode,
RequestPayloadMode::ChatCompletions
);
}
}
#[test]
fn provider_capability_xiaomi_mimo_has_thinking_no_cache() {
let cap = provider_capability(ApiProvider::XiaomiMimo, DEFAULT_XIAOMI_MIMO_MODEL);
+1
View File
@@ -418,6 +418,7 @@ impl Engine {
ApiProvider::Novita => "NOVITA_API_KEY",
ApiProvider::Fireworks => "FIREWORKS_API_KEY",
ApiProvider::Siliconflow => "SILICONFLOW_API_KEY",
ApiProvider::Arcee => "ARCEE_API_KEY",
ApiProvider::Moonshot => "MOONSHOT_API_KEY/KIMI_API_KEY",
ApiProvider::Sglang => "SGLANG_API_KEY",
ApiProvider::Vllm => "VLLM_API_KEY",
+5
View File
@@ -2005,6 +2005,10 @@ fn run_setup_status(config: &Config, workspace: &Path) -> Result<()> {
"SILICONFLOW_API_KEY",
"codewhale auth set --provider siliconflow --api-key \"...\"",
),
crate::config::ApiProvider::Arcee => (
"ARCEE_API_KEY",
"codewhale auth set --provider arcee --api-key \"...\"",
),
crate::config::ApiProvider::Moonshot => (
"MOONSHOT_API_KEY/KIMI_API_KEY",
"codewhale auth set --provider moonshot --api-key \"...\"",
@@ -2042,6 +2046,7 @@ fn run_setup_status(config: &Config, workspace: &Path) -> Result<()> {
crate::config::ApiProvider::Novita => "novita",
crate::config::ApiProvider::Fireworks => "fireworks",
crate::config::ApiProvider::Siliconflow => "siliconflow",
crate::config::ApiProvider::Arcee => "arcee",
crate::config::ApiProvider::Moonshot => "moonshot",
crate::config::ApiProvider::Sglang => "sglang",
crate::config::ApiProvider::Vllm => "vllm",
+19 -2
View File
@@ -240,7 +240,8 @@ pub fn context_window_for_model(model: &str) -> Option<u32> {
fn known_context_window_for_model(model_lower: &str) -> Option<u32> {
match model_lower {
"arcee-ai/trinity-large-thinking" => Some(262_144),
"trinity-mini" | "trinity-large-preview" => Some(128_000),
"arcee-ai/trinity-large-thinking" | "trinity-large-thinking" => Some(262_144),
"google/gemma-4-31b-it"
| "google/gemma-4-31b-it:free"
| "google/gemma-4-26b-a4b-it"
@@ -269,7 +270,9 @@ pub fn max_output_tokens_for_model(model: &str) -> Option<u32> {
return Some(384_000);
}
match lower.as_str() {
"arcee-ai/trinity-large-thinking" | "moonshotai/kimi-k2.6" => Some(262_144),
"arcee-ai/trinity-large-thinking" | "trinity-large-thinking" | "moonshotai/kimi-k2.6" => {
Some(262_144)
}
"minimax/minimax-m3" => Some(524_288),
"qwen/qwen3.6-35b-a3b" | "qwen/qwen3.6-27b" => Some(262_140),
"xiaomi/mimo-v2.5-pro" | "xiaomi/mimo-v2.5" | "mimo-v2.5-pro" | "mimo-v2.5" => {
@@ -291,6 +294,7 @@ pub fn model_supports_reasoning(model: &str) -> bool {
matches!(
lower.as_str(),
"arcee-ai/trinity-large-thinking"
| "trinity-large-thinking"
| "google/gemma-4-31b-it"
| "google/gemma-4-31b-it:free"
| "google/gemma-4-26b-a4b-it"
@@ -498,6 +502,7 @@ mod tests {
fn recent_openrouter_large_models_have_static_windows() {
for (model, expected_window) in [
("arcee-ai/trinity-large-thinking", 262_144),
("trinity-large-thinking", 262_144),
(concat!("qwen/", "qwen3.6-35b-a3b"), 262_144),
(concat!("xiaomi/", "mimo-v2.5-pro"), 1_000_000),
("mimo-v2.5-pro", 1_000_000),
@@ -511,12 +516,24 @@ mod tests {
}
}
#[test]
fn arcee_direct_models_have_static_windows_without_reasoning_flag() {
for model in ["trinity-mini", "trinity-large-preview"] {
assert_eq!(context_window_for_model(model), Some(128_000));
assert!(!model_supports_reasoning(model));
}
}
#[test]
fn recent_openrouter_large_models_have_known_output_caps() {
assert_eq!(
max_output_tokens_for_model("arcee-ai/trinity-large-thinking"),
Some(262_144)
);
assert_eq!(
max_output_tokens_for_model("trinity-large-thinking"),
Some(262_144)
);
assert_eq!(
max_output_tokens_for_model(concat!("xiaomi/", "mimo-v2.5-pro")),
Some(131_072)
+1
View File
@@ -107,6 +107,7 @@ impl ProviderPickerView {
ApiProvider::Novita => "NOVITA_API_KEY",
ApiProvider::Fireworks => "FIREWORKS_API_KEY",
ApiProvider::Siliconflow => "SILICONFLOW_API_KEY",
ApiProvider::Arcee => "ARCEE_API_KEY",
ApiProvider::Moonshot => "MOONSHOT_API_KEY / KIMI_API_KEY",
ApiProvider::Sglang => "SGLANG_API_KEY",
ApiProvider::Vllm => "VLLM_API_KEY",
+3
View File
@@ -6309,6 +6309,7 @@ fn render(f: &mut Frame, app: &mut App) {
crate::config::ApiProvider::Novita => Some("Novita"),
crate::config::ApiProvider::Fireworks => Some("Fireworks"),
crate::config::ApiProvider::Siliconflow => Some("SiliconFlow"),
crate::config::ApiProvider::Arcee => Some("Arcee"),
crate::config::ApiProvider::Moonshot => Some("Kimi"),
crate::config::ApiProvider::Sglang => Some("SGLang"),
crate::config::ApiProvider::Vllm => Some("vLLM"),
@@ -7262,6 +7263,7 @@ async fn apply_provider_picker_api_key(
ApiProvider::Novita => &mut providers.novita,
ApiProvider::Fireworks => &mut providers.fireworks,
ApiProvider::Siliconflow => &mut providers.siliconflow,
ApiProvider::Arcee => &mut providers.arcee,
ApiProvider::Moonshot => &mut providers.moonshot,
ApiProvider::Sglang => &mut providers.sglang,
ApiProvider::Vllm => &mut providers.vllm,
@@ -7317,6 +7319,7 @@ fn set_provider_auth_mode_in_memory(config: &mut Config, provider: ApiProvider,
ApiProvider::Novita => &mut providers.novita,
ApiProvider::Fireworks => &mut providers.fireworks,
ApiProvider::Siliconflow => &mut providers.siliconflow,
ApiProvider::Arcee => &mut providers.arcee,
ApiProvider::Moonshot => &mut providers.moonshot,
ApiProvider::Sglang => &mut providers.sglang,
ApiProvider::Vllm => &mut providers.vllm,
+14 -6
View File
@@ -87,7 +87,7 @@ provider's keyring entry.
For hosted, generic OpenAI-compatible, or self-hosted providers, set
`provider = "nvidia-nim"`, `"openai"`, `"atlascloud"`, `"wanjie-ark"`,
`"volcengine"`, `"openrouter"`, `"xiaomi-mimo"`, `"novita"`, `"fireworks"`,
`"siliconflow"`, `"moonshot"`, `"sglang"`, `"vllm"`, or `"ollama"` or pass
`"siliconflow"`, `"arcee"`, `"moonshot"`, `"sglang"`, `"vllm"`, or `"ollama"` or pass
`codewhale --provider <name>`.
For the provider-by-provider registry, including auth variables, default base
URLs, model IDs, and capability metadata, see [PROVIDERS.md](PROVIDERS.md).
@@ -99,7 +99,8 @@ the resolved key, base URL, provider, and model to the TUI process. Use
`codewhale auth set --provider wanjie-ark --api-key "YOUR_WANJIE_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"`
`codewhale auth set --provider siliconflow --api-key "YOUR_SILICONFLOW_API_KEY"` or
`codewhale auth set --provider arcee --api-key "YOUR_ARCEE_API_KEY"`
to save provider keys through the facade. The generic `openai` provider defaults
to `https://api.openai.com/v1`, accepts `OPENAI_BASE_URL`, and defaults to
`deepseek-v4-pro` for OpenAI-compatible gateways. `atlascloud` defaults to
@@ -119,6 +120,10 @@ SiliconFlow defaults to `https://api.siliconflow.com/v1`, accepts
`SILICONFLOW_BASE_URL`, and uses `deepseek-ai/DeepSeek-V4-Pro` by default.
`https://api.siliconflow.cn/v1` can still be configured explicitly when a user
needs the regional endpoint.
Arcee AI defaults to `https://api.arcee.ai/api/v1`, accepts `ARCEE_BASE_URL`,
and uses `trinity-mini` by default. `trinity-large-preview` is also listed as a
direct Arcee API model; OpenRouter's `arcee-ai/trinity-large-thinking` remains
an OpenRouter model ID, not the direct Arcee default.
### Custom OpenAI-Compatible Gateways
@@ -294,7 +299,7 @@ aliases. When both forms are set the `CODEWHALE_*` value wins; the
`DEEPSEEK_*` form is kept for older shells:
- `CODEWHALE_PROVIDER` (preferred) / `DEEPSEEK_PROVIDER` (legacy alias) —
`deepseek|nvidia-nim|openai|atlascloud|wanjie-ark|openrouter|xiaomi-mimo|novita|fireworks|siliconflow|moonshot|sglang|vllm|ollama`
`deepseek|nvidia-nim|openai|atlascloud|wanjie-ark|volcengine|openrouter|xiaomi-mimo|novita|fireworks|siliconflow|arcee|moonshot|sglang|vllm|ollama`
- `CODEWHALE_MODEL` (preferred) / `DEEPSEEK_MODEL` (legacy alias) — default model for the active provider
- `CODEWHALE_BASE_URL` (preferred) / `DEEPSEEK_BASE_URL` (legacy alias) — base URL for the active provider
@@ -332,6 +337,9 @@ Remaining variables:
- `SILICONFLOW_API_KEY`
- `SILICONFLOW_BASE_URL`
- `SILICONFLOW_MODEL`
- `ARCEE_API_KEY`
- `ARCEE_BASE_URL`
- `ARCEE_MODEL`
- `MOONSHOT_API_KEY` or `KIMI_API_KEY`
- `MOONSHOT_BASE_URL` or `KIMI_BASE_URL`
- `MOONSHOT_MODEL`, `KIMI_MODEL_NAME`, or `KIMI_MODEL`
@@ -615,10 +623,10 @@ If you are upgrading from older releases:
### Core keys (used by the TUI/engine)
- `provider` (string, optional): `deepseek` (default), `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `moonshot`, `sglang`, `vllm`, or `ollama`. Legacy `deepseek-cn` configs are still accepted as an alias for `deepseek`; DeepSeek uses the same official host [`https://api.deepseek.com`](https://api-docs.deepseek.com/) worldwide. `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `atlascloud` targets AtlasCloud's OpenAI-compatible endpoint at `https://api.atlascloud.ai/v1`; `wanjie-ark` targets Wanjie Ark's OpenAI-compatible endpoint at `https://maas-openapi.wanjiedata.com/api/v1`; `openrouter` targets `https://openrouter.ai/api/v1`; `xiaomi-mimo` targets Xiaomi MiMo's OpenAI-compatible endpoint at `https://api.xiaomimimo.com/v1`; `novita` targets `https://api.novita.ai/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `siliconflow` targets SiliconFlow, defaulting to `https://api.siliconflow.com/v1`; `moonshot` targets Moonshot/Kimi, defaulting to `https://api.moonshot.ai/v1`; `sglang` targets a self-hosted OpenAI-compatible endpoint, defaulting to `http://localhost:30000/v1`; `vllm` targets a self-hosted vLLM OpenAI-compatible endpoint, defaulting to `http://localhost:8000/v1`; `ollama` targets Ollama's OpenAI-compatible endpoint, defaulting to `http://localhost:11434/v1`.
- `provider` (string, optional): `deepseek` (default), `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `arcee`, `moonshot`, `sglang`, `vllm`, or `ollama`. Legacy `deepseek-cn` configs are still accepted as an alias for `deepseek`; DeepSeek uses the same official host [`https://api.deepseek.com`](https://api-docs.deepseek.com/) worldwide. `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `atlascloud` targets AtlasCloud's OpenAI-compatible endpoint at `https://api.atlascloud.ai/v1`; `wanjie-ark` targets Wanjie Ark's OpenAI-compatible endpoint at `https://maas-openapi.wanjiedata.com/api/v1`; `volcengine` targets Volcengine Ark's OpenAI-compatible coding endpoint at `https://ark.cn-beijing.volces.com/api/coding/v3`; `openrouter` targets `https://openrouter.ai/api/v1`; `xiaomi-mimo` targets Xiaomi MiMo's OpenAI-compatible endpoint at `https://api.xiaomimimo.com/v1`; `novita` targets `https://api.novita.ai/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `siliconflow` targets SiliconFlow, defaulting to `https://api.siliconflow.com/v1`; `arcee` targets Arcee AI's OpenAI-compatible endpoint at `https://api.arcee.ai/api/v1`; `moonshot` targets Moonshot/Kimi, defaulting to `https://api.moonshot.ai/v1`; `sglang` targets a self-hosted OpenAI-compatible endpoint, defaulting to `http://localhost:30000/v1`; `vllm` targets a self-hosted vLLM OpenAI-compatible endpoint, defaulting to `http://localhost:8000/v1`; `ollama` targets Ollama's OpenAI-compatible endpoint, defaulting to `http://localhost:11434/v1`.
- `api_key` (string, required for hosted providers): must be non-empty for DeepSeek/hosted providers (or set the provider API key env var). Self-hosted SGLang, vLLM, and Ollama can omit it.
- `base_url` (string, optional): defaults to `https://api.deepseek.com/beta` for DeepSeek's OpenAI-compatible Chat Completions API, including legacy `provider = "deepseek-cn"` configs. Other defaults are `https://integrate.api.nvidia.com/v1` for `nvidia-nim`, `https://api.openai.com/v1` for `openai`, `https://api.atlascloud.ai/v1` for `atlascloud`, `https://maas-openapi.wanjiedata.com/api/v1` for `wanjie-ark`, `https://openrouter.ai/api/v1` for `openrouter`, `https://api.xiaomimimo.com/v1` for `xiaomi-mimo`, `https://api.novita.ai/v1` for `novita`, `https://api.fireworks.ai/inference/v1` for `fireworks`, `https://api.siliconflow.com/v1` for `siliconflow`, `https://api.moonshot.ai/v1` for `moonshot`, `http://localhost:30000/v1` for `sglang`, `http://localhost:8000/v1` for `vllm`, and `http://localhost:11434/v1` for `ollama`. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features.
- `default_text_model` (string, optional): defaults to `deepseek-v4-pro` for DeepSeek and generic OpenAI-compatible endpoints, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `deepseek-reasoner` for Wanjie Ark, `deepseek/deepseek-v4-pro` for OpenRouter and Novita, `mimo-v2.5-pro` for Xiaomi MiMo, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `deepseek-ai/DeepSeek-V4-Pro` for SiliconFlow, `kimi-k2.6` for Moonshot, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, and `deepseek-coder:1.3b` for Ollama. Current public DeepSeek IDs are `deepseek-v4-pro` and `deepseek-v4-flash`, both with 1M context windows, 384K max output, and thinking mode enabled by default. Legacy `deepseek-chat` and `deepseek-reasoner` remain compatibility aliases for `deepseek-v4-flash` until July 24, 2026, except SiliconFlow maps `deepseek-reasoner` and `deepseek-r1` to its Pro model while `deepseek-chat` and `deepseek-v3` map to Flash. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. OpenRouter also recognizes recent large IDs such as `arcee-ai/trinity-large-thinking`, `minimax/minimax-m3`, `xiaomi/mimo-v2.5-pro`, `qwen/qwen3.6-35b-a3b`, `google/gemma-4-31b-it`, and `moonshotai/kimi-k2.6`. Generic `openai`, `atlascloud`, `wanjie-ark`, `xiaomi-mimo`, and Ollama model IDs are passed through unchanged. OpenRouter and SiliconFlow provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `codewhale models` to discover live IDs from your configured endpoint. `CODEWHALE_MODEL` overrides this for a single process; `DEEPSEEK_MODEL` is the legacy alias.
- `base_url` (string, optional): defaults to `https://api.deepseek.com/beta` for DeepSeek's OpenAI-compatible Chat Completions API, including legacy `provider = "deepseek-cn"` configs. Other defaults are `https://integrate.api.nvidia.com/v1` for `nvidia-nim`, `https://api.openai.com/v1` for `openai`, `https://api.atlascloud.ai/v1` for `atlascloud`, `https://maas-openapi.wanjiedata.com/api/v1` for `wanjie-ark`, `https://ark.cn-beijing.volces.com/api/coding/v3` for `volcengine`, `https://openrouter.ai/api/v1` for `openrouter`, `https://api.xiaomimimo.com/v1` for `xiaomi-mimo`, `https://api.novita.ai/v1` for `novita`, `https://api.fireworks.ai/inference/v1` for `fireworks`, `https://api.siliconflow.com/v1` for `siliconflow`, `https://api.arcee.ai/api/v1` for `arcee`, `https://api.moonshot.ai/v1` for `moonshot`, `http://localhost:30000/v1` for `sglang`, `http://localhost:8000/v1` for `vllm`, and `http://localhost:11434/v1` for `ollama`. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features.
- `default_text_model` (string, optional): defaults to `deepseek-v4-pro` for DeepSeek and generic OpenAI-compatible endpoints, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `deepseek-reasoner` for Wanjie Ark, `DeepSeek-V4-Pro` for Volcengine Ark, `deepseek/deepseek-v4-pro` for OpenRouter and Novita, `mimo-v2.5-pro` for Xiaomi MiMo, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `deepseek-ai/DeepSeek-V4-Pro` for SiliconFlow, `trinity-mini` for Arcee AI, `kimi-k2.6` for Moonshot, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, and `deepseek-coder:1.3b` for Ollama. Current public DeepSeek IDs are `deepseek-v4-pro` and `deepseek-v4-flash`, both with 1M context windows, 384K max output, and thinking mode enabled by default. Legacy `deepseek-chat` and `deepseek-reasoner` remain compatibility aliases for `deepseek-v4-flash` until July 24, 2026, except SiliconFlow maps `deepseek-reasoner` and `deepseek-r1` to its Pro model while `deepseek-chat` and `deepseek-v3` map to Flash. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. OpenRouter also recognizes recent large IDs such as `arcee-ai/trinity-large-thinking`, `minimax/minimax-m3`, `xiaomi/mimo-v2.5-pro`, `qwen/qwen3.6-35b-a3b`, `google/gemma-4-31b-it`, and `moonshotai/kimi-k2.6`; direct Arcee uses bare IDs such as `trinity-mini` and `trinity-large-preview`. Generic `openai`, `atlascloud`, `wanjie-ark`, `xiaomi-mimo`, `arcee`, and Ollama model IDs are passed through unchanged after known aliases are normalized. OpenRouter and SiliconFlow provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `codewhale models` to discover live IDs from your configured endpoint. `CODEWHALE_MODEL` overrides this for a single process; `DEEPSEEK_MODEL` is the legacy alias.
- `reasoning_effort` (string, optional): `off`, `low`, `medium`, `high`, or `max`; defaults to the configured UI tier. DeepSeek Platform receives top-level `thinking` / `reasoning_effort` fields. NVIDIA NIM receives equivalent settings through `chat_template_kwargs`.
- `allow_shell` (bool, optional): defaults to `false`; shell tools must be explicitly enabled.
- `approval_policy` (string, optional): `on-request`, `untrusted`, or `never`. Runtime `approval_mode` editing in `/config` also accepts `on-request` and `untrusted` aliases.
+6 -3
View File
@@ -6,7 +6,7 @@ limited to provider IDs, config keys, auth paths, base URLs, model resolution,
and capability metadata that the code already knows about.
DeepSeek remains the first-class default provider. NVIDIA NIM, OpenRouter,
Volcengine Ark, Xiaomi MiMo, Novita, Fireworks, SiliconFlow, generic
Volcengine Ark, Xiaomi MiMo, Novita, Fireworks, SiliconFlow, Arcee AI, generic
OpenAI-compatible endpoints, self-hosted runtimes, and Moonshot/Kimi are
additive routes for running the same terminal harness against other hosted or
local model endpoints. Hugging Face Inference Providers are a planned additive
@@ -30,8 +30,8 @@ Sources to keep in sync:
The canonical provider IDs are:
`deepseek`, `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`,
`openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `moonshot`,
`sglang`, `vllm`, and `ollama`.
`openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `arcee`,
`moonshot`, `sglang`, `vllm`, and `ollama`.
Use any of these surfaces to select a provider:
@@ -122,6 +122,7 @@ endpoint.
| `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. |
| `arcee` | `[providers.arcee]` | `ARCEE_API_KEY` | `ARCEE_BASE_URL`; default `https://api.arcee.ai/api/v1` | `trinity-mini`, `trinity-large-preview` | Arcee AI direct OpenAI-compatible route. `ARCEE_MODEL` is accepted. OpenRouter's `arcee-ai/trinity-large-thinking` remains an OpenRouter model ID; direct Arcee uses bare model IDs such as `trinity-mini`. |
| `moonshot` | `[providers.moonshot]` | `MOONSHOT_API_KEY`, `KIMI_API_KEY` | `MOONSHOT_BASE_URL`, `KIMI_BASE_URL`; default `https://api.moonshot.ai/v1` | `kimi-k2.6`; Kimi Code path uses `kimi-for-coding` at `https://api.kimi.com/coding/v1` | Moonshot/Kimi route. `MOONSHOT_MODEL`, `KIMI_MODEL_NAME`, and `KIMI_MODEL` are accepted. `[providers.moonshot] auth_mode = "kimi_oauth"` reads Kimi CLI OAuth credentials when present. |
| `sglang` | `[providers.sglang]` | Optional `SGLANG_API_KEY` | `SGLANG_BASE_URL`; default `http://localhost:30000/v1` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | Self-hosted OpenAI-compatible route. Localhost deployments commonly omit auth. `SGLANG_MODEL` is accepted. |
| `vllm` | `[providers.vllm]` | Optional `VLLM_API_KEY` | `VLLM_BASE_URL`; default `http://localhost:8000/v1` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | Self-hosted vLLM OpenAI-compatible route. Localhost deployments commonly omit auth. `VLLM_MODEL` is accepted. |
@@ -172,6 +173,7 @@ endpoint when the endpoint supports model listing.
| `novita` | `deepseek/deepseek-v4-pro`, `deepseek/deepseek-v4-flash` | yes | yes |
| `fireworks` | `accounts/fireworks/models/deepseek-v4-pro` | yes | yes |
| `siliconflow` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | yes | yes |
| `arcee` | `trinity-mini`, `trinity-large-preview`; provider-hinted custom model IDs pass through | yes | no for documented direct API models |
| `moonshot` | `kimi-k2.6` | yes | yes |
| `sglang` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | yes | yes |
| `vllm` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | yes | yes |
@@ -201,6 +203,7 @@ All shipped providers use the Chat Completions request payload mode today.
| OpenRouter, Novita, Fireworks, SiliconFlow, SGLang, and vLLM V4 model IDs | 1,000,000 | 384,000 | yes | no | not documented in code |
| Xiaomi MiMo models | 1,000,000 | 128,000 | yes | no | not documented in code |
| Wanjie Ark `reasoner` / `r1` model IDs | 128,000 | 4,096 | yes | no | not documented in code |
| Direct Arcee API models (`trinity-mini`, `trinity-large-preview`) | 128,000 | 4,096 | no in doctor capability metadata | no | not documented in code |
| Generic `openai`, AtlasCloud, and Moonshot/Kimi | 128,000 | 4,096 | no in doctor capability metadata | no | not documented in code |
| Ollama | 8,192 | 4,096 | no | no | not documented in code |
| Other recognized DeepSeek model IDs | 128,000 unless the model name carries an explicit `Nk` hint | 4,096 | no unless V4/reasoner logic matches | DeepSeek/NIM only | DeepSeek beta only |