Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions crates/openfang-cli/src/tui/screens/wizard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ const PROVIDERS: &[ProviderInfo] = &[
default_model: "qwen-plus",
needs_key: true,
},
ProviderInfo {
name: "qwen_coding_intl",
env_var: "DASHSCOPE_API_KEY",
default_model: "qwen3.5-plus",
needs_key: true,
},
ProviderInfo {
name: "perplexity",
env_var: "PERPLEXITY_API_KEY",
Expand Down
42 changes: 42 additions & 0 deletions crates/openfang-runtime/src/browser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,12 @@ impl BrowserSession {
args.insert(0, "--headless=new".to_string());
args.push("--disable-gpu".to_string());
}
// Chromium refuses to run as root without --no-sandbox. Detect this
// without adding a libc dependency by reading the effective UID from
// /proc/self/status (Linux) or falling back to the HOME env var.
if is_running_as_root() {
args.push("--no-sandbox".to_string());
}

let mut cmd = tokio::process::Command::new(&chrome_path);
cmd.args(&args);
Expand Down Expand Up @@ -1154,6 +1160,36 @@ const EXTRACT_CONTENT_JS: &str = r#"(() => {
return JSON.stringify({title, url, content});
})()"#;

// ── Root detection ─────────────────────────────────────────────────────────

/// Returns true if the current process is running as root (UID 0).
///
/// On Linux, reads `/proc/self/status` to get the effective UID without
/// requiring a `libc` dependency. Falls back to checking the `HOME` env var
/// on systems where `/proc` is not available.
fn is_running_as_root() -> bool {
#[cfg(unix)]
{
// Primary: read effective UID from /proc/self/status (Linux)
if let Ok(status) = std::fs::read_to_string("/proc/self/status") {
for line in status.lines() {
if let Some(rest) = line.strip_prefix("Uid:") {
// Format: "Uid: <real> <effective> <saved> <fs>"
if let Some(euid_str) = rest.split_whitespace().nth(1) {
return euid_str == "0";
}
}
}
}
// Fallback: HOME=/root is a reliable indicator on most Unix systems
std::env::var("HOME").map(|h| h == "/root").unwrap_or(false)
}
#[cfg(not(unix))]
{
false
}
}

// ── Tests ──────────────────────────────────────────────────────────────────

#[cfg(test)]
Expand Down Expand Up @@ -1290,6 +1326,12 @@ mod tests {
assert!(mgr.sessions.is_empty());
}

#[test]
fn test_is_running_as_root_returns_bool() {
// Just verify it doesn't panic and returns a bool.
let _ = is_running_as_root();
}

#[test]
fn test_chromium_candidates_not_empty() {
let paths = chromium_candidates();
Expand Down
37 changes: 36 additions & 1 deletion crates/openfang-runtime/src/drivers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ use openfang_types::model_catalog::{
KIMI_CODING_BASE_URL, LEMONADE_BASE_URL, LMSTUDIO_BASE_URL, MINIMAX_BASE_URL,
MISTRAL_BASE_URL, MOONSHOT_BASE_URL, OLLAMA_BASE_URL, OPENAI_BASE_URL,
OPENROUTER_BASE_URL, PERPLEXITY_BASE_URL, QIANFAN_BASE_URL, QWEN_BASE_URL,
REPLICATE_BASE_URL, SAMBANOVA_BASE_URL, TOGETHER_BASE_URL, VENICE_BASE_URL, VLLM_BASE_URL,
QWEN_CODING_BASE_URL, QWEN_CODING_INTL_BASE_URL, QWEN_INTL_BASE_URL, REPLICATE_BASE_URL,
SAMBANOVA_BASE_URL, TOGETHER_BASE_URL, VENICE_BASE_URL, VLLM_BASE_URL,
VOLCENGINE_BASE_URL, VOLCENGINE_CODING_BASE_URL, XAI_BASE_URL, ZAI_BASE_URL,
ZAI_CODING_BASE_URL, ZHIPU_BASE_URL, ZHIPU_CODING_BASE_URL,
};
Expand Down Expand Up @@ -165,6 +166,12 @@ fn provider_defaults(provider: &str) -> Option<ProviderDefaults> {
api_key_env: "DASHSCOPE_API_KEY",
key_required: true,
}),
// DashScope Coding Plan — multi-brand (Qwen, Zhipu/GLM, Kimi, MiniMax)
"qwen_coding_intl" | "dashscope_coding" | "dashscope_coding_intl" => Some(ProviderDefaults {
base_url: QWEN_CODING_INTL_BASE_URL,
api_key_env: "DASHSCOPE_API_KEY",
key_required: true,
}),
"minimax" => Some(ProviderDefaults {
base_url: MINIMAX_BASE_URL,
api_key_env: "MINIMAX_API_KEY",
Expand Down Expand Up @@ -332,6 +339,32 @@ pub fn create_driver(config: &DriverConfig) -> Result<Arc<dyn LlmDriver>, LlmErr
)));
}

