Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion crates/openfang-cli/src/launcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const PROVIDER_ENV_VARS: &[(&str, &str)] = &[
("ANTHROPIC_API_KEY", "Anthropic"),
("OPENAI_API_KEY", "OpenAI"),
("DEEPSEEK_API_KEY", "DeepSeek"),
("VOLCENGINE_API_KEY", "Volcano Engine"),
("GEMINI_API_KEY", "Gemini"),
("GOOGLE_API_KEY", "Gemini"),
("GROQ_API_KEY", "Groq"),
Expand All @@ -31,7 +32,7 @@ const PROVIDER_ENV_VARS: &[(&str, &str)] = &[

fn detect_provider() -> Option<(&'static str, &'static str)> {
for &(var, name) in PROVIDER_ENV_VARS {
if std::env::var(var).is_ok() {
if std::env::var(var).ok().filter(|v| !v.is_empty()).is_some() {
return Some((name, var));
}
}
Expand Down
22 changes: 22 additions & 0 deletions crates/openfang-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1420,6 +1420,18 @@ fn provider_list() -> Vec<(&'static str, &'static str, &'static str, &'static st
("groq", "GROQ_API_KEY", "llama-3.3-70b-versatile", "Groq"),
("gemini", "GEMINI_API_KEY", "gemini-2.5-flash", "Gemini"),
("deepseek", "DEEPSEEK_API_KEY", "deepseek-chat", "DeepSeek"),
(
"volcengine",
"VOLCENGINE_API_KEY",
"doubao-seed-1-6-251015",
"Volcano Engine",
),
(
"volcengine_coding",
"VOLCENGINE_API_KEY",
"ark-code-latest",
"Volcano Engine Coding Plan",
),
(
"anthropic",
"ANTHROPIC_API_KEY",
Expand Down Expand Up @@ -4541,6 +4553,7 @@ fn provider_to_env_var(provider: &str) -> String {
"perplexity" => "PERPLEXITY_API_KEY".to_string(),
"cohere" => "COHERE_API_KEY".to_string(),
"xai" => "XAI_API_KEY".to_string(),
"volcengine" | "doubao" | "volcengine_coding" => "VOLCENGINE_API_KEY".to_string(),
"brave" => "BRAVE_API_KEY".to_string(),
"tavily" => "TAVILY_API_KEY".to_string(),
other => format!("{}_API_KEY", other.to_uppercase()),
Expand Down Expand Up @@ -4592,6 +4605,15 @@ pub(crate) fn test_api_key(provider: &str, env_var: &str) -> bool {
.get("https://openrouter.ai/api/v1/models")
.bearer_auth(&key)
.send(),
"volcengine" | "doubao" => {
let base = openfang_types::model_catalog::VOLCENGINE_BASE_URL.trim_end_matches('/');
client.get(format!("{base}/models")).bearer_auth(&key).send()
}
"volcengine_coding" => {
let base = openfang_types::model_catalog::VOLCENGINE_CODING_BASE_URL
.trim_end_matches('/');
client.get(format!("{base}/models")).bearer_auth(&key).send()
}
_ => return true, // unknown provider — skip test
};

Expand Down
22 changes: 19 additions & 3 deletions crates/openfang-cli/src/tui/screens/init_wizard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,22 @@ const PROVIDERS: &[ProviderInfo] = &[
needs_key: true,
hint: "",
},
ProviderInfo {
name: "volcengine",
display: "Volcano Engine",
env_var: "VOLCENGINE_API_KEY",
default_model: "doubao-seed-1-6-251015",
needs_key: true,
hint: "cn-beijing; override base_url for other regions",
},
ProviderInfo {
name: "volcengine_coding",
display: "Volcano Engine (Coding Plan)",
env_var: "VOLCENGINE_API_KEY",
default_model: "ark-code-latest",
needs_key: true,
hint: "ByteDance Ark — coding models (cn-beijing)",
},
ProviderInfo {
name: "openrouter",
display: "OpenRouter",
Expand Down Expand Up @@ -143,15 +159,15 @@ const PROVIDERS: &[ProviderInfo] = &[
ProviderInfo {
name: "qwen",
display: "Qwen (Alibaba)",
env_var: "QWEN_API_KEY",
env_var: "DASHSCOPE_API_KEY",
default_model: "qwen-plus",
needs_key: true,
hint: "",
},
ProviderInfo {
name: "huggingface",
display: "Hugging Face",
env_var: "HUGGINGFACE_API_KEY",
env_var: "HF_API_KEY",
default_model: "meta-llama/Llama-3.3-70B-Instruct",
needs_key: true,
hint: "",
Expand All @@ -167,7 +183,7 @@ const PROVIDERS: &[ProviderInfo] = &[
ProviderInfo {
name: "replicate",
display: "Replicate",
env_var: "REPLICATE_API_KEY",
env_var: "REPLICATE_API_TOKEN",
default_model: "meta/meta-llama-3-70b-instruct",
needs_key: true,
hint: "",
Expand Down
3 changes: 2 additions & 1 deletion crates/openfang-cli/src/tui/screens/welcome.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const PROVIDER_ENV_VARS: &[(&str, &str)] = &[
("ANTHROPIC_API_KEY", "Anthropic"),
("OPENAI_API_KEY", "OpenAI"),
("DEEPSEEK_API_KEY", "DeepSeek"),
("VOLCENGINE_API_KEY", "Volcano Engine"),
("GEMINI_API_KEY", "Gemini"),
("GOOGLE_API_KEY", "Gemini"),
("GROQ_API_KEY", "Groq"),
Expand All @@ -47,7 +48,7 @@ const PROVIDER_ENV_VARS: &[(&str, &str)] = &[
/// Returns (provider_name, env_var_name) for the first detected key, or None.
fn detect_provider() -> Option<(&'static str, &'static str)> {
for &(var, name) in PROVIDER_ENV_VARS {
if std::env::var(var).is_ok() {
if std::env::var(var).ok().filter(|v| !v.is_empty()).is_some() {
return Some((name, var));
}
}
Expand Down
41 changes: 39 additions & 2 deletions crates/openfang-cli/src/tui/screens/wizard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::tui::theme;
/// Provider metadata for the setup wizard.
struct ProviderInfo {
name: &'static str,
display: &'static str,
env_var: &'static str,
default_model: &'static str,
needs_key: bool,
Expand All @@ -21,108 +22,140 @@ struct ProviderInfo {
const PROVIDERS: &[ProviderInfo] = &[
ProviderInfo {
name: "groq",
display: "Groq",
env_var: "GROQ_API_KEY",
default_model: "llama-3.3-70b-versatile",
needs_key: true,
},
ProviderInfo {
name: "anthropic",
display: "Anthropic",
env_var: "ANTHROPIC_API_KEY",
default_model: "claude-sonnet-4-20250514",
needs_key: true,
},
ProviderInfo {
name: "openai",
display: "OpenAI",
env_var: "OPENAI_API_KEY",
default_model: "gpt-4o",
needs_key: true,
},
ProviderInfo {
name: "openrouter",
display: "OpenRouter",
env_var: "OPENROUTER_API_KEY",
default_model: "google/gemini-2.5-flash",
needs_key: true,
},
ProviderInfo {
name: "deepseek",
display: "DeepSeek",
env_var: "DEEPSEEK_API_KEY",
default_model: "deepseek-chat",
needs_key: true,
},
ProviderInfo {
name: "together",
display: "Together AI",
env_var: "TOGETHER_API_KEY",
default_model: "meta-llama/Llama-3.3-70B-Instruct-Turbo",
needs_key: true,
},
ProviderInfo {
name: "mistral",
display: "Mistral",
env_var: "MISTRAL_API_KEY",
default_model: "mistral-large-latest",
needs_key: true,
},
ProviderInfo {
name: "fireworks",
display: "Fireworks AI",
env_var: "FIREWORKS_API_KEY",
default_model: "accounts/fireworks/models/llama-v3p3-70b-instruct",
needs_key: true,
},
ProviderInfo {
name: "gemini",
display: "Gemini",
env_var: "GEMINI_API_KEY",
default_model: "gemini-2.5-flash",
needs_key: true,
},
ProviderInfo {
name: "xai",
display: "xAI",
env_var: "XAI_API_KEY",
default_model: "grok-4-0709",
needs_key: true,
},
ProviderInfo {
name: "qwen",
display: "Qwen",
env_var: "DASHSCOPE_API_KEY",
default_model: "qwen-plus",
needs_key: true,
},
ProviderInfo {
name: "volcengine",
display: "Volcano Engine",
env_var: "VOLCENGINE_API_KEY",
default_model: "doubao-seed-1-6-251015",
needs_key: true,
},
ProviderInfo {
name: "volcengine_coding",
display: "Volcano Engine (Coding Plan)",
env_var: "VOLCENGINE_API_KEY",
default_model: "ark-code-latest",
needs_key: true,
},
ProviderInfo {
name: "perplexity",
display: "Perplexity",
env_var: "PERPLEXITY_API_KEY",
default_model: "sonar-pro",
needs_key: true,
},
ProviderInfo {
name: "cohere",
env_var: "CO_API_KEY",
display: "Cohere",
env_var: "COHERE_API_KEY",
default_model: "command-a",
needs_key: true,
},
ProviderInfo {
name: "cerebras",
display: "Cerebras",
env_var: "CEREBRAS_API_KEY",
default_model: "llama-3.3-70b",
needs_key: true,
},
ProviderInfo {
name: "sambanova",
display: "SambaNova",
env_var: "SAMBANOVA_API_KEY",
default_model: "Meta-Llama-3.3-70B-Instruct",
needs_key: true,
},
ProviderInfo {
name: "moonshot",
display: "Moonshot",
env_var: "MOONSHOT_API_KEY",
default_model: "moonshot-v1-128k",
needs_key: true,
},
ProviderInfo {
name: "zhipu",
display: "Zhipu AI",
env_var: "ZHIPU_API_KEY",
default_model: "glm-4-plus",
needs_key: true,
},
ProviderInfo {
name: "zhipu_coding",
display: "Zhipu AI (Coding)",
env_var: "ZHIPU_API_KEY",
default_model: "codegeex-4",
needs_key: true,
Expand All @@ -135,24 +168,28 @@ const PROVIDERS: &[ProviderInfo] = &[
},
ProviderInfo {
name: "claude-code",
display: "Claude Code",
env_var: "",
default_model: "claude-code/sonnet",
needs_key: false,
},
ProviderInfo {
name: "ollama",
display: "Ollama",
env_var: "OLLAMA_API_KEY",
default_model: "llama3.2",
needs_key: false,
},
ProviderInfo {
name: "vllm",
display: "vLLM",
env_var: "VLLM_API_KEY",
default_model: "local-model",
needs_key: false,
},
ProviderInfo {
name: "lmstudio",
display: "LM Studio",
env_var: "LMSTUDIO_API_KEY",
default_model: "local-model",
needs_key: false,
Expand Down Expand Up @@ -544,7 +581,7 @@ fn draw_provider(f: &mut Frame, area: Rect, state: &mut WizardState) {
format!("requires {}", p.env_var)
};
ListItem::new(Line::from(vec![
Span::raw(format!(" {:<14}", p.name)),
Span::raw(format!(" {:<24}", p.display)),
Span::styled(hint, theme::dim_style()),
]))
})
Expand Down
49 changes: 48 additions & 1 deletion crates/openfang-runtime/src/agent_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,24 @@ fn append_tool_error_guidance(tool_result_blocks: &mut Vec<ContentBlock>) {
pub fn strip_provider_prefix(model: &str, provider: &str) -> String {
let slash_prefix = format!("{}/", provider);
let colon_prefix = format!("{}:", provider);
if model.starts_with(&slash_prefix) {
let mut result = if model.starts_with(&slash_prefix) {
model[slash_prefix.len()..].to_string()
} else if model.starts_with(&colon_prefix) {
model[colon_prefix.len()..].to_string()
} else {
model.to_string()
};
// Strip "ark/" catalog namespace prefix before sending to Ark API.
// "ark/" is used internally to disambiguate Ark marketplace models from
// native provider models with the same name (e.g. ark/minimax-m2.5 vs
// minimax provider's minimax-m2.5). The Ark API endpoint expects the bare
// model name (e.g. "minimax-m2.5"), not the namespaced form.
if (provider == "volcengine_coding" || provider == "volcengine" || provider == "doubao")
&& result.starts_with("ark/")
{
result = result["ark/".len()..].to_string();
}
result
}

/// Default context window size (tokens) for token-based trimming.
Expand Down Expand Up @@ -3022,6 +3033,42 @@ mod tests {
assert_eq!(MAX_HISTORY_MESSAGES, 20);
}

#[test]
fn test_strip_ark_catalog_prefix_for_volcengine_coding() {
// ark/ is catalog-only; Ark API expects bare name
assert_eq!(
strip_provider_prefix("ark/doubao-seed-code", "volcengine_coding"),
"doubao-seed-code"
);
}

#[test]
fn test_strip_provider_prefix_ark_volcengine() {
// Should strip ark/ for volcengine
assert_eq!(
strip_provider_prefix("ark/doubao-seed-code", "volcengine"),
"doubao-seed-code"
);
}

#[test]
fn test_strip_provider_prefix_ark_doubao() {
// Should strip ark/ for doubao provider alias
assert_eq!(
strip_provider_prefix("ark/some-model", "doubao"),
"some-model"
);
}

#[test]
fn test_strip_provider_prefix_ark_not_stripped_for_other_providers() {
// Must NOT strip ark/ for non-volcengine providers
assert_eq!(
strip_provider_prefix("ark/some-model", "openai"),
"ark/some-model"
);
}

// --- Integration tests for empty response guards ---

fn test_manifest() -> AgentManifest {
Expand Down
Loading