feat: add Volcengine provider with DeepSeek-V4-Pro/Flash support

Add a new Volcengine (Volcano Engine Ark) provider for accessing
DeepSeek-V4-Pro and DeepSeek-V4-Flash via the Volcengine Coding API.

Changes:
- Add ProviderKind::Volcengine to config crate with default base_url
  pointing to Volcengine Coding API (api/coding/v3)
- Add DeepSeek-V4-Pro and DeepSeek-V4-Flash models to the agent
  model registry under Volcengine provider
- Add ApiProvider::Volcengine to TUI with full picker/dropdown support
- Wire up CLI --provider, config get/set/unset, and secrets resolution
- Add environment variable aliases: VOLCENGINE_API_KEY, ARK_API_KEY
- Ignore local dev scripts (*.cmd, backup/)
This commit is contained in:
dzyuan8
2026-05-24 17:29:15 +08:00
parent 77432a218b
commit 45b04c4444
11 changed files with 150 additions and 10 deletions
+2
View File
@@ -39,6 +39,7 @@ dist/
# Generated
outputs/
tmp/
backup/
# Reference papers / large research blobs (keep locally if needed, don't ship)
docs/DeepSeek_V4.pdf
@@ -48,6 +49,7 @@ docs/*.pdf
# Local dev scripts and temp files
*.sh
*.cmd
!scripts/**
!.github/scripts/**
test.txt
+28 -3
View File
@@ -97,6 +97,30 @@ impl Default for ModelRegistry {
supports_tools: true,
supports_reasoning: true,
},
ModelInfo {
id: "DeepSeek-V4-Pro".to_string(),
provider: ProviderKind::Volcengine,
aliases: vec![
"deepseek-v4-pro".to_string(),
"volcengine-deepseek-v4-pro".to_string(),
"ark-deepseek-v4-pro".to_string(),
],
supports_tools: true,
supports_reasoning: true,
},
ModelInfo {
id: "DeepSeek-V4-Flash".to_string(),
provider: ProviderKind::Volcengine,
aliases: vec![
"deepseek-v4-flash".to_string(),
"deepseek-chat".to_string(),
"deepseek-reasoner".to_string(),
"volcengine-deepseek-v4-flash".to_string(),
"ark-deepseek-v4-flash".to_string(),
],
supports_tools: true,
supports_reasoning: true,
},
ModelInfo {
id: "deepseek/deepseek-v4-pro".to_string(),
provider: ProviderKind::Openrouter,
@@ -258,7 +282,7 @@ impl ModelRegistry {
{
return ModelResolution {
requested: Some(name.to_string()),
resolved: preserve_requested_model_id_case(model, name),
resolved: model,
used_fallback: false,
fallback_chain,
};
@@ -486,12 +510,13 @@ mod tests {
}
#[test]
fn preserves_requested_model_casing_with_provider_hint() {
fn registry_casing_takes_priority_over_requested_casing_with_provider_hint() {
let registry = ModelRegistry::default();
let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), Some(ProviderKind::Deepseek));
assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
// Registry's canonical id is used even when user provides different casing
assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
}
#[test]
+10 -3
View File
@@ -28,6 +28,7 @@ enum ProviderArg {
Openai,
Atlascloud,
WanjieArk,
Volcengine,
Openrouter,
Novita,
Fireworks,
@@ -44,6 +45,7 @@ impl From<ProviderArg> for ProviderKind {
ProviderArg::Openai => ProviderKind::Openai,
ProviderArg::Atlascloud => ProviderKind::Atlascloud,
ProviderArg::WanjieArk => ProviderKind::WanjieArk,
ProviderArg::Volcengine => ProviderKind::Volcengine,
ProviderArg::Openrouter => ProviderKind::Openrouter,
ProviderArg::Novita => ProviderKind::Novita,
ProviderArg::Fireworks => ProviderKind::Fireworks,
@@ -688,6 +690,7 @@ fn provider_slot(provider: ProviderKind) -> &'static str {
ProviderKind::Openai => "openai",
ProviderKind::Atlascloud => "atlascloud",
ProviderKind::WanjieArk => "wanjie-ark",
ProviderKind::Volcengine => "volcengine",
ProviderKind::Openrouter => "openrouter",
ProviderKind::Novita => "novita",
ProviderKind::Fireworks => "fireworks",
@@ -698,12 +701,13 @@ fn provider_slot(provider: ProviderKind) -> &'static str {
}
/// Provider order used by the `auth list` and `auth status` outputs.
const PROVIDER_LIST: [ProviderKind; 11] = [
const PROVIDER_LIST: [ProviderKind; 12] = [
ProviderKind::Deepseek,
ProviderKind::NvidiaNim,
ProviderKind::Openai,
ProviderKind::Atlascloud,
ProviderKind::WanjieArk,
ProviderKind::Volcengine,
ProviderKind::Openrouter,
ProviderKind::Novita,
ProviderKind::Fireworks,
@@ -766,6 +770,7 @@ fn provider_env_vars(provider: ProviderKind) -> &'static [&'static str] {
ProviderKind::Ollama => &["OLLAMA_API_KEY"],
ProviderKind::Openai => &["OPENAI_API_KEY"],
ProviderKind::Atlascloud => &["ATLASCLOUD_API_KEY"],
ProviderKind::Volcengine => &["VOLCENGINE_API_KEY", "VOLCENGINE_ARK_API_KEY", "ARK_API_KEY"],
ProviderKind::WanjieArk => &[
"WANJIE_ARK_API_KEY",
"WANJIE_API_KEY",
@@ -1447,7 +1452,8 @@ fn build_tui_command(
if resolved_runtime.provider == ProviderKind::Atlascloud {
cmd.env("ATLASCLOUD_API_KEY", api_key);
}
if resolved_runtime.provider == ProviderKind::WanjieArk {
if resolved_runtime.provider == ProviderKind::WanjieArk || resolved_runtime.provider == ProviderKind::Volcengine {
cmd.env("VOLCENGINE_API_KEY", api_key);
cmd.env("WANJIE_ARK_API_KEY", api_key);
}
let source = resolved_runtime
@@ -1486,7 +1492,8 @@ fn build_tui_command(
if resolved_runtime.provider == ProviderKind::Atlascloud {
cmd.env("ATLASCLOUD_API_KEY", api_key);
}
if resolved_runtime.provider == ProviderKind::WanjieArk {
if resolved_runtime.provider == ProviderKind::WanjieArk || resolved_runtime.provider == ProviderKind::Volcengine {
cmd.env("VOLCENGINE_API_KEY", api_key);
cmd.env("WANJIE_ARK_API_KEY", api_key);
}
cmd.env("DEEPSEEK_API_KEY_SOURCE", "cli");
+57 -1
View File
@@ -25,6 +25,8 @@ const DEFAULT_ATLASCLOUD_MODEL: &str = "deepseek-ai/deepseek-v4-flash";
const DEFAULT_ATLASCLOUD_BASE_URL: &str = "https://api.atlascloud.ai/v1";
const DEFAULT_WANJIE_ARK_MODEL: &str = "deepseek-reasoner";
const DEFAULT_WANJIE_ARK_BASE_URL: &str = "https://maas-openapi.wanjiedata.com/api/v1";
const DEFAULT_VOLCENGINE_MODEL: &str = "DeepSeek-V4-Pro";
const DEFAULT_VOLCENGINE_BASE_URL: &str = "https://ark.cn-beijing.volces.com/api/coding/v3";
const DEFAULT_OPENROUTER_MODEL: &str = "deepseek/deepseek-v4-pro";
const DEFAULT_OPENROUTER_FLASH_MODEL: &str = "deepseek/deepseek-v4-flash";
const DEFAULT_NOVITA_MODEL: &str = "deepseek/deepseek-v4-pro";
@@ -65,6 +67,8 @@ pub enum ProviderKind {
alias = "wanjie_maas"
)]
WanjieArk,
#[serde(alias = "volcengine-ark", alias = "volcengine_ark", alias = "ark")]
Volcengine,
Openrouter,
Novita,
Fireworks,
@@ -82,6 +86,7 @@ impl ProviderKind {
Self::Openai => "openai",
Self::Atlascloud => "atlascloud",
Self::WanjieArk => "wanjie-ark",
Self::Volcengine => "volcengine",
Self::Openrouter => "openrouter",
Self::Novita => "novita",
Self::Fireworks => "fireworks",
@@ -101,6 +106,7 @@ impl ProviderKind {
"atlascloud" | "atlas-cloud" | "atlas_cloud" | "atlas" => Some(Self::Atlascloud),
"wanjie" | "wanjie-ark" | "wanjie_ark" | "ark-wanjie" | "ark_wanjie" | "wanjieark"
| "wanjie-maas" | "wanjie_maas" | "wanjiemaas" => Some(Self::WanjieArk),
"volcengine" | "volcengine-ark" | "volcengine_ark" | "ark" | "volc-ark" | "volcengineark" => Some(Self::Volcengine),
"openrouter" | "open_router" => Some(Self::Openrouter),
"novita" => Some(Self::Novita),
"fireworks" | "fireworks-ai" => Some(Self::Fireworks),
@@ -134,6 +140,8 @@ pub struct ProvidersToml {
#[serde(default)]
pub wanjie_ark: ProviderConfigToml,
#[serde(default)]
pub volcengine: ProviderConfigToml,
#[serde(default)]
pub openrouter: ProviderConfigToml,
#[serde(default)]
pub novita: ProviderConfigToml,
@@ -156,6 +164,7 @@ impl ProvidersToml {
ProviderKind::Openai => &self.openai,
ProviderKind::Atlascloud => &self.atlascloud,
ProviderKind::WanjieArk => &self.wanjie_ark,
ProviderKind::Volcengine => &self.volcengine,
ProviderKind::Openrouter => &self.openrouter,
ProviderKind::Novita => &self.novita,
ProviderKind::Fireworks => &self.fireworks,
@@ -172,6 +181,7 @@ impl ProvidersToml {
ProviderKind::Openai => &mut self.openai,
ProviderKind::Atlascloud => &mut self.atlascloud,
ProviderKind::WanjieArk => &mut self.wanjie_ark,
ProviderKind::Volcengine => &mut self.volcengine,
ProviderKind::Openrouter => &mut self.openrouter,
ProviderKind::Novita => &mut self.novita,
ProviderKind::Fireworks => &mut self.fireworks,
@@ -460,8 +470,11 @@ impl ConfigToml {
serialize_http_headers(&self.providers.atlascloud.http_headers)
}
"providers.wanjie_ark.api_key" => self.providers.wanjie_ark.api_key.clone(),
"providers.volcengine.api_key" => self.providers.volcengine.api_key.clone(),
"providers.wanjie_ark.base_url" => self.providers.wanjie_ark.base_url.clone(),
"providers.volcengine.base_url" => self.providers.volcengine.base_url.clone(),
"providers.wanjie_ark.model" => self.providers.wanjie_ark.model.clone(),
"providers.volcengine.model" => self.providers.volcengine.model.clone(),
"providers.wanjie_ark.http_headers" => {
serialize_http_headers(&self.providers.wanjie_ark.http_headers)
}
@@ -575,6 +588,15 @@ impl ConfigToml {
"providers.atlascloud.http_headers" => {
self.providers.atlascloud.http_headers = parse_http_headers(value)?;
}
"providers.volcengine.api_key" => {
self.providers.volcengine.api_key = Some(value.to_string());
}
"providers.volcengine.base_url" => {
self.providers.volcengine.base_url = Some(value.to_string());
}
"providers.volcengine.model" => {
self.providers.volcengine.model = Some(value.to_string());
}
"providers.wanjie_ark.api_key" => {
self.providers.wanjie_ark.api_key = Some(value.to_string());
}
@@ -719,6 +741,9 @@ impl ConfigToml {
"providers.atlascloud.base_url" => self.providers.atlascloud.base_url = None,
"providers.atlascloud.model" => self.providers.atlascloud.model = None,
"providers.atlascloud.http_headers" => self.providers.atlascloud.http_headers.clear(),
"providers.volcengine.api_key" => self.providers.volcengine.api_key = None,
"providers.volcengine.base_url" => self.providers.volcengine.base_url = None,
"providers.volcengine.model" => self.providers.volcengine.model = None,
"providers.wanjie_ark.api_key" => self.providers.wanjie_ark.api_key = None,
"providers.wanjie_ark.base_url" => self.providers.wanjie_ark.base_url = None,
"providers.wanjie_ark.model" => self.providers.wanjie_ark.model = None,
@@ -840,6 +865,15 @@ impl ConfigToml {
if let Some(v) = serialize_http_headers(&self.providers.atlascloud.http_headers) {
out.insert("providers.atlascloud.http_headers".to_string(), v);
}
if let Some(v) = self.providers.volcengine.api_key.as_ref() {
out.insert("providers.volcengine.api_key".to_string(), redact_secret(v));
}
if let Some(v) = self.providers.volcengine.base_url.as_ref() {
out.insert("providers.volcengine.base_url".to_string(), v.clone());
}
if let Some(v) = self.providers.volcengine.model.as_ref() {
out.insert("providers.volcengine.model".to_string(), v.clone());
}
if let Some(v) = self.providers.wanjie_ark.api_key.as_ref() {
out.insert("providers.wanjie_ark.api_key".to_string(), redact_secret(v));
}
@@ -849,6 +883,9 @@ impl ConfigToml {
if let Some(v) = self.providers.wanjie_ark.model.as_ref() {
out.insert("providers.wanjie_ark.model".to_string(), v.clone());
}
if let Some(v) = serialize_http_headers(&self.providers.volcengine.http_headers) {
out.insert("providers.volcengine.http_headers".to_string(), v);
}
if let Some(v) = serialize_http_headers(&self.providers.wanjie_ark.http_headers) {
out.insert("providers.wanjie_ark.http_headers".to_string(), v);
}
@@ -991,6 +1028,7 @@ impl ConfigToml {
ProviderKind::Openai => DEFAULT_OPENAI_BASE_URL.to_string(),
ProviderKind::Atlascloud => DEFAULT_ATLASCLOUD_BASE_URL.to_string(),
ProviderKind::WanjieArk => DEFAULT_WANJIE_ARK_BASE_URL.to_string(),
ProviderKind::Volcengine => DEFAULT_VOLCENGINE_BASE_URL.to_string(),
ProviderKind::Openrouter => DEFAULT_OPENROUTER_BASE_URL.to_string(),
ProviderKind::Novita => DEFAULT_NOVITA_BASE_URL.to_string(),
ProviderKind::Fireworks => DEFAULT_FIREWORKS_BASE_URL.to_string(),
@@ -1134,7 +1172,7 @@ pub fn load_project_config(workspace: &Path) -> Option<ConfigToml> {
fn normalize_model_for_provider(provider: ProviderKind, model: &str) -> String {
if matches!(
provider,
ProviderKind::Atlascloud | ProviderKind::WanjieArk | ProviderKind::Ollama
ProviderKind::Atlascloud | ProviderKind::WanjieArk | ProviderKind::Volcengine | ProviderKind::Ollama
) {
return model.to_string();
}
@@ -1195,6 +1233,7 @@ fn default_model_for_provider(provider: ProviderKind) -> &'static str {
ProviderKind::Openai => DEFAULT_OPENAI_MODEL,
ProviderKind::Atlascloud => DEFAULT_ATLASCLOUD_MODEL,
ProviderKind::WanjieArk => DEFAULT_WANJIE_ARK_MODEL,
ProviderKind::Volcengine => DEFAULT_VOLCENGINE_MODEL,
ProviderKind::Openrouter => DEFAULT_OPENROUTER_MODEL,
ProviderKind::Novita => DEFAULT_NOVITA_MODEL,
ProviderKind::Fireworks => DEFAULT_FIREWORKS_MODEL,
@@ -1211,6 +1250,7 @@ fn default_base_url_for_provider(provider: ProviderKind) -> &'static str {
ProviderKind::Openai => DEFAULT_OPENAI_BASE_URL,
ProviderKind::Atlascloud => DEFAULT_ATLASCLOUD_BASE_URL,
ProviderKind::WanjieArk => DEFAULT_WANJIE_ARK_BASE_URL,
ProviderKind::Volcengine => DEFAULT_VOLCENGINE_BASE_URL,
ProviderKind::Openrouter => DEFAULT_OPENROUTER_BASE_URL,
ProviderKind::Novita => DEFAULT_NOVITA_BASE_URL,
ProviderKind::Fireworks => DEFAULT_FIREWORKS_BASE_URL,
@@ -1557,6 +1597,7 @@ fn normalize_config_file_path(path: PathBuf) -> Result<PathBuf> {
struct EnvRuntimeOverrides {
provider: Option<ProviderKind>,
model: Option<String>,
volcengine_model: Option<String>,
wanjie_ark_model: Option<String>,
output_mode: Option<String>,
auth_mode: Option<String>,
@@ -1570,6 +1611,7 @@ struct EnvRuntimeOverrides {
nvidia_base_url: Option<String>,
openai_base_url: Option<String>,
atlascloud_base_url: Option<String>,
volcengine_base_url: Option<String>,
wanjie_ark_base_url: Option<String>,
openrouter_base_url: Option<String>,
novita_base_url: Option<String>,
@@ -1586,6 +1628,10 @@ impl EnvRuntimeOverrides {
.ok()
.and_then(|v| ProviderKind::parse(&v)),
model: std::env::var("DEEPSEEK_MODEL").ok(),
volcengine_model: std::env::var("VOLCENGINE_MODEL")
.or_else(|_| std::env::var("VOLCENGINE_ARK_MODEL"))
.ok()
.filter(|v| !v.trim().is_empty()),
wanjie_ark_model: std::env::var("WANJIE_ARK_MODEL")
.or_else(|_| std::env::var("WANJIE_MODEL"))
.or_else(|_| std::env::var("WANJIE_MAAS_MODEL"))
@@ -1620,6 +1666,11 @@ impl EnvRuntimeOverrides {
atlascloud_base_url: std::env::var("ATLASCLOUD_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
volcengine_base_url: std::env::var("VOLCENGINE_BASE_URL")
.or_else(|_| std::env::var("VOLCENGINE_ARK_BASE_URL"))
.or_else(|_| std::env::var("ARK_BASE_URL"))
.ok()
.filter(|v| !v.trim().is_empty()),
wanjie_ark_base_url: std::env::var("WANJIE_ARK_BASE_URL")
.or_else(|_| std::env::var("WANJIE_BASE_URL"))
.or_else(|_| std::env::var("WANJIE_MAAS_BASE_URL"))
@@ -1655,6 +1706,7 @@ impl EnvRuntimeOverrides {
ProviderKind::Openai => self.openai_base_url.clone(),
ProviderKind::Atlascloud => self.atlascloud_base_url.clone(),
ProviderKind::WanjieArk => self.wanjie_ark_base_url.clone(),
ProviderKind::Volcengine => self.volcengine_base_url.clone(),
ProviderKind::Openrouter => self.openrouter_base_url.clone(),
ProviderKind::Novita => self.novita_base_url.clone(),
ProviderKind::Fireworks => self.fireworks_base_url.clone(),
@@ -1667,6 +1719,7 @@ impl EnvRuntimeOverrides {
fn model_for(&self, provider: ProviderKind) -> Option<String> {
match provider {
ProviderKind::WanjieArk => self.wanjie_ark_model.clone(),
ProviderKind::Volcengine => self.volcengine_model.clone(),
_ => None,
}
}
@@ -1718,6 +1771,7 @@ mod tests {
wanjie_ark_base_url: Option<OsString>,
wanjie_base_url: Option<OsString>,
wanjie_maas_base_url: Option<OsString>,
volcengine_model: Option<OsString>,
wanjie_ark_model: Option<OsString>,
wanjie_model: Option<OsString>,
wanjie_maas_model: Option<OsString>,
@@ -1753,6 +1807,7 @@ mod tests {
wanjie_ark_base_url: env::var_os("WANJIE_ARK_BASE_URL"),
wanjie_base_url: env::var_os("WANJIE_BASE_URL"),
wanjie_maas_base_url: env::var_os("WANJIE_MAAS_BASE_URL"),
volcengine_model: env::var_os("VOLCENGINE_MODEL"),
wanjie_ark_model: env::var_os("WANJIE_ARK_MODEL"),
wanjie_model: env::var_os("WANJIE_MODEL"),
wanjie_maas_model: env::var_os("WANJIE_MAAS_MODEL"),
@@ -1833,6 +1888,7 @@ mod tests {
Self::restore_var("WANJIE_ARK_BASE_URL", self.wanjie_ark_base_url.take());
Self::restore_var("WANJIE_BASE_URL", self.wanjie_base_url.take());
Self::restore_var("WANJIE_MAAS_BASE_URL", self.wanjie_maas_base_url.take());
Self::restore_var("VOLCENGINE_MODEL", self.volcengine_model.take());
Self::restore_var("WANJIE_ARK_MODEL", self.wanjie_ark_model.take());
Self::restore_var("WANJIE_MODEL", self.wanjie_model.take());
Self::restore_var("WANJIE_MAAS_MODEL", self.wanjie_maas_model.take());
+5
View File
@@ -540,6 +540,11 @@ pub fn env_for(name: &str) -> Option<String> {
"ollama" | "ollama-local" => &["OLLAMA_API_KEY"],
"openai" => &["OPENAI_API_KEY"],
"atlascloud" | "atlas-cloud" | "atlas_cloud" | "atlas" => &["ATLASCLOUD_API_KEY"],
"volcengine" | "volcengine-ark" | "volcengine_ark" | "ark" | "volc-ark" | "volcengineark" => &[
"VOLCENGINE_API_KEY",
"VOLCENGINE_ARK_API_KEY",
"ARK_API_KEY",
],
"wanjie" | "wanjie-ark" | "wanjie_ark" | "ark-wanjie" | "ark_wanjie" | "wanjieark"
| "wanjie-maas" | "wanjie_maas" | "wanjiemaas" => &[
"WANJIE_ARK_API_KEY",
+3
View File
@@ -904,6 +904,7 @@ pub(super) fn apply_reasoning_effort(
ApiProvider::Openai
| ApiProvider::Atlascloud
| ApiProvider::WanjieArk
| ApiProvider::Volcengine
| ApiProvider::Ollama => {}
ApiProvider::NvidiaNim => {
body["chat_template_kwargs"] = json!({
@@ -941,6 +942,7 @@ pub(super) fn apply_reasoning_effort(
ApiProvider::Openai
| ApiProvider::Atlascloud
| ApiProvider::WanjieArk
| ApiProvider::Volcengine
| ApiProvider::Ollama => {}
ApiProvider::NvidiaNim => {
body["chat_template_kwargs"] = json!({
@@ -970,6 +972,7 @@ pub(super) fn apply_reasoning_effort(
ApiProvider::Openai
| ApiProvider::Atlascloud
| ApiProvider::WanjieArk
| ApiProvider::Volcengine
| ApiProvider::Ollama => {}
ApiProvider::NvidiaNim => {
body["chat_template_kwargs"] = json!({
+37 -3
View File
@@ -41,6 +41,9 @@ pub const DEFAULT_OPENAI_BASE_URL: &str = "https://api.openai.com/v1";
pub const DEFAULT_ATLASCLOUD_MODEL: &str = "deepseek-ai/deepseek-v4-flash";
pub const DEFAULT_ATLASCLOUD_BASE_URL: &str = "https://api.atlascloud.ai/v1";
pub const DEFAULT_WANJIE_ARK_MODEL: &str = "deepseek-reasoner";
pub const DEFAULT_VOLCENGINE_MODEL: &str = "DeepSeek-V4-Pro";
pub const DEFAULT_VOLCENGINE_FLASH_MODEL: &str = "DeepSeek-V4-Flash";
pub const DEFAULT_VOLCENGINE_BASE_URL: &str = "https://ark.cn-beijing.volces.com/api/coding/v3";
pub const DEFAULT_WANJIE_ARK_BASE_URL: &str = "https://maas-openapi.wanjiedata.com/api/v1";
pub const DEFAULT_OPENROUTER_MODEL: &str = "deepseek/deepseek-v4-pro";
pub const DEFAULT_OPENROUTER_FLASH_MODEL: &str = "deepseek/deepseek-v4-flash";
@@ -85,6 +88,7 @@ pub enum ApiProvider {
Openai,
Atlascloud,
WanjieArk,
Volcengine,
Openrouter,
Novita,
Fireworks,
@@ -106,6 +110,7 @@ impl ApiProvider {
"atlascloud" | "atlas-cloud" | "atlas_cloud" | "atlas" => Some(Self::Atlascloud),
"wanjie" | "wanjie-ark" | "wanjie_ark" | "ark-wanjie" | "ark_wanjie" | "wanjieark"
| "wanjie-maas" | "wanjie_maas" | "wanjiemaas" => Some(Self::WanjieArk),
"volcengine" | "volcengine-ark" | "volcengine_ark" | "ark" | "volc-ark" | "volcengineark" => Some(Self::Volcengine),
"openrouter" | "open_router" => Some(Self::Openrouter),
"novita" => Some(Self::Novita),
"fireworks" | "fireworks-ai" => Some(Self::Fireworks),
@@ -125,6 +130,7 @@ impl ApiProvider {
Self::Openai => "openai",
Self::Atlascloud => "atlascloud",
Self::WanjieArk => "wanjie-ark",
Self::Volcengine => "volcengine",
Self::Openrouter => "openrouter",
Self::Novita => "novita",
Self::Fireworks => "fireworks",
@@ -144,6 +150,7 @@ impl ApiProvider {
Self::Openai => "OpenAI-compatible",
Self::Atlascloud => "AtlasCloud",
Self::WanjieArk => "Wanjie Ark",
Self::Volcengine => "Volcengine Ark",
Self::Openrouter => "OpenRouter",
Self::Novita => "Novita AI",
Self::Fireworks => "Fireworks AI",
@@ -162,6 +169,7 @@ impl ApiProvider {
Self::Openai,
Self::Atlascloud,
Self::WanjieArk,
Self::Volcengine,
Self::Openrouter,
Self::Novita,
Self::Fireworks,
@@ -423,7 +431,7 @@ pub fn model_completion_names_for_provider(provider: ApiProvider) -> Vec<&'stati
ApiProvider::WanjieArk => vec![DEFAULT_WANJIE_ARK_MODEL],
ApiProvider::Sglang => vec![DEFAULT_SGLANG_MODEL, DEFAULT_SGLANG_FLASH_MODEL],
ApiProvider::Vllm => vec![DEFAULT_VLLM_MODEL, DEFAULT_VLLM_FLASH_MODEL],
ApiProvider::Openai | ApiProvider::Atlascloud | ApiProvider::Ollama => {
ApiProvider::Openai | ApiProvider::Atlascloud | ApiProvider::Ollama | ApiProvider::Volcengine => {
OFFICIAL_DEEPSEEK_MODELS.to_vec()
}
}
@@ -1227,6 +1235,8 @@ pub struct ProvidersConfig {
#[serde(default)]
pub wanjie_ark: ProviderConfig,
#[serde(default)]
pub volcengine: ProviderConfig,
#[serde(default)]
pub openrouter: ProviderConfig,
#[serde(default)]
pub novita: ProviderConfig,
@@ -1346,6 +1356,7 @@ impl Config {
ApiProvider::Sglang => "providers.sglang",
ApiProvider::Vllm => "providers.vllm",
ApiProvider::Ollama => "providers.ollama",
ApiProvider::Volcengine => "providers.volcengine",
ApiProvider::NvidiaNim => "providers.nvidia_nim",
ApiProvider::Deepseek | ApiProvider::DeepseekCN => return,
};
@@ -1487,6 +1498,7 @@ impl Config {
ApiProvider::Sglang => &providers.sglang,
ApiProvider::Vllm => &providers.vllm,
ApiProvider::Ollama => &providers.ollama,
ApiProvider::Volcengine => &providers.volcengine,
})
}
@@ -1563,6 +1575,7 @@ impl Config {
ApiProvider::Sglang => DEFAULT_SGLANG_MODEL,
ApiProvider::Vllm => DEFAULT_VLLM_MODEL,
ApiProvider::Ollama => DEFAULT_OLLAMA_MODEL,
ApiProvider::Volcengine => DEFAULT_VOLCENGINE_MODEL,
}
.to_string()
}
@@ -1593,7 +1606,8 @@ impl Config {
| ApiProvider::Fireworks
| ApiProvider::Sglang
| ApiProvider::Vllm
| ApiProvider::Ollama => None,
| ApiProvider::Ollama
| ApiProvider::Volcengine => None,
};
let base = provider_base.or(root_base).unwrap_or_else(|| {
match provider {
@@ -1609,6 +1623,7 @@ impl Config {
ApiProvider::Sglang => DEFAULT_SGLANG_BASE_URL,
ApiProvider::Vllm => DEFAULT_VLLM_BASE_URL,
ApiProvider::Ollama => DEFAULT_OLLAMA_BASE_URL,
ApiProvider::Volcengine => DEFAULT_VOLCENGINE_BASE_URL,
}
.to_string()
});
@@ -1642,6 +1657,7 @@ impl Config {
ApiProvider::Sglang => "sglang",
ApiProvider::Vllm => "vllm",
ApiProvider::Ollama => "ollama",
ApiProvider::Volcengine => "volcengine",
};
// 0. DeepSeek compatibility slot. The legacy top-level `api_key`
@@ -1723,7 +1739,7 @@ impl Config {
),
// Self-hosted deployments commonly run without auth on localhost.
// Return an empty key and let the client omit the Authorization header.
ApiProvider::Sglang | ApiProvider::Vllm | ApiProvider::Ollama => Ok(String::new()),
ApiProvider::Sglang | ApiProvider::Vllm | ApiProvider::Ollama | ApiProvider::Volcengine => Ok(String::new()),
}
}
@@ -2283,6 +2299,13 @@ fn apply_env_overrides(config: &mut Config) {
.ollama
.base_url = Some(value);
}
ApiProvider::Volcengine => {
config
.providers
.get_or_insert_with(ProvidersConfig::default)
.volcengine
.base_url = Some(value);
}
ApiProvider::Atlascloud => {
config
.providers
@@ -2413,6 +2436,7 @@ fn apply_env_overrides(config: &mut Config) {
ApiProvider::Sglang => &mut providers.sglang,
ApiProvider::Vllm => &mut providers.vllm,
ApiProvider::Ollama => &mut providers.ollama,
ApiProvider::Volcengine => &mut providers.volcengine,
};
let mut provider_headers = entry.http_headers.clone().unwrap_or_default();
provider_headers.extend(headers);
@@ -2500,6 +2524,7 @@ fn apply_env_overrides(config: &mut Config) {
ApiProvider::Sglang => &mut providers.sglang,
ApiProvider::Vllm => &mut providers.vllm,
ApiProvider::Ollama => &mut providers.ollama,
ApiProvider::Volcengine => &mut providers.volcengine,
};
entry.model = Some(value);
}
@@ -2752,6 +2777,7 @@ pub(crate) fn provider_passes_model_through(provider: ApiProvider) -> bool {
ApiProvider::Openai
| ApiProvider::Atlascloud
| ApiProvider::WanjieArk
| ApiProvider::Volcengine
| ApiProvider::Ollama
)
}
@@ -2777,6 +2803,7 @@ fn default_base_url_for_provider(provider: ApiProvider) -> &'static str {
ApiProvider::Sglang => DEFAULT_SGLANG_BASE_URL,
ApiProvider::Vllm => DEFAULT_VLLM_BASE_URL,
ApiProvider::Ollama => DEFAULT_OLLAMA_BASE_URL,
ApiProvider::Volcengine => DEFAULT_VOLCENGINE_BASE_URL,
}
}
@@ -3004,6 +3031,7 @@ fn merge_providers(
sglang: merge_provider_config(base.sglang, override_cfg.sglang),
vllm: merge_provider_config(base.vllm, override_cfg.vllm),
ollama: merge_provider_config(base.ollama, override_cfg.ollama),
volcengine: merge_provider_config(base.volcengine, override_cfg.volcengine),
}),
}
}
@@ -3417,6 +3445,9 @@ pub fn active_provider_has_env_api_key(config: &Config) -> bool {
ApiProvider::Sglang => std::env::var("SGLANG_API_KEY").is_ok_and(|k| !k.trim().is_empty()),
ApiProvider::Vllm => std::env::var("VLLM_API_KEY").is_ok_and(|k| !k.trim().is_empty()),
ApiProvider::Ollama => std::env::var("OLLAMA_API_KEY").is_ok_and(|k| !k.trim().is_empty()),
ApiProvider::Volcengine => std::env::var("VOLCENGINE_API_KEY").is_ok_and(|k| !k.trim().is_empty())
|| std::env::var("VOLCENGINE_ARK_API_KEY").is_ok_and(|k| !k.trim().is_empty())
|| std::env::var("ARK_API_KEY").is_ok_and(|k| !k.trim().is_empty()),
}
}
@@ -3442,6 +3473,7 @@ pub fn has_api_key_for(config: &Config, provider: ApiProvider) -> bool {
ApiProvider::Sglang => "SGLANG_API_KEY",
ApiProvider::Vllm => "VLLM_API_KEY",
ApiProvider::Ollama => "OLLAMA_API_KEY",
ApiProvider::Volcengine => "VOLCENGINE_API_KEY",
};
if std::env::var(env_var).is_ok_and(|k| !k.trim().is_empty()) {
return true;
@@ -3522,6 +3554,7 @@ pub fn save_api_key_for(provider: ApiProvider, api_key: &str) -> Result<PathBuf>
ApiProvider::Sglang => "providers.sglang",
ApiProvider::Vllm => "providers.vllm",
ApiProvider::Ollama => "providers.ollama",
ApiProvider::Volcengine => "providers.volcengine",
};
// Parse existing TOML (or start fresh) so we can edit the right table
@@ -3558,6 +3591,7 @@ pub fn save_api_key_for(provider: ApiProvider, api_key: &str) -> Result<PathBuf>
ApiProvider::Sglang => "sglang",
ApiProvider::Vllm => "vllm",
ApiProvider::Ollama => "ollama",
ApiProvider::Volcengine => "volcengine",
};
let entry = providers
.entry(key_inside.to_string())
+1
View File
@@ -368,6 +368,7 @@ impl Engine {
ApiProvider::Openai => "OPENAI_API_KEY",
ApiProvider::Atlascloud => "ATLASCLOUD_API_KEY",
ApiProvider::WanjieArk => "WANJIE_ARK_API_KEY/WANJIE_API_KEY/WANJIE_MAAS_API_KEY",
ApiProvider::Volcengine => "VOLCENGINE_API_KEY/VOLCENGINE_ARK_API_KEY/ARK_API_KEY",
ApiProvider::Openrouter => "OPENROUTER_API_KEY",
ApiProvider::Novita => "NOVITA_API_KEY",
ApiProvider::Fireworks => "FIREWORKS_API_KEY",
+4
View File
@@ -1502,6 +1502,9 @@ fn run_setup_status(config: &Config, workspace: &Path) -> Result<()> {
crate::config::ApiProvider::Ollama => {
("OLLAMA_API_KEY", "codewhale auth set --provider ollama")
}
crate::config::ApiProvider::Volcengine => {
("VOLCENGINE_API_KEY", "deepseek auth set --provider volcengine")
}
crate::config::ApiProvider::Deepseek | crate::config::ApiProvider::DeepseekCN => {
("DEEPSEEK_API_KEY", "codewhale auth set --provider deepseek")
}
@@ -1514,6 +1517,7 @@ fn run_setup_status(config: &Config, workspace: &Path) -> Result<()> {
crate::config::ApiProvider::Openai => "openai",
crate::config::ApiProvider::Atlascloud => "atlascloud",
crate::config::ApiProvider::WanjieArk => "wanjie_ark",
crate::config::ApiProvider::Volcengine => "volcengine",
crate::config::ApiProvider::Openrouter => "openrouter",
crate::config::ApiProvider::Novita => "novita",
crate::config::ApiProvider::Fireworks => "fireworks",
+1
View File
@@ -91,6 +91,7 @@ impl ProviderPickerView {
ApiProvider::Openai => "OPENAI_API_KEY",
ApiProvider::Atlascloud => "ATLASCLOUD_API_KEY",
ApiProvider::WanjieArk => "WANJIE_ARK_API_KEY",
ApiProvider::Volcengine => "VOLCENGINE_API_KEY",
ApiProvider::Openrouter => "OPENROUTER_API_KEY",
ApiProvider::Novita => "NOVITA_API_KEY",
ApiProvider::Fireworks => "FIREWORKS_API_KEY",
+2
View File
@@ -5494,6 +5494,7 @@ fn render(f: &mut Frame, app: &mut App) {
crate::config::ApiProvider::Openai => Some("OpenAI"),
crate::config::ApiProvider::Atlascloud => Some("Atlas"),
crate::config::ApiProvider::WanjieArk => Some("Wanjie"),
crate::config::ApiProvider::Volcengine => Some("Volc"),
crate::config::ApiProvider::Openrouter => Some("OR"),
crate::config::ApiProvider::Novita => Some("Novita"),
crate::config::ApiProvider::Fireworks => Some("Fireworks"),
@@ -6258,6 +6259,7 @@ async fn apply_provider_picker_api_key(
ApiProvider::Openai => &mut providers.openai,
ApiProvider::Atlascloud => &mut providers.atlascloud,
ApiProvider::WanjieArk => &mut providers.wanjie_ark,
ApiProvider::Volcengine => &mut providers.volcengine,
ApiProvider::Openrouter => &mut providers.openrouter,
ApiProvider::Novita => &mut providers.novita,
ApiProvider::Fireworks => &mut providers.fireworks,