From e54a0a500b933d11d928abd880780a4cb32c1fb7 Mon Sep 17 00:00:00 2001 From: Hunter B Date: Tue, 2 Jun 2026 08:51:39 -0700 Subject: [PATCH] feat(provider): add direct arcee support --- CHANGELOG.md | 7 + README.md | 10 +- config.example.toml | 23 ++- crates/agent/src/lib.rs | 74 ++++++++ crates/config/src/lib.rs | 136 +++++++++++++++ crates/secrets/src/lib.rs | 17 ++ crates/tui/CHANGELOG.md | 7 + crates/tui/src/client.rs | 37 ++++ crates/tui/src/config.rs | 239 +++++++++++++++++++++++++- crates/tui/src/core/engine.rs | 1 + crates/tui/src/main.rs | 5 + crates/tui/src/models.rs | 21 ++- crates/tui/src/tui/provider_picker.rs | 1 + crates/tui/src/tui/ui.rs | 3 + docs/CONFIGURATION.md | 20 ++- docs/PROVIDERS.md | 9 +- 16 files changed, 589 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be0dba4f..6fdabac0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 6a75ffbe..6a071520 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/config.example.toml b/config.example.toml index 6f9eb289..13649c80 100644 --- a/config.example.toml +++ b/config.example.toml @@ -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 diff --git a/crates/agent/src/lib.rs b/crates/agent/src/lib.rs index ff750307..a96dc226 100644 --- a/crates/agent/src/lib.rs +++ b/crates/agent/src/lib.rs @@ -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 { }) } +fn arcee_passthrough_model(requested: &str) -> Option { + 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(); diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 05f4e504..5f3161b8 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -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, moonshot_model: Option, xiaomi_mimo_model: Option, + arcee_model: Option, output_mode: Option, auth_mode: Option, log_level: Option, @@ -2229,6 +2287,7 @@ struct EnvRuntimeOverrides { fireworks_base_url: Option, siliconflow_base_url: Option, siliconflow_model: Option, + arcee_base_url: Option, moonshot_base_url: Option, sglang_base_url: Option, vllm_base_url: Option, @@ -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, siliconflow_base_url: Option, siliconflow_model: Option, + arcee_api_key: Option, + arcee_base_url: Option, + arcee_model: Option, moonshot_api_key: Option, moonshot_base_url: Option, moonshot_model: Option, @@ -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(); diff --git a/crates/secrets/src/lib.rs b/crates/secrets/src/lib.rs index 6eeb0a0b..ca6314cf 100644 --- a/crates/secrets/src/lib.rs +++ b/crates/secrets/src/lib.rs @@ -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 { } "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(); diff --git a/crates/tui/CHANGELOG.md b/crates/tui/CHANGELOG.md index be0dba4f..6fdabac0 100644 --- a/crates/tui/CHANGELOG.md +++ b/crates/tui/CHANGELOG.md @@ -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 diff --git a/crates/tui/src/client.rs b/crates/tui/src/client.rs index 7215f6d8..90e61610 100644 --- a/crates/tui/src/client.rs +++ b/crates/tui/src/client.rs @@ -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 [ diff --git a/crates/tui/src/config.rs b/crates/tui/src/config.rs index b2ce12a7..42a247bb 100644 --- a/crates/tui/src/config.rs +++ b/crates/tui/src/config.rs @@ -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 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 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, siliconflow_base_url: Option, siliconflow_model: Option, + arcee_api_key: Option, + arcee_base_url: Option, + arcee_model: Option, moonshot_api_key: Option, moonshot_base_url: Option, moonshot_model: Option, @@ -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); diff --git a/crates/tui/src/core/engine.rs b/crates/tui/src/core/engine.rs index 54ba0c24..23b4194a 100644 --- a/crates/tui/src/core/engine.rs +++ b/crates/tui/src/core/engine.rs @@ -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", diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index 1459f3b7..72795d03 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -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", diff --git a/crates/tui/src/models.rs b/crates/tui/src/models.rs index 301e9c09..60ed6351 100644 --- a/crates/tui/src/models.rs +++ b/crates/tui/src/models.rs @@ -240,7 +240,8 @@ pub fn context_window_for_model(model: &str) -> Option { fn known_context_window_for_model(model_lower: &str) -> Option { 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 { 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) diff --git a/crates/tui/src/tui/provider_picker.rs b/crates/tui/src/tui/provider_picker.rs index bc7ab13a..9dee8226 100644 --- a/crates/tui/src/tui/provider_picker.rs +++ b/crates/tui/src/tui/provider_picker.rs @@ -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", diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 9aa56b05..d0082c31 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -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, diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 230d077b..c5de80eb 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -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 `. 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. diff --git a/docs/PROVIDERS.md b/docs/PROVIDERS.md index 63b81d6e..000fe9a9 100644 --- a/docs/PROVIDERS.md +++ b/docs/PROVIDERS.md @@ -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 |