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
95 changes: 70 additions & 25 deletions crates/openfang-runtime/src/drivers/claude_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,42 @@ impl ClaudeCodeDriver {
}
}

/// Build the CLI argument list for a completion request.
///
/// Exposed as a testable method so unit tests can verify that
/// `--dangerously-skip-permissions`, `--model`, and output format flags
/// are set correctly.
fn build_args(
&self,
prompt: &str,
model_flag: Option<&str>,
streaming: bool,
) -> Vec<String> {
let mut args = vec![
"-p".to_string(),
prompt.to_string(),
];

if self.skip_permissions {
args.push("--dangerously-skip-permissions".to_string());
}

args.push("--output-format".to_string());
if streaming {
args.push("stream-json".to_string());
args.push("--verbose".to_string());
} else {
args.push("json".to_string());
}

if let Some(model) = model_flag {
args.push("--model".to_string());
args.push(model.to_string());
}

args
}

/// Apply security env filtering to a command.
///
/// Instead of `env_clear()` (which breaks Node.js, NVM, SSL, proxies),
Expand Down Expand Up @@ -199,19 +235,10 @@ impl LlmDriver for ClaudeCodeDriver {
let prompt = Self::build_prompt(&request);
let model_flag = Self::model_flag(&request.model);

let mut cmd = tokio::process::Command::new(&self.cli_path);
cmd.arg("-p")
.arg(&prompt)
.arg("--output-format")
.arg("json");
let args = self.build_args(&prompt, model_flag.as_deref(), false);

if self.skip_permissions {
cmd.arg("--dangerously-skip-permissions");
}

if let Some(ref model) = model_flag {
cmd.arg("--model").arg(model);
}
let mut cmd = tokio::process::Command::new(&self.cli_path);
cmd.args(&args);

Self::apply_env_filter(&mut cmd);

Expand Down Expand Up @@ -302,20 +329,10 @@ impl LlmDriver for ClaudeCodeDriver {
let prompt = Self::build_prompt(&request);
let model_flag = Self::model_flag(&request.model);

let mut cmd = tokio::process::Command::new(&self.cli_path);
cmd.arg("-p")
.arg(&prompt)
.arg("--output-format")
.arg("stream-json")
.arg("--verbose");
let args = self.build_args(&prompt, model_flag.as_deref(), true);

if self.skip_permissions {
cmd.arg("--dangerously-skip-permissions");
}

if let Some(ref model) = model_flag {
cmd.arg("--model").arg(model);
}
let mut cmd = tokio::process::Command::new(&self.cli_path);
cmd.args(&args);

Self::apply_env_filter(&mut cmd);

Expand Down Expand Up @@ -538,6 +555,34 @@ mod tests {
assert!(!driver.skip_permissions);
}

#[test]
fn test_build_args_with_skip_permissions() {
let driver = ClaudeCodeDriver::new(None, true);
let args = driver.build_args("hello", Some("opus"), false);
assert!(args.contains(&"--dangerously-skip-permissions".to_string()),
"should contain --dangerously-skip-permissions when skip_permissions=true");
assert!(args.contains(&"json".to_string()));
assert!(args.contains(&"--model".to_string()));
assert!(args.contains(&"opus".to_string()));
}

#[test]
fn test_build_args_without_skip_permissions() {
let driver = ClaudeCodeDriver::new(None, false);
let args = driver.build_args("hello", Some("sonnet"), false);
assert!(!args.contains(&"--dangerously-skip-permissions".to_string()),
"should NOT contain --dangerously-skip-permissions when skip_permissions=false");
}

#[test]
fn test_build_args_streaming() {
let driver = ClaudeCodeDriver::new(None, true);
let args = driver.build_args("hello", None, true);
assert!(args.contains(&"stream-json".to_string()));
assert!(args.contains(&"--verbose".to_string()));
assert!(!args.contains(&"--model".to_string()), "no model flag when model_flag is None");
}

#[test]
fn test_sensitive_env_list_coverage() {
// Ensure all major provider keys are in the strip list
Expand Down
21 changes: 19 additions & 2 deletions crates/openfang-runtime/src/drivers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
pub mod anthropic;
pub mod claude_code;
pub mod copilot;
pub mod qwen_code;
pub mod fallback;
pub mod gemini;
pub mod openai;
Expand Down Expand Up @@ -150,6 +151,11 @@ fn provider_defaults(provider: &str) -> Option<ProviderDefaults> {
api_key_env: "",
key_required: false,
}),
"qwen-code" => Some(ProviderDefaults {
base_url: "",
api_key_env: "",
key_required: false,
}),
"moonshot" | "kimi" | "kimi2" => Some(ProviderDefaults {
base_url: MOONSHOT_BASE_URL,
api_key_env: "MOONSHOT_API_KEY",
Expand Down Expand Up @@ -309,6 +315,15 @@ pub fn create_driver(config: &DriverConfig) -> Result<Arc<dyn LlmDriver>, LlmErr
)));
}

// Qwen Code CLI — subprocess-based, no API key needed
if provider == "qwen-code" {
let cli_path = config.base_url.clone();
return Ok(Arc::new(qwen_code::QwenCodeDriver::new(
cli_path,
config.skip_permissions,
)));
}

// GitHub Copilot — wraps OpenAI-compatible driver with automatic token exchange.
// The CopilotDriver exchanges the GitHub PAT for a Copilot API token on demand,
// caches it, and refreshes when expired.
Expand Down Expand Up @@ -411,7 +426,7 @@ pub fn create_driver(config: &DriverConfig) -> Result<Arc<dyn LlmDriver>, LlmErr
"Unknown provider '{}'. Supported: anthropic, gemini, openai, groq, openrouter, \
deepseek, together, mistral, fireworks, ollama, vllm, lmstudio, perplexity, \
cohere, ai21, cerebras, sambanova, huggingface, xai, replicate, github-copilot, \
chutes, venice, codex, claude-code. Or set base_url for a custom OpenAI-compatible endpoint.",
chutes, venice, codex, claude-code, qwen-code. Or set base_url for a custom OpenAI-compatible endpoint.",
provider
),
})
Expand Down Expand Up @@ -486,6 +501,7 @@ pub fn known_providers() -> &'static [&'static str] {
"venice",
"codex",
"claude-code",
"qwen-code",
]
}

Expand Down Expand Up @@ -587,7 +603,8 @@ mod tests {
assert!(providers.contains(&"chutes"));
assert!(providers.contains(&"codex"));
assert!(providers.contains(&"claude-code"));
assert_eq!(providers.len(), 34);
assert!(providers.contains(&"qwen-code"));
assert_eq!(providers.len(), 35);
}

#[test]
Expand Down
Loading