// DashScope Coding Plan — requires User-Agent: OpenClaw/1.0
if provider == "qwen_coding_intl" || provider == "dashscope_coding" || provider == "dashscope_coding_intl" {
let api_key = config
.api_key
.clone()
.or_else(|| std::env::var("DASHSCOPE_API_KEY").ok())
.or_else(|| std::env::var("QWEN_API_KEY").ok())
.unwrap_or_default();

if api_key.is_empty() {
return Err(LlmError::MissingApiKey(
"Set DASHSCOPE_API_KEY environment variable for DashScope Coding Plan".to_string(),
));
}

let base_url = config
.base_url
.clone()
.unwrap_or_else(|| QWEN_CODING_INTL_BASE_URL.to_string());

return Ok(Arc::new(
openai::OpenAIDriver::new(api_key, base_url)
.with_extra_headers(vec![("User-Agent".to_string(), "OpenClaw/1.0".to_string())]),
));
}

// Kimi for Code — Anthropic-compatible endpoint
if provider == "kimi_coding" {
let api_key = config
Expand Down Expand Up @@ -475,6 +508,7 @@ pub fn known_providers() -> &'static [&'static str] {
"github-copilot",
"moonshot",
"qwen",
"qwen_coding_intl",
"minimax",
"zhipu",
"zhipu_coding",
Expand Down Expand Up @@ -577,6 +611,7 @@ mod tests {
assert!(providers.contains(&"github-copilot"));
assert!(providers.contains(&"moonshot"));
assert!(providers.contains(&"qwen"));
assert!(providers.contains(&"qwen_coding_intl"));
assert!(providers.contains(&"minimax"));
assert!(providers.contains(&"zhipu"));
assert!(providers.contains(&"zhipu_coding"));
Expand Down
170 changes: 152 additions & 18 deletions crates/openfang-runtime/src/model_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ use openfang_types::model_catalog::{
HUGGINGFACE_BASE_URL, KIMI_CODING_BASE_URL, LEMONADE_BASE_URL, LMSTUDIO_BASE_URL,
MINIMAX_BASE_URL, MISTRAL_BASE_URL, MOONSHOT_BASE_URL, OLLAMA_BASE_URL, OPENAI_BASE_URL,
OPENROUTER_BASE_URL, PERPLEXITY_BASE_URL, QIANFAN_BASE_URL, QWEN_BASE_URL,
REPLICATE_BASE_URL, SAMBANOVA_BASE_URL, TOGETHER_BASE_URL, VENICE_BASE_URL, VLLM_BASE_URL,
VOLCENGINE_BASE_URL, VOLCENGINE_CODING_BASE_URL, XAI_BASE_URL, ZAI_BASE_URL,
ZAI_CODING_BASE_URL, ZHIPU_BASE_URL, ZHIPU_CODING_BASE_URL,
QWEN_CODING_BASE_URL, QWEN_CODING_INTL_BASE_URL, QWEN_INTL_BASE_URL, REPLICATE_BASE_URL,
SAMBANOVA_BASE_URL, TOGETHER_BASE_URL, VENICE_BASE_URL, VLLM_BASE_URL, VOLCENGINE_BASE_URL,
VOLCENGINE_CODING_BASE_URL, XAI_BASE_URL, ZAI_BASE_URL, ZAI_CODING_BASE_URL, ZHIPU_BASE_URL,
ZHIPU_CODING_BASE_URL,
};
use std::collections::HashMap;

Expand Down Expand Up @@ -59,12 +60,11 @@ impl ModelCatalog {
// Claude Code is special: no API key needed, but we probe for CLI
// installation so the dashboard shows "Configured" vs "Not Installed".
if provider.id == "claude-code" {
provider.auth_status =
if crate::drivers::claude_code::claude_code_available() {
AuthStatus::Configured
} else {
AuthStatus::Missing
};
provider.auth_status = if crate::drivers::claude_code::claude_code_available() {
AuthStatus::Configured
} else {
AuthStatus::Missing
};
continue;
}

Expand All @@ -80,8 +80,7 @@ impl ModelCatalog {
let has_fallback = match provider.id.as_str() {
"gemini" => std::env::var("GOOGLE_API_KEY").is_ok(),
"codex" => {
std::env::var("OPENAI_API_KEY").is_ok()
|| read_codex_credential().is_some()
std::env::var("OPENAI_API_KEY").is_ok() || read_codex_credential().is_some()
}
// claude-code is handled above (before key_required check)
_ => false,
Expand Down Expand Up @@ -624,7 +623,7 @@ fn builtin_providers() -> Vec<ProviderInfo> {
auth_status: AuthStatus::Missing,
model_count: 0,
},
// ── Chinese providers (5) ────────────────────────────────────
// ── DashScope / Qwen (Alibaba Cloud) ────────────────────────
ProviderInfo {
id: "qwen".into(),
display_name: "Qwen (Alibaba)".into(),
Expand All @@ -634,6 +633,17 @@ fn builtin_providers() -> Vec<ProviderInfo> {
auth_status: AuthStatus::Missing,
model_count: 0,
},
// DashScope Coding Plan: single API key, multi-brand models
// (Qwen, Zhipu/GLM, Kimi, MiniMax — all via Alibaba Cloud)
ProviderInfo {
id: "qwen_coding_intl".into(),
display_name: "DashScope Coding Plan".into(),
api_key_env: "DASHSCOPE_API_KEY".into(),
base_url: QWEN_CODING_INTL_BASE_URL.into(),
key_required: true,
auth_status: AuthStatus::Missing,
model_count: 0,
},
ProviderInfo {
id: "minimax".into(),
display_name: "MiniMax".into(),
Expand Down Expand Up @@ -2744,7 +2754,7 @@ fn builtin_models() -> Vec<ModelCatalogEntry> {
provider: "qwen".into(),
tier: ModelTier::Frontier,
context_window: 131_072,
max_output_tokens: 8_192,
max_output_tokens: 32_768,
input_cost_per_m: 4.00,
output_cost_per_m: 12.00,
supports_tools: true,
Expand Down Expand Up @@ -2809,6 +2819,133 @@ fn builtin_models() -> Vec<ModelCatalogEntry> {
aliases: vec![],
},
// ══════════════════════════════════════════════════════════════
// DashScope Coding Plan — International
// All accessed via DASHSCOPE_API_KEY on qwen_coding_intl provider.
// Model IDs use "qwen_coding_intl/<model>" format — the provider prefix is
// stripped automatically by strip_provider_prefix() before the API call,
// so the API receives the bare model name (e.g. "glm-5", "kimi-k2.5").
// This mirrors the OpenRouter pattern and avoids collisions with native providers.
// ══════════════════════════════════════════════════════════════
// ── Qwen (4) ────────────────────────────────────────────────
ModelCatalogEntry {
id: "qwen3.5-plus".into(),
display_name: "Qwen 3.5 Plus".into(),
provider: "qwen_coding_intl".into(),
tier: ModelTier::Smart,
context_window: 1_000_000,
max_output_tokens: 65_536,
input_cost_per_m: 0.40,
output_cost_per_m: 2.40,
supports_tools: true,
supports_vision: true,
supports_streaming: true,
aliases: vec![],
},
ModelCatalogEntry {
id: "qwen3-max-2026-01-23".into(),
display_name: "Qwen 3 Max (2026-01-23)".into(),
provider: "qwen_coding_intl".into(),
tier: ModelTier::Frontier,
context_window: 131_072,
max_output_tokens: 32_768,
input_cost_per_m: 4.00,
output_cost_per_m: 16.00,
supports_tools: true,
supports_vision: false,
supports_streaming: true,
aliases: vec![],
},
ModelCatalogEntry {
id: "qwen3-coder-plus".into(),
display_name: "Qwen 3 Coder Plus".into(),
provider: "qwen_coding_intl".into(),
tier: ModelTier::Smart,
context_window: 131_072,
max_output_tokens: 32_768,
input_cost_per_m: 0.80,
output_cost_per_m: 3.20,
supports_tools: true,
supports_vision: false,
supports_streaming: true,
aliases: vec!["qwen3-coder".into()],
},
ModelCatalogEntry {
id: "qwen3-coder-next".into(),
display_name: "Qwen 3 Coder Next".into(),
provider: "qwen_coding_intl".into(),
tier: ModelTier::Frontier,
context_window: 131_072,
max_output_tokens: 32_768,
input_cost_per_m: 4.00,
output_cost_per_m: 16.00,
supports_tools: true,
supports_vision: false,
supports_streaming: true,
aliases: vec![],
},
// ── Zhipu / GLM via Coding Plan (2) ─────────────────────────
// API receives "glm-5" / "glm-4.7" after prefix stripping.
ModelCatalogEntry {
id: "qwen_coding_intl/glm-5".into(),
display_name: "GLM-5 (Coding Plan)".into(),
provider: "qwen_coding_intl".into(),
tier: ModelTier::Frontier,
context_window: 128_000,
max_output_tokens: 32_768,
input_cost_per_m: 4.00,
output_cost_per_m: 16.00,
supports_tools: true,
supports_vision: false,
supports_streaming: true,
aliases: vec![],
},
ModelCatalogEntry {
id: "qwen_coding_intl/glm-4.7".into(),
display_name: "GLM-4.7 (Coding Plan)".into(),
provider: "qwen_coding_intl".into(),
tier: ModelTier::Smart,
context_window: 128_000,
max_output_tokens: 32_768,
input_cost_per_m: 0.80,
output_cost_per_m: 3.20,
supports_tools: true,
supports_vision: false,
supports_streaming: true,
aliases: vec![],
},
// ── Moonshot / Kimi via Coding Plan (1) ─────────────────────
// API receives "kimi-k2.5" after prefix stripping.
ModelCatalogEntry {
id: "qwen_coding_intl/kimi-k2.5".into(),
display_name: "Kimi K2.5 (Coding Plan)".into(),
provider: "qwen_coding_intl".into(),
tier: ModelTier::Smart,
context_window: 128_000,
max_output_tokens: 32_768,
input_cost_per_m: 0.80,
output_cost_per_m: 3.20,
supports_tools: true,
supports_vision: true,
supports_streaming: true,
aliases: vec![],
},
// ── MiniMax via Coding Plan (1) ──────────────────────────────
// API receives "MiniMax-M2.5" after prefix stripping.
ModelCatalogEntry {
id: "qwen_coding_intl/MiniMax-M2.5".into(),
display_name: "MiniMax M2.5 (Coding Plan)".into(),
provider: "qwen_coding_intl".into(),
tier: ModelTier::Smart,
context_window: 1_000_000,
max_output_tokens: 32_768,
input_cost_per_m: 0.80,
output_cost_per_m: 3.20,
supports_tools: true,
supports_vision: false,
supports_streaming: true,
aliases: vec![],
},
// ══════════════════════════════════════════════════════════════
// MiniMax (6)
// ══════════════════════════════════════════════════════════════
ModelCatalogEntry {
Expand Down Expand Up @@ -3549,7 +3686,7 @@ mod tests {
#[test]
fn test_catalog_has_providers() {
let catalog = ModelCatalog::new();
assert_eq!(catalog.list_providers().len(), 38);
assert_eq!(catalog.list_providers().len(), 39);
}

#[test]
Expand Down Expand Up @@ -3584,10 +3721,7 @@ mod tests {
#[test]
fn test_resolve_alias() {
let catalog = ModelCatalog::new();
assert_eq!(
catalog.resolve_alias("sonnet"),
Some("claude-sonnet-4-6")
);
assert_eq!(catalog.resolve_alias("sonnet"), Some("claude-sonnet-4-6"));
assert_eq!(
catalog.resolve_alias("haiku"),
Some("claude-haiku-4-5-20251001")
Expand Down
6 changes: 6 additions & 0 deletions crates/openfang-types/src/model_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ pub const GITHUB_COPILOT_BASE_URL: &str = "https://api.githubcopilot.com";

// ── Chinese providers ─────────────────────────────────────────────
pub const QWEN_BASE_URL: &str = "https://dashscope.aliyuncs.com/compatible-mode/v1";
/// DashScope international endpoint (ap-southeast-1 and other international regions).
pub const QWEN_INTL_BASE_URL: &str = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
/// DashScope Coding Plan — China endpoint. Multi-brand: Qwen, Zhipu, Kimi, MiniMax.
pub const QWEN_CODING_BASE_URL: &str = "https://coding.dashscope.aliyuncs.com/compatible-mode/v1";
/// DashScope Coding Plan — International endpoint. Multi-brand: Qwen, Zhipu, Kimi, MiniMax.
pub const QWEN_CODING_INTL_BASE_URL: &str = "https://coding-intl.dashscope.aliyuncs.com/v1";
pub const MINIMAX_BASE_URL: &str = "https://api.minimaxi.chat/v1";
pub const ZHIPU_BASE_URL: &str = "https://open.bigmodel.cn/api/paas/v4";
pub const ZHIPU_CODING_BASE_URL: &str = "https://open.bigmodel.cn/api/coding/paas/v4";
Expand Down