From ed93991e9bae05ad88b404a9bff320084d6d545e Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Thu, 26 Jun 2025 11:57:03 +1000 Subject: [PATCH 1/8] working now with claude code --- crates/goose/src/providers/factory.rs | 3 + crates/goose/src/providers/mod.rs | 1 + crates/goose/src/providers/subprocess.rs | 335 +++++++++++++++++++++++ 3 files changed, 339 insertions(+) create mode 100644 crates/goose/src/providers/subprocess.rs diff --git a/crates/goose/src/providers/factory.rs b/crates/goose/src/providers/factory.rs index 11757aa9b8b5..619b863920cd 100644 --- a/crates/goose/src/providers/factory.rs +++ b/crates/goose/src/providers/factory.rs @@ -16,6 +16,7 @@ use super::{ openrouter::OpenRouterProvider, sagemaker_tgi::SageMakerTgiProvider, snowflake::SnowflakeProvider, + subprocess::SubprocessProvider, venice::VeniceProvider, xai::XaiProvider, }; @@ -51,6 +52,7 @@ pub fn providers() -> Vec { OpenAiProvider::metadata(), OpenRouterProvider::metadata(), SageMakerTgiProvider::metadata(), + SubprocessProvider::metadata(), VeniceProvider::metadata(), SnowflakeProvider::metadata(), XaiProvider::metadata(), @@ -127,6 +129,7 @@ fn create_provider(name: &str, model: ModelConfig) -> Result> "gcp_vertex_ai" => Ok(Arc::new(GcpVertexAIProvider::from_env(model)?)), "google" => Ok(Arc::new(GoogleProvider::from_env(model)?)), "sagemaker_tgi" => Ok(Arc::new(SageMakerTgiProvider::from_env(model)?)), + "subprocess" => Ok(Arc::new(SubprocessProvider::from_env(model)?)), "venice" => Ok(Arc::new(VeniceProvider::from_env(model)?)), "snowflake" => Ok(Arc::new(SnowflakeProvider::from_env(model)?)), "github_copilot" => Ok(Arc::new(GithubCopilotProvider::from_env(model)?)), diff --git a/crates/goose/src/providers/mod.rs b/crates/goose/src/providers/mod.rs index decd346a6c6b..e61f6d6eb850 100644 --- a/crates/goose/src/providers/mod.rs +++ b/crates/goose/src/providers/mod.rs @@ -20,6 +20,7 @@ pub mod openai; pub mod openrouter; pub mod sagemaker_tgi; pub mod snowflake; +pub mod subprocess; pub mod toolshim; pub mod utils; pub mod utils_universal_openai_stream; diff --git a/crates/goose/src/providers/subprocess.rs b/crates/goose/src/providers/subprocess.rs new file mode 100644 index 000000000000..4c970e2f3692 --- /dev/null +++ b/crates/goose/src/providers/subprocess.rs @@ -0,0 +1,335 @@ +use anyhow::Result; +use async_trait::async_trait; +use serde_json::{json, Value}; +use std::process::Stdio; +use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::process::Command; + +use super::base::{ConfigKey, Provider, ProviderMetadata, ProviderUsage, Usage}; +use super::errors::ProviderError; +use super::utils::emit_debug_trace; +use crate::message::{Message, MessageContent}; +use crate::model::ModelConfig; +use mcp_core::content::TextContent; +use mcp_core::tool::Tool; +use mcp_core::Role; + +pub const SUBPROCESS_DEFAULT_MODEL: &str = "default"; +pub const SUBPROCESS_KNOWN_MODELS: &[&str] = &["default"]; + +pub const SUBPROCESS_DOC_URL: &str = "https://claude.ai/cli"; + +#[derive(Debug, serde::Serialize)] +pub struct SubprocessProvider { + command: String, + model: ModelConfig, +} + +impl Default for SubprocessProvider { + fn default() -> Self { + let model = ModelConfig::new(SubprocessProvider::metadata().default_model); + SubprocessProvider::from_env(model).expect("Failed to initialize Subprocess provider") + } +} + +impl SubprocessProvider { + pub fn from_env(model: ModelConfig) -> Result { + let config = crate::config::Config::global(); + let command: String = config + .get_param("SUBPROCESS_COMMAND") + .unwrap_or_else(|_| "claude".to_string()); + + Ok(Self { command, model }) + } + + /// Convert goose messages to the format expected by claude CLI + fn messages_to_claude_format(&self, _system: &str, messages: &[Message]) -> Result { + let mut claude_messages = Vec::new(); + + for message in messages { + let role = match message.role { + Role::User => "user", + Role::Assistant => "assistant", + }; + + let mut content_parts = Vec::new(); + for content in &message.content { + match content { + MessageContent::Text(text_content) => { + content_parts.push(json!({ + "type": "text", + "text": text_content.text + })); + } + MessageContent::ToolRequest(tool_request) => { + if let Ok(tool_call) = &tool_request.tool_call { + content_parts.push(json!({ + "type": "tool_use", + "id": tool_request.id, + "name": tool_call.name, + "input": tool_call.arguments + })); + } + } + MessageContent::ToolResponse(tool_response) => { + if let Ok(tool_contents) = &tool_response.tool_result { + // Convert tool result contents to text + let content_text = tool_contents + .iter() + .filter_map(|content| content.as_text()) + .collect::>() + .join("\n"); + + content_parts.push(json!({ + "type": "tool_result", + "tool_use_id": tool_response.id, + "content": content_text + })); + } + } + _ => { + // Skip other content types for now + } + } + } + + claude_messages.push(json!({ + "role": role, + "content": content_parts + })); + } + + Ok(json!(claude_messages)) + } + + /// Parse the JSON response from claude CLI + fn parse_claude_response( + &self, + json_lines: &[String], + ) -> Result<(Message, Usage), ProviderError> { + let mut all_text_content = Vec::new(); + let mut usage = Usage::default(); + + // Join all lines and parse as a single JSON array + let full_response = json_lines.join(""); + let json_array: Vec = serde_json::from_str(&full_response) + .map_err(|e| ProviderError::RequestFailed(format!("Failed to parse JSON response: {}", e)))?; + + for parsed in json_array { + if let Some(msg_type) = parsed.get("type").and_then(|t| t.as_str()) { + match msg_type { + "assistant" => { + if let Some(message) = parsed.get("message") { + // Extract text content from this assistant message + if let Some(content) = message.get("content").and_then(|c| c.as_array()) { + for item in content { + if let Some(content_type) = item.get("type").and_then(|t| t.as_str()) { + if content_type == "text" { + if let Some(text) = item.get("text").and_then(|t| t.as_str()) { + all_text_content.push(text.to_string()); + } + } + // Skip tool_use - those are claude CLI's internal tools + } + } + } + + // Extract usage information + if let Some(usage_info) = message.get("usage") { + usage.input_tokens = usage_info + .get("input_tokens") + .and_then(|v| v.as_i64()) + .map(|v| v as i32); + usage.output_tokens = usage_info + .get("output_tokens") + .and_then(|v| v.as_i64()) + .map(|v| v as i32); + + // Calculate total if not provided + if usage.total_tokens.is_none() { + if let (Some(input), Some(output)) = + (usage.input_tokens, usage.output_tokens) + { + usage.total_tokens = Some(input + output); + } + } + } + } + } + "result" => { + // Extract additional usage info from result if available + if let Some(result_usage) = parsed.get("usage") { + if usage.input_tokens.is_none() { + usage.input_tokens = result_usage + .get("input_tokens") + .and_then(|v| v.as_i64()) + .map(|v| v as i32); + } + if usage.output_tokens.is_none() { + usage.output_tokens = result_usage + .get("output_tokens") + .and_then(|v| v.as_i64()) + .map(|v| v as i32); + } + } + } + _ => {} // Ignore other message types + } + } + } + + // Combine all text content into a single message + let combined_text = all_text_content.join("\n\n"); + if combined_text.is_empty() { + return Err(ProviderError::RequestFailed("No text content found in response".to_string())); + } + + let message_content = vec![MessageContent::Text(TextContent { + text: combined_text, + annotations: None, + })]; + + let response_message = Message { + role: Role::Assistant, + created: chrono::Utc::now().timestamp(), + content: message_content, + }; + + Ok((response_message, usage)) + } + + async fn execute_command( + &self, + system: &str, + messages: &[Message], + _tools: &[Tool], + ) -> Result, ProviderError> { + let messages_json = self + .messages_to_claude_format(system, messages) + .map_err(|e| { + ProviderError::RequestFailed(format!("Failed to format messages: {}", e)) + })?; + + let mut cmd = Command::new(&self.command); + cmd.arg("-p") + .arg(messages_json.to_string()) + .arg("--system-prompt") + .arg(system) + .arg("--verbose") + .arg("--output-format") + .arg("json"); + + // Let claude CLI use its own configured model + + cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); + + let mut child = cmd + .spawn() + .map_err(|e| ProviderError::RequestFailed(format!("Failed to spawn command: {}", e)))?; + + let stdout = child + .stdout + .take() + .ok_or_else(|| ProviderError::RequestFailed("Failed to capture stdout".to_string()))?; + + let mut reader = BufReader::new(stdout); + let mut lines = Vec::new(); + let mut line = String::new(); + + loop { + line.clear(); + match reader.read_line(&mut line).await { + Ok(0) => break, // EOF + Ok(_) => { + let trimmed = line.trim(); + if !trimmed.is_empty() { + lines.push(trimmed.to_string()); + } + } + Err(e) => { + return Err(ProviderError::RequestFailed(format!( + "Failed to read output: {}", + e + ))); + } + } + } + + let exit_status = child.wait().await.map_err(|e| { + ProviderError::RequestFailed(format!("Failed to wait for command: {}", e)) + })?; + + if !exit_status.success() { + return Err(ProviderError::RequestFailed(format!( + "Command failed with exit code: {:?}", + exit_status.code() + ))); + } + + tracing::debug!("Command executed successfully, got {} lines", lines.len()); + for (i, line) in lines.iter().enumerate() { + tracing::debug!("Line {}: {}", i, line); + } + + Ok(lines) + } +} + +#[async_trait] +impl Provider for SubprocessProvider { + fn metadata() -> ProviderMetadata { + ProviderMetadata::new( + "subprocess", + "Subprocess", + "Execute AI models via command-line tools (e.g., claude CLI)", + SUBPROCESS_DEFAULT_MODEL, + SUBPROCESS_KNOWN_MODELS.to_vec(), + SUBPROCESS_DOC_URL, + vec![ConfigKey::new( + "SUBPROCESS_COMMAND", + false, + false, + Some("claude"), + )], + ) + } + + fn get_model_config(&self) -> ModelConfig { + self.model.clone() + } + + #[tracing::instrument( + skip(self, system, messages, tools), + fields(model_config, input, output, input_tokens, output_tokens, total_tokens) + )] + async fn complete( + &self, + system: &str, + messages: &[Message], + tools: &[Tool], + ) -> Result<(Message, ProviderUsage), ProviderError> { + let json_lines = self.execute_command(system, messages, tools).await?; + + let (message, usage) = self.parse_claude_response(&json_lines)?; + + // Create a dummy payload for debug tracing + let payload = json!({ + "command": self.command, + "model": self.model.model_name, + "system": system, + "messages": messages.len() + }); + + let response = json!({ + "lines": json_lines.len(), + "usage": usage + }); + + emit_debug_trace(&self.model, &payload, &response, &usage); + + Ok(( + message, + ProviderUsage::new(self.model.model_name.clone(), usage), + )) + } +} From a239ed8aa736e0be39932b2ab787b22873ad9ee4 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Thu, 26 Jun 2025 12:27:58 +1000 Subject: [PATCH 2/8] logging --- crates/goose/src/providers/subprocess.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/goose/src/providers/subprocess.rs b/crates/goose/src/providers/subprocess.rs index 4c970e2f3692..a5384edf8b0c 100644 --- a/crates/goose/src/providers/subprocess.rs +++ b/crates/goose/src/providers/subprocess.rs @@ -210,6 +210,12 @@ impl SubprocessProvider { ProviderError::RequestFailed(format!("Failed to format messages: {}", e)) })?; + println!("=== SUBPROCESS PROVIDER DEBUG ==="); + println!("Command: {}", self.command); + println!("System prompt: {}", system); + println!("Messages JSON: {}", serde_json::to_string_pretty(&messages_json).unwrap_or_else(|_| "Failed to serialize".to_string())); + println!("================================"); + let mut cmd = Command::new(&self.command); cmd.arg("-p") .arg(messages_json.to_string()) From 0453781bc05fcafa39a7dce4129a0254454e6dfb Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Thu, 26 Jun 2025 12:50:08 +1000 Subject: [PATCH 3/8] smaller prompt --- crates/goose/src/providers/subprocess.rs | 47 +++++++++++++++++++++--- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/crates/goose/src/providers/subprocess.rs b/crates/goose/src/providers/subprocess.rs index a5384edf8b0c..f4fd90d05af3 100644 --- a/crates/goose/src/providers/subprocess.rs +++ b/crates/goose/src/providers/subprocess.rs @@ -42,6 +42,35 @@ impl SubprocessProvider { Ok(Self { command, model }) } + /// Create a simplified system prompt without Extensions section + fn create_simplified_system_prompt(&self) -> String { + let current_date = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S"); + let current_dir = std::env::current_dir() + .map(|p| p.display().to_string()) + .unwrap_or_else(|_| "unknown".to_string()); + + format!( + "You are a general-purpose AI agent called Goose, created by Block, the parent company of Square, CashApp, and Tidal. Goose is being developed as an open-source software project. + +The current date is {}. + +You are working in the directory: {} + +You have access to your own built-in tools for file operations, shell commands, and other tasks. Use them as needed to help the user accomplish their goals. + +# Response Guidelines + +- Use Markdown formatting for all responses. +- Follow best practices for Markdown, including: + - Using headers for organization. + - Bullet points for lists. + - Links formatted correctly. +- For code examples, use fenced code blocks with language identifiers. +- Ensure clarity, conciseness, and proper formatting to enhance readability and usability.", + current_date, current_dir + ) + } + /// Convert goose messages to the format expected by claude CLI fn messages_to_claude_format(&self, _system: &str, messages: &[Message]) -> Result { let mut claude_messages = Vec::new(); @@ -210,17 +239,23 @@ impl SubprocessProvider { ProviderError::RequestFailed(format!("Failed to format messages: {}", e)) })?; - println!("=== SUBPROCESS PROVIDER DEBUG ==="); - println!("Command: {}", self.command); - println!("System prompt: {}", system); - println!("Messages JSON: {}", serde_json::to_string_pretty(&messages_json).unwrap_or_else(|_| "Failed to serialize".to_string())); - println!("================================"); + // Create a simplified system prompt without Extensions section + let simplified_system = self.create_simplified_system_prompt(); + + if std::env::var("GOOSE_SUBPROCESS_DEBUG").is_ok() { + println!("=== SUBPROCESS PROVIDER DEBUG ==="); + println!("Command: {}", self.command); + println!("Original system prompt length: {} chars", system.len()); + println!("Simplified system prompt: {}", simplified_system); + println!("Messages JSON: {}", serde_json::to_string_pretty(&messages_json).unwrap_or_else(|_| "Failed to serialize".to_string())); + println!("================================"); + } let mut cmd = Command::new(&self.command); cmd.arg("-p") .arg(messages_json.to_string()) .arg("--system-prompt") - .arg(system) + .arg(&simplified_system) // Use simplified prompt instead of original .arg("--verbose") .arg("--output-format") .arg("json"); From 3f4e0c2b2cc07a935b0afdde22bdc3f26b798475 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Thu, 26 Jun 2025 13:16:53 +1000 Subject: [PATCH 4/8] now working --- crates/goose/src/providers/subprocess.rs | 109 ++++++++++++++++------- 1 file changed, 78 insertions(+), 31 deletions(-) diff --git a/crates/goose/src/providers/subprocess.rs b/crates/goose/src/providers/subprocess.rs index f4fd90d05af3..f73584b920c5 100644 --- a/crates/goose/src/providers/subprocess.rs +++ b/crates/goose/src/providers/subprocess.rs @@ -42,33 +42,26 @@ impl SubprocessProvider { Ok(Self { command, model }) } - /// Create a simplified system prompt without Extensions section - fn create_simplified_system_prompt(&self) -> String { - let current_date = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S"); - let current_dir = std::env::current_dir() - .map(|p| p.display().to_string()) - .unwrap_or_else(|_| "unknown".to_string()); - - format!( - "You are a general-purpose AI agent called Goose, created by Block, the parent company of Square, CashApp, and Tidal. Goose is being developed as an open-source software project. - -The current date is {}. - -You are working in the directory: {} - -You have access to your own built-in tools for file operations, shell commands, and other tasks. Use them as needed to help the user accomplish their goals. - -# Response Guidelines - -- Use Markdown formatting for all responses. -- Follow best practices for Markdown, including: - - Using headers for organization. - - Bullet points for lists. - - Links formatted correctly. -- For code examples, use fenced code blocks with language identifiers. -- Ensure clarity, conciseness, and proper formatting to enhance readability and usability.", - current_date, current_dir - ) + /// Filter out the Extensions section from the system prompt + fn filter_extensions_from_system_prompt(&self, system: &str) -> String { + // Find the Extensions section and remove it + if let Some(extensions_start) = system.find("# Extensions") { + // Look for the next major section that starts with # + let after_extensions = &system[extensions_start..]; + if let Some(next_section_pos) = after_extensions[1..].find("\n# ") { + // Found next section, keep everything before Extensions and after the next section + let before_extensions = &system[..extensions_start]; + let next_section_start = extensions_start + next_section_pos + 1; + let after_next_section = &system[next_section_start..]; + format!("{}{}", before_extensions.trim_end(), after_next_section) + } else { + // No next section found, just remove everything from Extensions onward + system[..extensions_start].trim_end().to_string() + } + } else { + // No Extensions section found, return original + system.to_string() + } } /// Convert goose messages to the format expected by claude CLI @@ -239,14 +232,15 @@ You have access to your own built-in tools for file operations, shell commands, ProviderError::RequestFailed(format!("Failed to format messages: {}", e)) })?; - // Create a simplified system prompt without Extensions section - let simplified_system = self.create_simplified_system_prompt(); + // Create a filtered system prompt without Extensions section + let filtered_system = self.filter_extensions_from_system_prompt(system); if std::env::var("GOOSE_SUBPROCESS_DEBUG").is_ok() { println!("=== SUBPROCESS PROVIDER DEBUG ==="); println!("Command: {}", self.command); println!("Original system prompt length: {} chars", system.len()); - println!("Simplified system prompt: {}", simplified_system); + println!("Filtered system prompt length: {} chars", filtered_system.len()); + println!("Filtered system prompt: {}", filtered_system); println!("Messages JSON: {}", serde_json::to_string_pretty(&messages_json).unwrap_or_else(|_| "Failed to serialize".to_string())); println!("================================"); } @@ -255,7 +249,7 @@ You have access to your own built-in tools for file operations, shell commands, cmd.arg("-p") .arg(messages_json.to_string()) .arg("--system-prompt") - .arg(&simplified_system) // Use simplified prompt instead of original + .arg(&filtered_system) // Use filtered prompt instead of original .arg("--verbose") .arg("--output-format") .arg("json"); @@ -314,6 +308,54 @@ You have access to your own built-in tools for file operations, shell commands, Ok(lines) } + + /// Generate a simple session description without calling subprocess + fn generate_simple_session_description( + &self, + messages: &[Message], + ) -> Result<(Message, ProviderUsage), ProviderError> { + // Extract the first user message text + let description = messages + .iter() + .find(|m| m.role == mcp_core::Role::User) + .and_then(|m| { + m.content.iter().find_map(|c| match c { + MessageContent::Text(text_content) => Some(&text_content.text), + _ => None, + }) + }) + .map(|text| { + // Take first few words, limit to 4 words + text.split_whitespace() + .take(4) + .collect::>() + .join(" ") + }) + .unwrap_or_else(|| "Simple task".to_string()); + + if std::env::var("GOOSE_SUBPROCESS_DEBUG").is_ok() { + println!("=== SUBPROCESS PROVIDER DEBUG ==="); + println!("Generated simple session description: {}", description); + println!("Skipped subprocess call for session description"); + println!("================================"); + } + + let message = Message { + role: mcp_core::Role::Assistant, + created: chrono::Utc::now().timestamp(), + content: vec![MessageContent::Text(mcp_core::content::TextContent { + text: description.clone(), + annotations: None, + })], + }; + + let usage = Usage::default(); + + Ok(( + message, + ProviderUsage::new(self.model.model_name.clone(), usage), + )) + } } #[async_trait] @@ -349,6 +391,11 @@ impl Provider for SubprocessProvider { messages: &[Message], tools: &[Tool], ) -> Result<(Message, ProviderUsage), ProviderError> { + // Check if this is a session description request (short system prompt asking for 4 words or less) + if system.contains("four words or less") || system.contains("4 words or less") { + return self.generate_simple_session_description(messages); + } + let json_lines = self.execute_command(system, messages, tools).await?; let (message, usage) = self.parse_claude_response(&json_lines)?; From 9d6a5323fac91655c3800fc5f55f28070761c20e Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Thu, 26 Jun 2025 14:43:55 +1000 Subject: [PATCH 5/8] now has claude code and gemini cli --- .../{subprocess.rs => claude_code.rs} | 53 ++-- crates/goose/src/providers/factory.rs | 9 +- crates/goose/src/providers/gemini_cli.rs | 292 ++++++++++++++++++ crates/goose/src/providers/mod.rs | 3 +- 4 files changed, 324 insertions(+), 33 deletions(-) rename crates/goose/src/providers/{subprocess.rs => claude_code.rs} (92%) create mode 100644 crates/goose/src/providers/gemini_cli.rs diff --git a/crates/goose/src/providers/subprocess.rs b/crates/goose/src/providers/claude_code.rs similarity index 92% rename from crates/goose/src/providers/subprocess.rs rename to crates/goose/src/providers/claude_code.rs index f73584b920c5..fa160525949e 100644 --- a/crates/goose/src/providers/subprocess.rs +++ b/crates/goose/src/providers/claude_code.rs @@ -14,29 +14,29 @@ use mcp_core::content::TextContent; use mcp_core::tool::Tool; use mcp_core::Role; -pub const SUBPROCESS_DEFAULT_MODEL: &str = "default"; -pub const SUBPROCESS_KNOWN_MODELS: &[&str] = &["default"]; +pub const CLAUDE_CODE_DEFAULT_MODEL: &str = "default"; +pub const CLAUDE_CODE_KNOWN_MODELS: &[&str] = &["default"]; -pub const SUBPROCESS_DOC_URL: &str = "https://claude.ai/cli"; +pub const CLAUDE_CODE_DOC_URL: &str = "https://claude.ai/cli"; #[derive(Debug, serde::Serialize)] -pub struct SubprocessProvider { +pub struct ClaudeCodeProvider { command: String, model: ModelConfig, } -impl Default for SubprocessProvider { +impl Default for ClaudeCodeProvider { fn default() -> Self { - let model = ModelConfig::new(SubprocessProvider::metadata().default_model); - SubprocessProvider::from_env(model).expect("Failed to initialize Subprocess provider") + let model = ModelConfig::new(ClaudeCodeProvider::metadata().default_model); + ClaudeCodeProvider::from_env(model).expect("Failed to initialize Claude Code provider") } } -impl SubprocessProvider { +impl ClaudeCodeProvider { pub fn from_env(model: ModelConfig) -> Result { let config = crate::config::Config::global(); let command: String = config - .get_param("SUBPROCESS_COMMAND") + .get_param("CLAUDE_CODE_COMMAND") .unwrap_or_else(|_| "claude".to_string()); Ok(Self { command, model }) @@ -235,8 +235,8 @@ impl SubprocessProvider { // Create a filtered system prompt without Extensions section let filtered_system = self.filter_extensions_from_system_prompt(system); - if std::env::var("GOOSE_SUBPROCESS_DEBUG").is_ok() { - println!("=== SUBPROCESS PROVIDER DEBUG ==="); + if std::env::var("GOOSE_CLAUDE_CODE_DEBUG").is_ok() { + println!("=== CLAUDE CODE PROVIDER DEBUG ==="); println!("Command: {}", self.command); println!("Original system prompt length: {} chars", system.len()); println!("Filtered system prompt length: {} chars", filtered_system.len()); @@ -249,13 +249,11 @@ impl SubprocessProvider { cmd.arg("-p") .arg(messages_json.to_string()) .arg("--system-prompt") - .arg(&filtered_system) // Use filtered prompt instead of original + .arg(&filtered_system) .arg("--verbose") .arg("--output-format") .arg("json"); - // Let claude CLI use its own configured model - cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); let mut child = cmd @@ -333,8 +331,8 @@ impl SubprocessProvider { }) .unwrap_or_else(|| "Simple task".to_string()); - if std::env::var("GOOSE_SUBPROCESS_DEBUG").is_ok() { - println!("=== SUBPROCESS PROVIDER DEBUG ==="); + if std::env::var("GOOSE_CLAUDE_CODE_DEBUG").is_ok() { + println!("=== CLAUDE CODE PROVIDER DEBUG ==="); println!("Generated simple session description: {}", description); println!("Skipped subprocess call for session description"); println!("================================"); @@ -359,21 +357,18 @@ impl SubprocessProvider { } #[async_trait] -impl Provider for SubprocessProvider { +impl Provider for ClaudeCodeProvider { fn metadata() -> ProviderMetadata { ProviderMetadata::new( - "subprocess", - "Subprocess", - "Execute AI models via command-line tools (e.g., claude CLI)", - SUBPROCESS_DEFAULT_MODEL, - SUBPROCESS_KNOWN_MODELS.to_vec(), - SUBPROCESS_DOC_URL, - vec![ConfigKey::new( - "SUBPROCESS_COMMAND", - false, - false, - Some("claude"), - )], + "claude-code", + "Claude Code", + "Execute Claude models via claude CLI tool", + CLAUDE_CODE_DEFAULT_MODEL, + CLAUDE_CODE_KNOWN_MODELS.to_vec(), + CLAUDE_CODE_DOC_URL, + vec![ + ConfigKey::new("CLAUDE_CODE_COMMAND", false, false, Some("claude")), + ], ) } diff --git a/crates/goose/src/providers/factory.rs b/crates/goose/src/providers/factory.rs index 619b863920cd..aa364ad3e992 100644 --- a/crates/goose/src/providers/factory.rs +++ b/crates/goose/src/providers/factory.rs @@ -5,8 +5,10 @@ use super::{ azure::AzureProvider, base::{Provider, ProviderMetadata}, bedrock::BedrockProvider, + claude_code::ClaudeCodeProvider, databricks::DatabricksProvider, gcpvertexai::GcpVertexAIProvider, + gemini_cli::GeminiCliProvider, githubcopilot::GithubCopilotProvider, google::GoogleProvider, groq::GroqProvider, @@ -16,7 +18,6 @@ use super::{ openrouter::OpenRouterProvider, sagemaker_tgi::SageMakerTgiProvider, snowflake::SnowflakeProvider, - subprocess::SubprocessProvider, venice::VeniceProvider, xai::XaiProvider, }; @@ -43,8 +44,10 @@ pub fn providers() -> Vec { AnthropicProvider::metadata(), AzureProvider::metadata(), BedrockProvider::metadata(), + ClaudeCodeProvider::metadata(), DatabricksProvider::metadata(), GcpVertexAIProvider::metadata(), + GeminiCliProvider::metadata(), GithubCopilotProvider::metadata(), GoogleProvider::metadata(), GroqProvider::metadata(), @@ -52,7 +55,6 @@ pub fn providers() -> Vec { OpenAiProvider::metadata(), OpenRouterProvider::metadata(), SageMakerTgiProvider::metadata(), - SubprocessProvider::metadata(), VeniceProvider::metadata(), SnowflakeProvider::metadata(), XaiProvider::metadata(), @@ -122,14 +124,15 @@ fn create_provider(name: &str, model: ModelConfig) -> Result> "anthropic" => Ok(Arc::new(AnthropicProvider::from_env(model)?)), "azure_openai" => Ok(Arc::new(AzureProvider::from_env(model)?)), "aws_bedrock" => Ok(Arc::new(BedrockProvider::from_env(model)?)), + "claude-code" => Ok(Arc::new(ClaudeCodeProvider::from_env(model)?)), "databricks" => Ok(Arc::new(DatabricksProvider::from_env(model)?)), + "gemini-cli" => Ok(Arc::new(GeminiCliProvider::from_env(model)?)), "groq" => Ok(Arc::new(GroqProvider::from_env(model)?)), "ollama" => Ok(Arc::new(OllamaProvider::from_env(model)?)), "openrouter" => Ok(Arc::new(OpenRouterProvider::from_env(model)?)), "gcp_vertex_ai" => Ok(Arc::new(GcpVertexAIProvider::from_env(model)?)), "google" => Ok(Arc::new(GoogleProvider::from_env(model)?)), "sagemaker_tgi" => Ok(Arc::new(SageMakerTgiProvider::from_env(model)?)), - "subprocess" => Ok(Arc::new(SubprocessProvider::from_env(model)?)), "venice" => Ok(Arc::new(VeniceProvider::from_env(model)?)), "snowflake" => Ok(Arc::new(SnowflakeProvider::from_env(model)?)), "github_copilot" => Ok(Arc::new(GithubCopilotProvider::from_env(model)?)), diff --git a/crates/goose/src/providers/gemini_cli.rs b/crates/goose/src/providers/gemini_cli.rs new file mode 100644 index 000000000000..41991f937d4e --- /dev/null +++ b/crates/goose/src/providers/gemini_cli.rs @@ -0,0 +1,292 @@ +use anyhow::Result; +use async_trait::async_trait; +use serde_json::json; +use std::process::Stdio; +use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::process::Command; + +use super::base::{Provider, ProviderMetadata, ProviderUsage, Usage}; +use super::errors::ProviderError; +use super::utils::emit_debug_trace; +use crate::message::{Message, MessageContent}; +use crate::model::ModelConfig; +use mcp_core::content::TextContent; +use mcp_core::tool::Tool; +use mcp_core::Role; + +pub const GEMINI_CLI_DEFAULT_MODEL: &str = "default"; +pub const GEMINI_CLI_KNOWN_MODELS: &[&str] = &["default"]; + +pub const GEMINI_CLI_DOC_URL: &str = "https://ai.google.dev/gemini-api/docs"; + +#[derive(Debug, serde::Serialize)] +pub struct GeminiCliProvider { + command: String, + model: ModelConfig, +} + +impl Default for GeminiCliProvider { + fn default() -> Self { + let model = ModelConfig::new(GeminiCliProvider::metadata().default_model); + GeminiCliProvider::from_env(model).expect("Failed to initialize Gemini CLI provider") + } +} + +impl GeminiCliProvider { + pub fn from_env(model: ModelConfig) -> Result { + let command = "gemini".to_string(); // Fixed command, no configuration needed + + Ok(Self { command, model }) + } + + /// Filter out the Extensions section from the system prompt + fn filter_extensions_from_system_prompt(&self, system: &str) -> String { + // Find the Extensions section and remove it + if let Some(extensions_start) = system.find("# Extensions") { + // Look for the next major section that starts with # + let after_extensions = &system[extensions_start..]; + if let Some(next_section_pos) = after_extensions[1..].find("\n# ") { + // Found next section, keep everything before Extensions and after the next section + let before_extensions = &system[..extensions_start]; + let next_section_start = extensions_start + next_section_pos + 1; + let after_next_section = &system[next_section_start..]; + format!("{}{}", before_extensions.trim_end(), after_next_section) + } else { + // No next section found, just remove everything from Extensions onward + system[..extensions_start].trim_end().to_string() + } + } else { + // No Extensions section found, return original + system.to_string() + } + } + + /// Execute gemini CLI command with simple text prompt + async fn execute_command( + &self, + system: &str, + messages: &[Message], + _tools: &[Tool], + ) -> Result, ProviderError> { + // Create a simple prompt combining system + conversation + let mut full_prompt = String::new(); + + // Add system prompt + let filtered_system = self.filter_extensions_from_system_prompt(system); + full_prompt.push_str(&filtered_system); + full_prompt.push_str("\n\n"); + + // Add conversation history + for message in messages { + let role_prefix = match message.role { + Role::User => "Human: ", + Role::Assistant => "Assistant: ", + }; + full_prompt.push_str(role_prefix); + + for content in &message.content { + if let MessageContent::Text(text_content) = content { + full_prompt.push_str(&text_content.text); + full_prompt.push('\n'); + } + } + full_prompt.push('\n'); + } + + full_prompt.push_str("Assistant: "); + + if std::env::var("GOOSE_GEMINI_CLI_DEBUG").is_ok() { + println!("=== GEMINI CLI PROVIDER DEBUG ==="); + println!("Command: {}", self.command); + println!("Full prompt: {}", full_prompt); + println!("================================"); + } + + let mut cmd = Command::new(&self.command); + cmd.arg("-p") + .arg(&full_prompt) + .arg("--yolo"); + + cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); + + let mut child = cmd + .spawn() + .map_err(|e| ProviderError::RequestFailed(format!("Failed to spawn command: {}", e)))?; + + let stdout = child + .stdout + .take() + .ok_or_else(|| ProviderError::RequestFailed("Failed to capture stdout".to_string()))?; + + let mut reader = BufReader::new(stdout); + let mut lines = Vec::new(); + let mut line = String::new(); + + loop { + line.clear(); + match reader.read_line(&mut line).await { + Ok(0) => break, // EOF + Ok(_) => { + let trimmed = line.trim(); + if !trimmed.is_empty() { + lines.push(trimmed.to_string()); + } + } + Err(e) => { + return Err(ProviderError::RequestFailed(format!( + "Failed to read output: {}", + e + ))); + } + } + } + + let exit_status = child.wait().await.map_err(|e| { + ProviderError::RequestFailed(format!("Failed to wait for command: {}", e)) + })?; + + if !exit_status.success() { + return Err(ProviderError::RequestFailed(format!( + "Command failed with exit code: {:?}", + exit_status.code() + ))); + } + + tracing::debug!("Gemini CLI executed successfully, got {} lines", lines.len()); + + Ok(lines) + } + + /// Parse simple text response + fn parse_response( + &self, + lines: &[String], + ) -> Result<(Message, Usage), ProviderError> { + // Join all lines into a single response + let response_text = lines.join("\n"); + + if response_text.trim().is_empty() { + return Err(ProviderError::RequestFailed("Empty response from gemini command".to_string())); + } + + let message = Message { + role: Role::Assistant, + created: chrono::Utc::now().timestamp(), + content: vec![MessageContent::Text(TextContent { + text: response_text, + annotations: None, + })], + }; + + let usage = Usage::default(); // No usage info available for gemini CLI + + Ok((message, usage)) + } + + /// Generate a simple session description without calling subprocess + fn generate_simple_session_description( + &self, + messages: &[Message], + ) -> Result<(Message, ProviderUsage), ProviderError> { + // Extract the first user message text + let description = messages + .iter() + .find(|m| m.role == Role::User) + .and_then(|m| { + m.content.iter().find_map(|c| match c { + MessageContent::Text(text_content) => Some(&text_content.text), + _ => None, + }) + }) + .map(|text| { + // Take first few words, limit to 4 words + text.split_whitespace() + .take(4) + .collect::>() + .join(" ") + }) + .unwrap_or_else(|| "Simple task".to_string()); + + if std::env::var("GOOSE_GEMINI_CLI_DEBUG").is_ok() { + println!("=== GEMINI CLI PROVIDER DEBUG ==="); + println!("Generated simple session description: {}", description); + println!("Skipped subprocess call for session description"); + println!("================================"); + } + + let message = Message { + role: Role::Assistant, + created: chrono::Utc::now().timestamp(), + content: vec![MessageContent::Text(TextContent { + text: description.clone(), + annotations: None, + })], + }; + + let usage = Usage::default(); + + Ok(( + message, + ProviderUsage::new(self.model.model_name.clone(), usage), + )) + } +} + +#[async_trait] +impl Provider for GeminiCliProvider { + fn metadata() -> ProviderMetadata { + ProviderMetadata::new( + "gemini-cli", + "Gemini CLI", + "Execute Gemini models via gemini CLI tool", + GEMINI_CLI_DEFAULT_MODEL, + GEMINI_CLI_KNOWN_MODELS.to_vec(), + GEMINI_CLI_DOC_URL, + vec![], // No configuration needed + ) + } + + fn get_model_config(&self) -> ModelConfig { + self.model.clone() + } + + #[tracing::instrument( + skip(self, system, messages, tools), + fields(model_config, input, output, input_tokens, output_tokens, total_tokens) + )] + async fn complete( + &self, + system: &str, + messages: &[Message], + tools: &[Tool], + ) -> Result<(Message, ProviderUsage), ProviderError> { + // Check if this is a session description request (short system prompt asking for 4 words or less) + if system.contains("four words or less") || system.contains("4 words or less") { + return self.generate_simple_session_description(messages); + } + + let lines = self.execute_command(system, messages, tools).await?; + + let (message, usage) = self.parse_response(&lines)?; + + // Create a dummy payload for debug tracing + let payload = json!({ + "command": self.command, + "model": self.model.model_name, + "system": system, + "messages": messages.len() + }); + + let response = json!({ + "lines": lines.len(), + "usage": usage + }); + + emit_debug_trace(&self.model, &payload, &response, &usage); + + Ok(( + message, + ProviderUsage::new(self.model.model_name.clone(), usage), + )) + } +} diff --git a/crates/goose/src/providers/mod.rs b/crates/goose/src/providers/mod.rs index e61f6d6eb850..fca5257b106c 100644 --- a/crates/goose/src/providers/mod.rs +++ b/crates/goose/src/providers/mod.rs @@ -20,7 +20,8 @@ pub mod openai; pub mod openrouter; pub mod sagemaker_tgi; pub mod snowflake; -pub mod subprocess; +pub mod claude_code; +pub mod gemini_cli; pub mod toolshim; pub mod utils; pub mod utils_universal_openai_stream; From 2dd535af729ecb4be7f2081d500f1289c1225f84 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Thu, 26 Jun 2025 14:51:00 +1000 Subject: [PATCH 6/8] fmt --- crates/goose/src/providers/claude_code.rs | 40 ++++++++++++++++------- crates/goose/src/providers/gemini_cli.rs | 28 ++++++++-------- crates/goose/src/providers/mod.rs | 4 +-- 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/crates/goose/src/providers/claude_code.rs b/crates/goose/src/providers/claude_code.rs index fa160525949e..911d0f049c23 100644 --- a/crates/goose/src/providers/claude_code.rs +++ b/crates/goose/src/providers/claude_code.rs @@ -134,8 +134,9 @@ impl ClaudeCodeProvider { // Join all lines and parse as a single JSON array let full_response = json_lines.join(""); - let json_array: Vec = serde_json::from_str(&full_response) - .map_err(|e| ProviderError::RequestFailed(format!("Failed to parse JSON response: {}", e)))?; + let json_array: Vec = serde_json::from_str(&full_response).map_err(|e| { + ProviderError::RequestFailed(format!("Failed to parse JSON response: {}", e)) + })?; for parsed in json_array { if let Some(msg_type) = parsed.get("type").and_then(|t| t.as_str()) { @@ -143,11 +144,16 @@ impl ClaudeCodeProvider { "assistant" => { if let Some(message) = parsed.get("message") { // Extract text content from this assistant message - if let Some(content) = message.get("content").and_then(|c| c.as_array()) { + if let Some(content) = message.get("content").and_then(|c| c.as_array()) + { for item in content { - if let Some(content_type) = item.get("type").and_then(|t| t.as_str()) { + if let Some(content_type) = + item.get("type").and_then(|t| t.as_str()) + { if content_type == "text" { - if let Some(text) = item.get("text").and_then(|t| t.as_str()) { + if let Some(text) = + item.get("text").and_then(|t| t.as_str()) + { all_text_content.push(text.to_string()); } } @@ -203,7 +209,9 @@ impl ClaudeCodeProvider { // Combine all text content into a single message let combined_text = all_text_content.join("\n\n"); if combined_text.is_empty() { - return Err(ProviderError::RequestFailed("No text content found in response".to_string())); + return Err(ProviderError::RequestFailed( + "No text content found in response".to_string(), + )); } let message_content = vec![MessageContent::Text(TextContent { @@ -239,9 +247,16 @@ impl ClaudeCodeProvider { println!("=== CLAUDE CODE PROVIDER DEBUG ==="); println!("Command: {}", self.command); println!("Original system prompt length: {} chars", system.len()); - println!("Filtered system prompt length: {} chars", filtered_system.len()); + println!( + "Filtered system prompt length: {} chars", + filtered_system.len() + ); println!("Filtered system prompt: {}", filtered_system); - println!("Messages JSON: {}", serde_json::to_string_pretty(&messages_json).unwrap_or_else(|_| "Failed to serialize".to_string())); + println!( + "Messages JSON: {}", + serde_json::to_string_pretty(&messages_json) + .unwrap_or_else(|_| "Failed to serialize".to_string()) + ); println!("================================"); } @@ -366,9 +381,12 @@ impl Provider for ClaudeCodeProvider { CLAUDE_CODE_DEFAULT_MODEL, CLAUDE_CODE_KNOWN_MODELS.to_vec(), CLAUDE_CODE_DOC_URL, - vec![ - ConfigKey::new("CLAUDE_CODE_COMMAND", false, false, Some("claude")), - ], + vec![ConfigKey::new( + "CLAUDE_CODE_COMMAND", + false, + false, + Some("claude"), + )], ) } diff --git a/crates/goose/src/providers/gemini_cli.rs b/crates/goose/src/providers/gemini_cli.rs index 41991f937d4e..7a6e9e994c23 100644 --- a/crates/goose/src/providers/gemini_cli.rs +++ b/crates/goose/src/providers/gemini_cli.rs @@ -70,12 +70,12 @@ impl GeminiCliProvider { ) -> Result, ProviderError> { // Create a simple prompt combining system + conversation let mut full_prompt = String::new(); - + // Add system prompt let filtered_system = self.filter_extensions_from_system_prompt(system); full_prompt.push_str(&filtered_system); full_prompt.push_str("\n\n"); - + // Add conversation history for message in messages { let role_prefix = match message.role { @@ -83,7 +83,7 @@ impl GeminiCliProvider { Role::Assistant => "Assistant: ", }; full_prompt.push_str(role_prefix); - + for content in &message.content { if let MessageContent::Text(text_content) = content { full_prompt.push_str(&text_content.text); @@ -92,7 +92,7 @@ impl GeminiCliProvider { } full_prompt.push('\n'); } - + full_prompt.push_str("Assistant: "); if std::env::var("GOOSE_GEMINI_CLI_DEBUG").is_ok() { @@ -103,9 +103,7 @@ impl GeminiCliProvider { } let mut cmd = Command::new(&self.command); - cmd.arg("-p") - .arg(&full_prompt) - .arg("--yolo"); + cmd.arg("-p").arg(&full_prompt).arg("--yolo"); cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); @@ -152,21 +150,23 @@ impl GeminiCliProvider { ))); } - tracing::debug!("Gemini CLI executed successfully, got {} lines", lines.len()); + tracing::debug!( + "Gemini CLI executed successfully, got {} lines", + lines.len() + ); Ok(lines) } /// Parse simple text response - fn parse_response( - &self, - lines: &[String], - ) -> Result<(Message, Usage), ProviderError> { + fn parse_response(&self, lines: &[String]) -> Result<(Message, Usage), ProviderError> { // Join all lines into a single response let response_text = lines.join("\n"); - + if response_text.trim().is_empty() { - return Err(ProviderError::RequestFailed("Empty response from gemini command".to_string())); + return Err(ProviderError::RequestFailed( + "Empty response from gemini command".to_string(), + )); } let message = Message { diff --git a/crates/goose/src/providers/mod.rs b/crates/goose/src/providers/mod.rs index fca5257b106c..0f1e13fbc074 100644 --- a/crates/goose/src/providers/mod.rs +++ b/crates/goose/src/providers/mod.rs @@ -3,6 +3,7 @@ pub mod azure; pub mod azureauth; pub mod base; pub mod bedrock; +pub mod claude_code; pub mod databricks; pub mod embedding; pub mod errors; @@ -10,6 +11,7 @@ mod factory; pub mod formats; mod gcpauth; pub mod gcpvertexai; +pub mod gemini_cli; pub mod githubcopilot; pub mod google; pub mod groq; @@ -20,8 +22,6 @@ pub mod openai; pub mod openrouter; pub mod sagemaker_tgi; pub mod snowflake; -pub mod claude_code; -pub mod gemini_cli; pub mod toolshim; pub mod utils; pub mod utils_universal_openai_stream; From 76c584db945c98d789fa749f6909fc63a1e3cb43 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Fri, 27 Jun 2025 09:38:33 +1000 Subject: [PATCH 7/8] using correct context sizes --- crates/goose/src/providers/claude_code.rs | 17 ++++++++++++++++- crates/goose/src/providers/gemini_cli.rs | 17 ++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/crates/goose/src/providers/claude_code.rs b/crates/goose/src/providers/claude_code.rs index 911d0f049c23..823afb6a71a3 100644 --- a/crates/goose/src/providers/claude_code.rs +++ b/crates/goose/src/providers/claude_code.rs @@ -391,7 +391,8 @@ impl Provider for ClaudeCodeProvider { } fn get_model_config(&self) -> ModelConfig { - self.model.clone() + // Return a custom config with 200K token limit for Claude Code + ModelConfig::new("claude-3-5-sonnet-latest".to_string()).with_context_limit(Some(200_000)) } #[tracing::instrument( @@ -434,3 +435,17 @@ impl Provider for ClaudeCodeProvider { )) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_claude_code_model_config() { + let provider = ClaudeCodeProvider::default(); + let config = provider.get_model_config(); + + assert_eq!(config.model_name, "claude-3-5-sonnet-latest"); + assert_eq!(config.context_limit(), 200_000); + } +} diff --git a/crates/goose/src/providers/gemini_cli.rs b/crates/goose/src/providers/gemini_cli.rs index 7a6e9e994c23..6308f9024056 100644 --- a/crates/goose/src/providers/gemini_cli.rs +++ b/crates/goose/src/providers/gemini_cli.rs @@ -247,7 +247,8 @@ impl Provider for GeminiCliProvider { } fn get_model_config(&self) -> ModelConfig { - self.model.clone() + // Return a custom config with 1M token limit for Gemini CLI + ModelConfig::new("gemini-1.5-pro".to_string()).with_context_limit(Some(1_000_000)) } #[tracing::instrument( @@ -290,3 +291,17 @@ impl Provider for GeminiCliProvider { )) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_gemini_cli_model_config() { + let provider = GeminiCliProvider::default(); + let config = provider.get_model_config(); + + assert_eq!(config.model_name, "gemini-1.5-pro"); + assert_eq!(config.context_limit(), 1_000_000); + } +} From 43deb521677956a90c78f7966870f1a9908958e2 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Fri, 27 Jun 2025 10:35:10 +1000 Subject: [PATCH 8/8] documenting CLI providers --- .../docs/getting-started/providers.md | 13 ++ documentation/docs/guides/cli-providers.md | 193 ++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 documentation/docs/guides/cli-providers.md diff --git a/documentation/docs/getting-started/providers.md b/documentation/docs/getting-started/providers.md index b75fdf5a554d..7a0adf52bb43 100644 --- a/documentation/docs/getting-started/providers.md +++ b/documentation/docs/getting-started/providers.md @@ -36,6 +36,19 @@ Goose relies heavily on tool calling capabilities and currently works best with | [Venice AI](https://venice.ai/home) | Provides access to open source models like Llama, Mistral, and Qwen while prioritizing user privacy. **Requires an account and an [API key](https://docs.venice.ai/overview/guides/generating-api-key)**. | `VENICE_API_KEY`, `VENICE_HOST` (optional), `VENICE_BASE_PATH` (optional), `VENICE_MODELS_PATH` (optional) | | [xAI](https://x.ai/) | Access to xAI's Grok models including grok-3, grok-3-mini, and grok-3-fast with 131,072 token context window. | `XAI_API_KEY`, `XAI_HOST` (optional) | +## CLI Providers + +Goose also supports special "pass-through" providers that work with existing CLI tools, allowing you to use your subscriptions instead of paying per token: + +| Provider | Description | Requirements | +|-----------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [Claude Code](https://claude.ai/cli) (`claude-code`) | Uses Anthropic's Claude CLI tool with your Claude Code subscription. Provides access to Claude with 200K context limit. | Claude CLI installed and authenticated, active Claude Code subscription | +| [Gemini CLI](https://ai.google.dev/gemini-api/docs) (`gemini-cli`) | Uses Google's Gemini CLI tool with your Google AI subscription. Provides access to Gemini with 1M context limit. | Gemini CLI installed and authenticated | + +:::tip CLI Providers +CLI providers are cost-effective alternatives that use your existing subscriptions. They work differently from API providers as they execute CLI commands and integrate with the tools' native capabilities. See the [CLI Providers guide](/docs/guides/cli-providers) for detailed setup instructions. +::: + ## Configure Provider diff --git a/documentation/docs/guides/cli-providers.md b/documentation/docs/guides/cli-providers.md new file mode 100644 index 000000000000..d6e6b57cd250 --- /dev/null +++ b/documentation/docs/guides/cli-providers.md @@ -0,0 +1,193 @@ +--- +sidebar_position: 25 +title: CLI Providers +sidebar_label: CLI Providers +--- + +# CLI Providers + +Goose supports two special "pass-through" providers that leverage existing CLI tools from Anthropic and Google. These providers allow you to use your existing subscriptions to Claude Code and Google Gemini CLI tools directly through Goose, providing a cost-effective way to access these excellent AI models and tools while maintaining the benefits of Goose's ecosystem, and working with multiple models and extensions. + +## Overview + +CLI providers are different from traditional LLM providers in several key ways: + +- **Pass-through architecture**: Instead of making direct API calls, these providers drive the CLI commands +- **Subscription-based**: Use your existing Claude Code or Google Gemini CLI subscriptions instead of paying per token (usually a flat monthly fee) +- **Tool integration**: These providers work with their respective CLI tools' built-in capabilities while Goose manages sessions and extensions +- **Cost-effective**: Leverage unlimited or subscription-based pricing models instead of token-based billing + +## Available CLI Providers + +### Claude Code (`claude-code`) + +The Claude Code provider integrates with Anthropic's [Claude CLI tool](https://claude.ai/cli), allowing you to use Claude models through your existing Claude Code subscription. + +**Features:** +- Uses Claude's latest models +- 200,000 token context limit +- Automatic filtering of Goose extensions from system prompts (since Claude Code has its own tool ecosystem) +- JSON output parsing for structured responses + +**Requirements:** +- Claude CLI tool installed and configured +- Active Claude Code subscription +- CLI tool authenticated with your Anthropic account + +### Gemini CLI (`gemini-cli`) + +The Gemini CLI provider integrates with Google's [Gemini CLI tool](https://ai.google.dev/gemini-api/docs), providing access to Gemini models through your Google AI subscription. + +**Features:** +- 1,000,000 token context limit + +**Requirements:** +- Gemini CLI tool installed and configured +- CLI tool authenticated with your Google account + +## Setup Instructions + +### Setting up Claude Code Provider + +1. **Install Claude CLI Tool** + + Follow the installation instructions [here](https://docs.anthropic.com/en/docs/claude-code/overview) to install and configure the Claude CLI tool. + +2. **Authenticate with Claude** + + Ensure your Claude CLI is authenticated and working + +3. **Configure Goose** + + Set the provider environment variable: + ```bash + export GOOSE_PROVIDER=claude-code + ``` + + Or configure through the Goose CLI: + ```bash + goose configure + # Select "Configure Providers" + # Choose "Claude Code" from the list + ``` + +### Setting up Gemini CLI Provider + +1. **Install Gemini CLI Tool** + + Follow the installation instructions for [gemini cli]https://blog.google/technology/developers/introducing-gemini-cli-open-source-ai-agent/) to install and configure the Gemini CLI tool. + +2. **Authenticate with Google** + + Ensure your Gemini CLI is authenticated and working. + +3. **Configure Goose** + + Set the provider environment variable: + ```bash + export GOOSE_PROVIDER=gemini-cli + ``` + + Or configure through the Goose CLI: + ```bash + goose configure + # Select "Configure Providers" + # Choose "Gemini CLI" from the list + ``` + +## Usage Examples + +### Basic Usage + +Once configured, use these providers just like any other Goose provider: + +```bash +# Using Claude Code +GOOSE_PROVIDER=claude-code goose session start + +# Using Gemini CLI +GOOSE_PROVIDER=gemini-cli goose session start +``` + +### Combining with Other Models + +CLI providers work well in combination with other models using Goose's lead/worker pattern: + +```bash +# Use Claude Code as lead model, GPT-4o as worker +export GOOSE_LEAD_PROVIDER=claude-code +export GOOSE_PROVIDER=openai +export GOOSE_MODEL=gpt-4o +export GOOSE_LEAD_MODEL=default + +goose session start +``` + +## Configuration Options + +### Claude Code Configuration + +| Environment Variable | Description | Default | +|---------------------|-------------|---------| +| `GOOSE_PROVIDER` | Set to `claude-code` to use this provider | None | +| `CLAUDE_CODE_COMMAND` | Path to the Claude CLI command | `claude` | + +### Gemini CLI Configuration + +| Environment Variable | Description | Default | +|---------------------|-------------|---------| +| `GOOSE_PROVIDER` | Set to `gemini-cli` to use this provider | None | + +## How It Works + +### System Prompt Filtering + +Both CLI providers automatically filter out Goose's extension information from system prompts since these CLI tools have their own tool ecosystems. This prevents conflicts and ensures clean interaction with the underlying CLI tools. + +### Message Translation + +- **Claude Code**: Converts Goose messages to Claude's JSON message format, handling tool calls and responses appropriately +- **Gemini CLI**: Converts messages to simple text prompts with role prefixes (Human:/Assistant:) + +### Response Processing + +- **Claude Code**: Parses JSON responses to extract text content and usage information +- **Gemini CLI**: Processes plain text responses from the CLI tool + +## Limitations and Considerations + +### Tool Calling + +These providers handle tool calling differently than standard API providers as they have their own tool integrations out of the box. + +### Error Handling + +CLI providers depend on external tools, so ensure: + +- CLI tools are properly installed and in your PATH +- Authentication is maintained and valid +- Subscription limits are not exceeded + + +## Benefits + +### Cost Effectiveness + +- **Subscription-based pricing**: Use unlimited or fixed-price subscriptions instead of per-token billing +- **Existing subscriptions**: Leverage subscriptions you may already have + +### Flexibility + +- **Hybrid usage**: Combine CLI providers with API-based providers using lead/worker patterns +- **Session management**: Full Goose session management and extension system +- **Easy switching**: Switch between CLI and API providers as needed + +## Best Practices + +1. **Authentication Management**: Keep CLI tools authenticated and monitor subscription status +2. **Hybrid Approaches**: Consider using CLI providers for heavy workloads and API providers for quick tasks +3. **Backup Providers**: Configure fallback API providers in case CLI tools are unavailable + +--- + +CLI providers offer a powerful way to integrate Goose with existing AI tool subscriptions while maintaining the benefits of Goose's session management and extension ecosystem. They're particularly valuable for users with existing subscriptions who want to maximize their investment while gaining access to Goose's capabilities.