From 86a34d8943890c0480073d45b8dbdf322417fcda Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Thu, 18 Jun 2026 13:06:29 -0400 Subject: [PATCH 1/6] logger refactor --- crates/goose-providers/src/base.rs | 5 + crates/goose-providers/src/lib.rs | 1 + crates/goose-providers/src/request_log.rs | 134 ++++++++++++++++++ crates/goose/src/providers/anthropic.rs | 9 +- crates/goose/src/providers/base.rs | 6 - crates/goose/src/providers/bedrock.rs | 12 +- crates/goose/src/providers/codex.rs | 8 +- crates/goose/src/providers/cursor_agent.rs | 18 ++- crates/goose/src/providers/databricks.rs | 8 +- crates/goose/src/providers/databricks_v2.rs | 14 +- crates/goose/src/providers/gcpvertexai.rs | 8 +- crates/goose/src/providers/gemini_cli.rs | 6 +- crates/goose/src/providers/gemini_oauth.rs | 8 +- crates/goose/src/providers/githubcopilot.rs | 15 +- crates/goose/src/providers/google.rs | 8 +- crates/goose/src/providers/kimicode.rs | 7 +- crates/goose/src/providers/litellm.rs | 9 +- crates/goose/src/providers/local_inference.rs | 10 +- .../src/providers/local_inference/backend.rs | 4 +- .../llamacpp/inference_engine.rs | 4 +- crates/goose/src/providers/nanogpt.rs | 5 +- crates/goose/src/providers/ollama.rs | 13 +- crates/goose/src/providers/openai.rs | 20 ++- .../goose/src/providers/openai_compatible.rs | 18 ++- crates/goose/src/providers/openrouter.rs | 5 +- crates/goose/src/providers/sagemaker_tgi.rs | 8 +- crates/goose/src/providers/snowflake.rs | 9 +- crates/goose/src/providers/tetrate.rs | 5 +- crates/goose/src/providers/toolshim.rs | 1 + crates/goose/src/providers/utils.rs | 97 +++++-------- 30 files changed, 320 insertions(+), 155 deletions(-) create mode 100644 crates/goose-providers/src/request_log.rs diff --git a/crates/goose-providers/src/base.rs b/crates/goose-providers/src/base.rs index 644cdb9341ed..5dfe73bd3d9b 100644 --- a/crates/goose-providers/src/base.rs +++ b/crates/goose-providers/src/base.rs @@ -168,6 +168,11 @@ pub async fn collect_stream( } } +pub fn stream_from_single_message(message: Message, usage: ProviderUsage) -> MessageStream { + let stream = futures::stream::once(async move { Ok((Some(message), Some(usage))) }); + Box::pin(stream) +} + /// Base trait for AI providers (OpenAI, Anthropic, etc) #[async_trait] pub trait Provider: Send + Sync { diff --git a/crates/goose-providers/src/lib.rs b/crates/goose-providers/src/lib.rs index 60d552f8bc76..3ab49ef87e46 100644 --- a/crates/goose-providers/src/lib.rs +++ b/crates/goose-providers/src/lib.rs @@ -9,6 +9,7 @@ pub mod json; pub(crate) mod mcp_utils; pub mod model; pub mod permission; +pub mod request_log; pub mod retry; pub mod thinking; pub mod utils; diff --git a/crates/goose-providers/src/request_log.rs b/crates/goose-providers/src/request_log.rs new file mode 100644 index 000000000000..f3b9c53815de --- /dev/null +++ b/crates/goose-providers/src/request_log.rs @@ -0,0 +1,134 @@ +use std::{ + error::Error, + fmt::Display, + sync::{Arc, OnceLock}, +}; + +use serde::Serialize; +use serde_json::json; + +use crate::conversation::token_usage::Usage; + +type RequestLogError = Box; + +static LOGGER: OnceLock> = OnceLock::new(); + +#[derive(Debug)] +pub struct LoggerAlreadyInstalled; + +impl Display for LoggerAlreadyInstalled { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "request logger is already installed") + } +} + +impl Error for LoggerAlreadyInstalled {} + +pub fn install_logger(r: R) -> Result<(), LoggerAlreadyInstalled> { + LOGGER.set(Arc::new(r)).map_err(|_| LoggerAlreadyInstalled) +} + +pub trait RequestLogger: Send + Sync { + fn start(&self) -> Result, RequestLogError>; +} + +pub trait RequestLogHandle: Send { + fn write(&mut self, s: &str) -> Result<(), RequestLogError>; +} + +#[derive(Debug)] +pub enum LogError { + LoggerError(String), + SerializeError(serde_json::Error), +} + +impl Display for LogError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + LogError::LoggerError(msg) => write!(f, "{}", msg), + LogError::SerializeError(error) => write!(f, "serialize error: {}", error), + } + } +} + +impl Error for LogError {} + +impl From for LogError { + fn from(value: RequestLogError) -> Self { + Self::LoggerError(value.to_string()) + } +} + +fn serialize(v: &serde_json::Value) -> Result { + serde_json::to_string(v).map_err(LogError::SerializeError) +} + +pub fn start_log( + model_config: M, + payload: P, +) -> Result>, LogError> +where + M: Serialize, + P: Serialize, +{ + let logger = if let Some(logger) = LOGGER.get() { + logger + } else { + return Ok(None); + }; + + let mut handle = logger.start()?; + let payload = json!({ + "model_config": model_config, + "input": payload, + }); + + handle.write(serialize(&payload)?.as_str())?; + Ok(Some(handle)) +} + +pub trait LoggerHandleExt { + fn write(&mut self, data: &Payload, usage: Option<&Usage>) -> Result<(), LogError> + where + Payload: Serialize; + fn error(&mut self, error: E) -> Result<(), LogError> + where + E: Display; +} + +impl LoggerHandleExt for Option> { + fn write(&mut self, data: &Payload, usage: Option<&Usage>) -> Result<(), LogError> + where + Payload: Serialize, + { + let log = if let Some(log) = self { + log + } else { + return Ok(()); + }; + + let line = serialize(&json!({ + "data": data, + "usage": usage, + }))?; + + Ok(log.write(line.as_str())?) + } + + fn error(&mut self, error: E) -> Result<(), LogError> + where + E: Display, + { + let log = if let Some(log) = self { + log + } else { + return Ok(()); + }; + + let line = serialize(&json!({ + "error": format!("{}", error), + }))?; + + Ok(log.write(line.as_str())?) + } +} diff --git a/crates/goose/src/providers/anthropic.rs b/crates/goose/src/providers/anthropic.rs index d5fc4627f59c..5e5bb297aa7f 100644 --- a/crates/goose/src/providers/anthropic.rs +++ b/crates/goose/src/providers/anthropic.rs @@ -3,6 +3,7 @@ use async_stream::try_stream; use async_trait::async_trait; use futures::TryStreamExt; use goose_providers::errors::ProviderError; +use goose_providers::request_log::{start_log, LoggerHandleExt}; use reqwest::StatusCode; use serde_json::Value; use std::io; @@ -20,7 +21,6 @@ use super::openai_compatible::map_http_error_to_provider_error; use super::retry::ProviderRetry; use crate::config::declarative_providers::DeclarativeProviderConfig; use crate::conversation::message::Message; -use crate::providers::utils::RequestLog; use futures::future::BoxFuture; use goose_providers::model::ModelConfig; use rmcp::model::Tool; @@ -330,7 +330,8 @@ impl Provider for AnthropicProvider { .insert("stream".to_string(), Value::Bool(true)); let conditional_headers = self.get_conditional_headers(); - let mut log = RequestLog::start(model_config, &payload)?; + let mut log = start_log(model_config, &payload) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; let response = self .with_retry(|| async { @@ -356,12 +357,14 @@ impl Provider for AnthropicProvider { pin!(message_stream); while let Some(message) = futures::StreamExt::next(&mut message_stream).await { let (message, usage) = message.map_err(ProviderError::from_stream_error)?; - log.write(&message, usage.as_ref().map(|f| f.usage).as_ref())?; + log.write(&message, usage.as_ref().map(|f| f.usage).as_ref()) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; yield (message, usage); } })) } } + #[cfg(test)] mod tests { use super::*; diff --git a/crates/goose/src/providers/base.rs b/crates/goose/src/providers/base.rs index f7ccf3018cb2..61af875a3230 100644 --- a/crates/goose/src/providers/base.rs +++ b/crates/goose/src/providers/base.rs @@ -12,7 +12,6 @@ pub const DEFAULT_PROVIDER_TIMEOUT_SECS: u64 = 600; use crate::config::base::ConfigValue; use crate::config::ExtensionConfig; -use goose_providers::conversation::message::Message; use goose_providers::model::ModelConfig; use utoipa::ToSchema; @@ -264,11 +263,6 @@ pub trait ProviderDef: Send + Sync { } } -pub fn stream_from_single_message(message: Message, usage: ProviderUsage) -> MessageStream { - let stream = futures::stream::once(async move { Ok((Some(message), Some(usage))) }); - Box::pin(stream) -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/goose/src/providers/bedrock.rs b/crates/goose/src/providers/bedrock.rs index b8bcac5491a2..6436be36cf17 100644 --- a/crates/goose/src/providers/bedrock.rs +++ b/crates/goose/src/providers/bedrock.rs @@ -5,7 +5,6 @@ use super::formats::openai_responses::create_responses_request; use super::openai_compatible::{handle_status, stream_responses_compat}; use super::retry::{ProviderRetry, RetryConfig}; use crate::conversation::message::Message; -use crate::providers::utils::RequestLog; use crate::session_context::SESSION_ID_HEADER; use anyhow::Result; use async_stream::try_stream; @@ -21,6 +20,7 @@ use goose_providers::conversation::token_usage::{ProviderUsage, Usage}; use goose_providers::errors::ProviderError; use goose_providers::formats::openai::extract_reasoning_effort; use goose_providers::model::ModelConfig; +use goose_providers::request_log::{start_log, LoggerHandleExt}; use reqwest::header::{HeaderName, HeaderValue, AUTHORIZATION}; use rmcp::model::{object, CallToolRequestParams, ErrorCode, ErrorData, Tool}; use serde_json::Value; @@ -526,11 +526,13 @@ impl BedrockProvider { "messages": messages, "tools": tools }); - let mut log = RequestLog::start(&self.model, &debug_payload)?; + let mut log = start_log(&self.model, &debug_payload) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; log.write( &serde_json::to_value(&message).unwrap_or_default(), Some(&usage), - )?; + ) + .map_err(anyhow::Error::from)?; let provider_usage = ProviderUsage::new(model_name.to_string(), usage); Ok(super::base::stream_from_single_message( @@ -775,7 +777,7 @@ impl Provider for BedrockProvider { create_responses_request(&normalized_config, system, messages, tools)?; payload["model"] = Value::String(bedrock_model_id.clone()); payload["stream"] = Value::Bool(true); - let mut log = RequestLog::start(model_config, &payload)?; + let mut log = start_log(model_config, &payload).map_err(anyhow::Error::from)?; let response = self .with_retry(|| self.post_mantle_streaming(session_id_opt, &payload)) @@ -810,7 +812,7 @@ impl Provider for BedrockProvider { "messages": messages, "tools": tools }); - let mut log = RequestLog::start(&self.model, &debug_payload)?; + let mut log = start_log(&self.model, &debug_payload).map_err(anyhow::Error::from)?; let mut event_stream = response.stream; diff --git a/crates/goose/src/providers/codex.rs b/crates/goose/src/providers/codex.rs index 79352a0e4645..a394d7560e03 100644 --- a/crates/goose/src/providers/codex.rs +++ b/crates/goose/src/providers/codex.rs @@ -14,7 +14,7 @@ use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::process::Command; use super::base::{ConfigKey, MessageStream, Provider, ProviderDef, ProviderMetadata}; -use super::utils::{filter_extensions_from_system_prompt, RequestLog}; +use super::utils::filter_extensions_from_system_prompt; use crate::config::base::{CodexCommand, CodexSkipGitCheck}; use crate::config::paths::Paths; use crate::config::search_path::SearchPaths; @@ -23,6 +23,7 @@ use crate::conversation::message::{Message, MessageContent}; use crate::subprocess::configure_subprocess; use goose_providers::errors::ProviderError; use goose_providers::model::ModelConfig; +use goose_providers::request_log::{start_log, LoggerHandleExt}; use rmcp::model::Role; use rmcp::model::Tool; @@ -717,9 +718,8 @@ impl Provider for CodexProvider { "messages_count": messages.len() }); - let mut log = RequestLog::start(model_config, &payload).map_err(|e| { - ProviderError::RequestFailed(format!("Failed to start request log: {}", e)) - })?; + let mut log = start_log(model_config, &payload) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; let response = json!({ "lines": lines.len(), diff --git a/crates/goose/src/providers/cursor_agent.rs b/crates/goose/src/providers/cursor_agent.rs index c4e2b04615ff..62ca9c86114e 100644 --- a/crates/goose/src/providers/cursor_agent.rs +++ b/crates/goose/src/providers/cursor_agent.rs @@ -10,8 +10,7 @@ use tokio::process::Command; use super::base::{ stream_from_single_message, ConfigKey, MessageStream, Provider, ProviderDef, ProviderMetadata, }; -use super::utils::{filter_extensions_from_system_prompt, RequestLog}; -use crate::config::base::CursorAgentCommand; +use super::utils::filter_extensions_from_system_prompt; use crate::config::search_path::SearchPaths; use crate::conversation::message::{Message, MessageContent}; use crate::subprocess::configure_subprocess; @@ -19,6 +18,7 @@ use futures::future::BoxFuture; use goose_providers::conversation::token_usage::{ProviderUsage, Usage}; use goose_providers::errors::ProviderError; use goose_providers::model::ModelConfig; +use goose_providers::request_log::{start_log, LoggerHandleExt}; use rmcp::model::Tool; const CURSOR_AGENT_PROVIDER_NAME: &str = "cursor-agent"; @@ -287,8 +287,12 @@ impl ProviderDef for CursorAgentProvider { CURSOR_AGENT_DEFAULT_MODEL, CURSOR_AGENT_KNOWN_MODELS.to_vec(), CURSOR_AGENT_DOC_URL, - vec![ConfigKey::from_value_type::( - true, false, true, + vec![ConfigKey::new( + "CURSOR_AGENT_COMMAND", + true, + false, + Some("cursor-agent"), + true, )], ) } @@ -352,8 +356,10 @@ impl Provider for CursorAgentProvider { "usage": usage }); - let mut log = RequestLog::start(&self.model, &payload)?; - log.write(&response, Some(&usage))?; + let mut log = start_log(&self.model, &payload) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + log.write(&response, Some(&usage)) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; let provider_usage = ProviderUsage::new(model_config.model_name.clone(), usage); Ok(stream_from_single_message(message, provider_usage)) diff --git a/crates/goose/src/providers/databricks.rs b/crates/goose/src/providers/databricks.rs index 1ff851071f24..7e4b0c3cc39a 100644 --- a/crates/goose/src/providers/databricks.rs +++ b/crates/goose/src/providers/databricks.rs @@ -24,7 +24,6 @@ use super::openai_compatible::{ stream_responses_compat, }; use super::retry::ProviderRetry; -use super::utils::RequestLog; use crate::config::ConfigError; use crate::conversation::message::Message; use crate::instance_id::get_instance_id; @@ -34,6 +33,7 @@ use crate::providers::retry::{ }; use goose_providers::errors::ProviderError; use goose_providers::model::ModelConfig; +use goose_providers::request_log::{start_log, LoggerHandleExt}; use rmcp::model::Tool; use serde_json::json; @@ -653,7 +653,8 @@ impl Provider for DatabricksProvider { payload["client_request_id"] = Value::String(client_request_id.clone()); } - let mut log = RequestLog::start(model_config, &payload)?; + let mut log = start_log(model_config, &payload) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; let response = self .with_retry(|| async { @@ -718,7 +719,8 @@ impl Provider for DatabricksProvider { .insert("stream_options".to_string(), json!({"include_usage": true})); } - let mut log = RequestLog::start(model_config, &payload)?; + let mut log = start_log(model_config, &payload) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; let response = self .with_retry(|| async { let resp = self diff --git a/crates/goose/src/providers/databricks_v2.rs b/crates/goose/src/providers/databricks_v2.rs index 69efc1f736ee..548b5e7fbec7 100644 --- a/crates/goose/src/providers/databricks_v2.rs +++ b/crates/goose/src/providers/databricks_v2.rs @@ -22,7 +22,6 @@ use super::databricks_auth::{DatabricksAuth, DatabricksAuthProvider}; use super::formats::{anthropic, openai_responses}; use super::openai_compatible::{handle_status, stream_openai_compat, stream_responses_compat}; use super::retry::ProviderRetry; -use super::utils::RequestLog; use crate::config::ConfigError; use crate::conversation::message::Message; use crate::providers::retry::{ @@ -31,6 +30,7 @@ use crate::providers::retry::{ }; use goose_providers::errors::ProviderError; use goose_providers::model::ModelConfig; +use goose_providers::request_log::{start_log, LoggerHandleExt}; use rmcp::model::Tool; const DATABRICKS_V2_PROVIDER_NAME: &str = "databricks_v2"; @@ -231,7 +231,8 @@ impl DatabricksV2Provider { let mut payload = openai_responses::create_responses_request(model_config, system, messages, tools)?; payload["stream"] = Value::Bool(true); - let mut log = RequestLog::start(model_config, &payload)?; + let mut log = start_log(model_config, &payload) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; let response = self .with_retry(|| async { @@ -268,7 +269,8 @@ impl DatabricksV2Provider { if payload.get("max_tokens").is_none() { payload["max_tokens"] = Value::from(model_config.max_output_tokens()); } - let mut log = RequestLog::start(model_config, &payload)?; + let mut log = start_log(model_config, &payload) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; let response = self .with_retry(|| async { @@ -300,7 +302,8 @@ impl DatabricksV2Provider { ) -> Result { let mut payload = anthropic::create_request(model_config, system, messages, tools)?; payload["stream"] = Value::Bool(true); - let mut log = RequestLog::start(model_config, &payload)?; + let mut log = start_log(model_config, &payload) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; let response = self .with_retry(|| async { @@ -330,7 +333,8 @@ impl DatabricksV2Provider { pin!(message_stream); while let Some(message) = futures::StreamExt::next(&mut message_stream).await { let (message, usage) = message.map_err(ProviderError::from_stream_error)?; - log.write(&message, usage.as_ref().map(|f| f.usage).as_ref())?; + log.write(&message, usage.as_ref().map(|f| f.usage).as_ref()) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; yield (message, usage); } })) diff --git a/crates/goose/src/providers/gcpvertexai.rs b/crates/goose/src/providers/gcpvertexai.rs index 45263ead8aeb..dbcd6a509769 100644 --- a/crates/goose/src/providers/gcpvertexai.rs +++ b/crates/goose/src/providers/gcpvertexai.rs @@ -28,9 +28,9 @@ use crate::providers::formats::gcpvertexai::{ use crate::providers::gcpauth::GcpAuth; use crate::providers::openai_compatible::{map_http_error_to_provider_error, sanitize_url}; use crate::providers::retry::RetryConfig; -use crate::providers::utils::RequestLog; use crate::session_context::SESSION_ID_HEADER; use goose_providers::errors::ProviderError; +use goose_providers::request_log::{start_log, LoggerHandleExt}; use rmcp::model::Tool; const GCP_VERTEX_AI_PROVIDER_NAME: &str = "gcp_vertex_ai"; @@ -618,7 +618,8 @@ impl Provider for GcpVertexAIProvider { } } - let mut log = RequestLog::start(model_config, &request)?; + let mut log = start_log(model_config, &request) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; let response = self .post_stream(Some(session_id), &request, &context) @@ -642,7 +643,8 @@ impl Provider for GcpVertexAIProvider { while let Some(message) = message_stream.next().await { let (message, usage) = message.map_err(ProviderError::from_stream_error)?; - log.write(&message, usage.as_ref().map(|u| &u.usage))?; + log.write(&message, usage.as_ref().map(|u| &u.usage)) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; yield (message, usage); } })) diff --git a/crates/goose/src/providers/gemini_cli.rs b/crates/goose/src/providers/gemini_cli.rs index 0a2339b774ec..13362dbf4838 100644 --- a/crates/goose/src/providers/gemini_cli.rs +++ b/crates/goose/src/providers/gemini_cli.rs @@ -12,7 +12,6 @@ use super::base::{ }; use super::cli_common::{error_from_event, extract_usage_tokens}; use super::utils::filter_extensions_from_system_prompt; -use crate::config::base::GeminiCliCommand; use crate::config::search_path::SearchPaths; use crate::config::Config; use crate::conversation::message::{Message, MessageContent}; @@ -165,8 +164,9 @@ impl ProviderDef for GeminiCliProvider { GEMINI_CLI_DEFAULT_MODEL, GEMINI_CLI_KNOWN_MODELS.to_vec(), GEMINI_CLI_DOC_URL, - vec![ConfigKey::from_value_type::( - true, false, true, + vec![ConfigKey::new( + "GEMINI_CLI_COMMAND", + true, false, Some("gemini"), true, )], ) } diff --git a/crates/goose/src/providers/gemini_oauth.rs b/crates/goose/src/providers/gemini_oauth.rs index 3733856ae678..3c2e84160c82 100644 --- a/crates/goose/src/providers/gemini_oauth.rs +++ b/crates/goose/src/providers/gemini_oauth.rs @@ -8,11 +8,11 @@ use crate::providers::formats::google::{create_request, response_to_streaming_me use crate::providers::google::GOOGLE_DOC_URL; use goose_providers::errors::ProviderError; use goose_providers::model::ModelConfig; +use goose_providers::request_log::{start_log, LoggerHandleExt}; const GEMINI_OAUTH_DEFAULT_MODEL: &str = "gemini-3-flash-preview"; const GEMINI_OAUTH_DEFAULT_FAST_MODEL: &str = "gemini-2.5-flash-lite"; use crate::providers::retry::ProviderRetry; -use crate::providers::utils::RequestLog; use crate::session_context::SESSION_ID_HEADER; use anyhow::{anyhow, Result}; use async_stream::try_stream; @@ -995,7 +995,8 @@ impl Provider for GeminiOAuthProvider { tools: &[Tool], ) -> Result { let payload = create_request(model_config, system, messages, tools)?; - let mut log = RequestLog::start(model_config, &payload)?; + let mut log = start_log(model_config, &payload) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; let response = self .with_retry(|| async { @@ -1024,7 +1025,8 @@ impl Provider for GeminiOAuthProvider { .unwrap_or_else(ProviderError::stream_decode_error) })?; if message.is_some() || usage.is_some() { - log.write(&message, usage.as_ref().map(|f| f.usage).as_ref())?; + log.write(&message, usage.as_ref().map(|f| f.usage).as_ref()) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; } yield (message, usage); } diff --git a/crates/goose/src/providers/githubcopilot.rs b/crates/goose/src/providers/githubcopilot.rs index 3089a2ae273d..00cefff0b0fe 100644 --- a/crates/goose/src/providers/githubcopilot.rs +++ b/crates/goose/src/providers/githubcopilot.rs @@ -30,7 +30,7 @@ use super::base::{ use super::formats::openai_responses::create_responses_request; use super::openai_compatible::handle_response_openai_compat; use super::retry::ProviderRetry; -use super::utils::{get_model, RequestLog}; +use super::utils::get_model; use goose_providers::formats::openai::{create_request, get_usage, response_to_message}; use crate::config::{Config, ConfigError}; @@ -40,6 +40,7 @@ use crate::providers::base::{ConfigKey, MessageStream}; use futures::future::BoxFuture; use goose_providers::conversation::token_usage::{ProviderUsage, Usage}; use goose_providers::model::ModelConfig; +use goose_providers::request_log::{start_log, LoggerHandleExt}; use rmcp::model::{RawContent, Tool}; use std::ops::Deref; @@ -403,7 +404,8 @@ impl GithubCopilotProvider { .map_err(|e| ProviderError::RequestFailed(e.to_string()))?; payload["stream"] = serde_json::Value::Bool(true); - let mut log = RequestLog::start(model_config, &payload)?; + let mut log = start_log(model_config, &payload) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; let response = self .with_retry(|| async { @@ -451,7 +453,8 @@ impl GithubCopilotProvider { &ImageFormat::OpenAi, true, )?; - let mut log = RequestLog::start(model_config, &payload)?; + let mut log = start_log(model_config, &payload) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; let response = self .with_retry(|| async { @@ -487,7 +490,8 @@ impl GithubCopilotProvider { &ImageFormat::OpenAi, false, )?; - let mut log = RequestLog::start(model_config, &payload)?; + let mut log = start_log(model_config, &payload) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; let response = self .with_retry(|| async { @@ -512,7 +516,8 @@ impl GithubCopilotProvider { Usage::default() }); let response_model = get_model(&response); - log.write(&response, Some(&usage))?; + log.write(&response, Some(&usage)) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; Ok(super::base::stream_from_single_message( message, diff --git a/crates/goose/src/providers/google.rs b/crates/goose/src/providers/google.rs index 41ecaf76327b..338de7f5f6aa 100644 --- a/crates/goose/src/providers/google.rs +++ b/crates/goose/src/providers/google.rs @@ -2,7 +2,6 @@ use super::api_client::{ApiClient, AuthMethod}; use super::base::MessageStream; use super::openai_compatible::{handle_status, map_http_error_to_provider_error, sanitize_url}; use super::retry::ProviderRetry; -use super::utils::RequestLog; use crate::conversation::message::Message; use goose_providers::errors::ProviderError; @@ -14,6 +13,7 @@ use async_trait::async_trait; use futures::future::BoxFuture; use futures::TryStreamExt; use goose_providers::model::ModelConfig; +use goose_providers::request_log::{start_log, LoggerHandleExt}; use rmcp::model::Tool; use serde_json::Value; use std::io; @@ -192,7 +192,8 @@ impl Provider for GoogleProvider { tools: &[Tool], ) -> Result { let payload = create_request(model_config, system, messages, tools)?; - let mut log = RequestLog::start(model_config, &payload)?; + let mut log = start_log(model_config, &payload) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; let response = self .with_retry(|| async { @@ -219,7 +220,8 @@ impl Provider for GoogleProvider { .unwrap_or_else(ProviderError::stream_decode_error) })?; if message.is_some() || usage.is_some() { - log.write(&message, usage.as_ref().map(|f| f.usage).as_ref())?; + log.write(&message, usage.as_ref().map(|f| f.usage).as_ref()) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; } yield (message, usage); } diff --git a/crates/goose/src/providers/kimicode.rs b/crates/goose/src/providers/kimicode.rs index b45ed523941d..f4281bfa2702 100644 --- a/crates/goose/src/providers/kimicode.rs +++ b/crates/goose/src/providers/kimicode.rs @@ -26,11 +26,11 @@ use super::oauth_device_flow::{ }; use super::openai_compatible::handle_status; use super::retry::ProviderRetry; -use super::utils::RequestLog; use crate::conversation::message::Message; use futures::future::BoxFuture; use goose_providers::errors::ProviderError; use goose_providers::model::ModelConfig; +use goose_providers::request_log::{start_log, LoggerHandleExt}; use rmcp::model::Tool; const KIMI_CODE_PROVIDER_NAME: &str = "kimi_code"; @@ -404,7 +404,7 @@ impl Provider for KimiCodeProvider { .unwrap() .insert("stream".to_string(), Value::Bool(true)); - let mut log = RequestLog::start(model_config, &payload) + let mut log = start_log(model_config, &payload) .map_err(|e| ProviderError::RequestFailed(e.to_string()))?; let response = self @@ -431,7 +431,8 @@ impl Provider for KimiCodeProvider { pin!(message_stream); while let Some(message) = futures::StreamExt::next(&mut message_stream).await { let (message, usage) = message.map_err(ProviderError::from_stream_error)?; - log.write(&message, usage.as_ref().map(|f| f.usage).as_ref())?; + log.write(&message, usage.as_ref().map(|f| f.usage).as_ref()) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; yield (message, usage); } })) diff --git a/crates/goose/src/providers/litellm.rs b/crates/goose/src/providers/litellm.rs index 45ac5bf14740..be8914ae87d6 100644 --- a/crates/goose/src/providers/litellm.rs +++ b/crates/goose/src/providers/litellm.rs @@ -14,9 +14,10 @@ use super::base::{ }; use super::openai_compatible::handle_response_openai_compat; use super::retry::ProviderRetry; -use super::utils::{get_model, RequestLog}; +use super::utils::get_model; use crate::conversation::message::Message; use goose_providers::model::ModelConfig; +use goose_providers::request_log::{start_log, LoggerHandleExt}; use rmcp::model::Tool; const LITELLM_PROVIDER_NAME: &str = "litellm"; @@ -249,8 +250,10 @@ impl Provider for LiteLLMProvider { let message = goose_providers::formats::openai::response_to_message(&response)?; let usage = goose_providers::formats::openai::get_usage(&response); let response_model = get_model(&response); - let mut log = RequestLog::start(model_config, &payload)?; - log.write(&response, Some(&usage))?; + let mut log = start_log(model_config, &payload) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + log.write(&response, Some(&usage)) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; let provider_usage = ProviderUsage::new(response_model, usage); Ok(super::base::stream_from_single_message( message, diff --git a/crates/goose/src/providers/local_inference.rs b/crates/goose/src/providers/local_inference.rs index 8e5f5e444ba7..99d974c3f6bf 100644 --- a/crates/goose/src/providers/local_inference.rs +++ b/crates/goose/src/providers/local_inference.rs @@ -13,7 +13,6 @@ mod tool_parsing; use crate::config::ExtensionConfig; use crate::conversation::message::{Message, MessageContent}; use crate::providers::base::{MessageStream, Provider, ProviderDef, ProviderMetadata}; -use crate::providers::utils::RequestLog; use anyhow::Result; use async_stream::try_stream; use async_trait::async_trait; @@ -23,6 +22,7 @@ use goose_providers::conversation::token_usage::{ProviderUsage, Usage}; use goose_providers::errors::ProviderError; use goose_providers::images::ImageFormat; use goose_providers::model::ModelConfig; +use goose_providers::request_log::{start_log, LoggerHandleExt, RequestLogHandle}; use llamacpp::{LlamaCppBackend, LLAMACPP_BACKEND_ID}; use local_model_registry::ChatTemplate; use mlx::{MlxBackend, MLX_BACKEND_ID}; @@ -447,7 +447,7 @@ fn strip_info_messages(text: &str) -> String { /// Build a `ProviderUsage` and write the request log entry. fn finalize_usage( - log: &mut RequestLog, + log: &mut Option>, model_name: String, path_label: &str, prompt_token_count: usize, @@ -605,7 +605,7 @@ impl Provider for LocalInferenceProvider { backend_for_load.load_model(&model_id, &resolved_for_load, &settings_for_load) }) .await - .map_err(|e| ProviderError::ExecutionError(e.to_string()))??; + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))??; *model_lock = Some(loaded); } } @@ -651,8 +651,8 @@ impl Provider for LocalInferenceProvider { }, }); - let mut log = RequestLog::start(&self.model_config, &log_payload) - .map_err(|e| ProviderError::ExecutionError(e.to_string()))?; + let mut log = start_log(&self.model_config, &log_payload) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; let (tx, mut rx) = tokio::sync::mpsc::channel::< Result<(Option, Option), ProviderError>, diff --git a/crates/goose/src/providers/local_inference/backend.rs b/crates/goose/src/providers/local_inference/backend.rs index 92259bc608dc..78a2cc3a91a4 100644 --- a/crates/goose/src/providers/local_inference/backend.rs +++ b/crates/goose/src/providers/local_inference/backend.rs @@ -3,8 +3,8 @@ use std::any::Any; use crate::conversation::message::Message; use crate::providers::local_inference::local_model_registry::ModelSettings; -use crate::providers::utils::RequestLog; use goose_providers::errors::ProviderError; +use goose_providers::request_log::RequestLogHandle; use super::{ResolvedModelPaths, StreamSender}; @@ -26,7 +26,7 @@ pub(super) struct LocalGenerationRequest<'a> { pub draft_model_path: Option, pub message_id: &'a str, pub tx: &'a StreamSender, - pub log: &'a mut RequestLog, + pub log: &'a mut Option>, } pub(super) trait LocalInferenceBackend: Send + Sync { diff --git a/crates/goose/src/providers/local_inference/llamacpp/inference_engine.rs b/crates/goose/src/providers/local_inference/llamacpp/inference_engine.rs index 5725bff18e67..166f6476c913 100644 --- a/crates/goose/src/providers/local_inference/llamacpp/inference_engine.rs +++ b/crates/goose/src/providers/local_inference/llamacpp/inference_engine.rs @@ -1,8 +1,8 @@ use crate::providers::local_inference::backend::LocalInferenceBackend; use crate::providers::local_inference::local_model_registry::ModelSettings; use crate::providers::local_inference::multimodal::ExtractedImage; -use crate::providers::utils::RequestLog; use goose_providers::errors::ProviderError; +use goose_providers::request_log::{LoggerHandleExt, RequestLogHandle}; use goose_providers::thinking::{FilterOut, ThinkFilter}; use llama_cpp_2::context::params::LlamaContextParams; use llama_cpp_2::llama_batch::LlamaBatch; @@ -24,7 +24,7 @@ pub(super) struct GenerationContext<'a> { pub model_name: String, pub message_id: &'a str, pub tx: &'a StreamSender, - pub log: &'a mut RequestLog, + pub log: &'a mut Option>, pub images: &'a [ExtractedImage], } diff --git a/crates/goose/src/providers/nanogpt.rs b/crates/goose/src/providers/nanogpt.rs index 568122add557..a65e799e8966 100644 --- a/crates/goose/src/providers/nanogpt.rs +++ b/crates/goose/src/providers/nanogpt.rs @@ -2,7 +2,6 @@ use super::api_client::{ApiClient, AuthMethod}; use super::base::{ConfigKey, MessageStream, Provider, ProviderDef, ProviderMetadata}; use super::openai_compatible::{handle_status, stream_openai_compat}; use super::retry::ProviderRetry; -use super::utils::RequestLog; use crate::conversation::message::Message; use anyhow::Result; use async_trait::async_trait; @@ -11,6 +10,7 @@ use goose_providers::errors::ProviderError; use goose_providers::formats::openai::create_request; use goose_providers::images::ImageFormat; use goose_providers::model::ModelConfig; +use goose_providers::request_log::{start_log, LoggerHandleExt}; use rmcp::model::Tool; pub const NANOGPT_PROVIDER_NAME: &str = "nano-gpt"; @@ -184,7 +184,8 @@ impl Provider for NanoGptProvider { true, )?; - let mut log = RequestLog::start(model_config, &payload)?; + let mut log = start_log(model_config, &payload) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; let response = self .with_retry(|| async { diff --git a/crates/goose/src/providers/ollama.rs b/crates/goose/src/providers/ollama.rs index f3dbfab94778..25e46dae96dc 100644 --- a/crates/goose/src/providers/ollama.rs +++ b/crates/goose/src/providers/ollama.rs @@ -5,7 +5,6 @@ use super::base::{ }; use super::openai_compatible::handle_status; use super::retry::{ProviderRetry, RetryConfig}; -use super::utils::RequestLog; use crate::config::declarative_providers::DeclarativeProviderConfig; use crate::conversation::message::Message; use crate::providers::formats::ollama::{create_request, response_to_streaming_message_ollama}; @@ -17,6 +16,7 @@ use futures::TryStreamExt; use goose_providers::errors::ProviderError; use goose_providers::images::ImageFormat; use goose_providers::model::ModelConfig; +use goose_providers::request_log::{start_log, LoggerHandleExt, RequestLogHandle}; use reqwest::Response; use rmcp::model::Tool; use serde_json::{json, Value}; @@ -307,7 +307,8 @@ impl Provider for OllamaProvider { true, )?; apply_ollama_options(&mut payload, model_config); - let mut log = RequestLog::start(model_config, &payload)?; + let mut log = start_log(model_config, &payload) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; let response = self .with_retry(|| async { @@ -431,7 +432,10 @@ fn with_line_timeout( /// preventing duplicate content from being emitted to the UI. /// Timeout is applied at the raw SSE line level via with_line_timeout so that /// buffering inside response_to_streaming_message_ollama does not cause false stalls. -fn stream_ollama(response: Response, mut log: RequestLog) -> Result { +fn stream_ollama( + response: Response, + mut log: Option>, +) -> Result { let stream = response.bytes_stream().map_err(std::io::Error::other); Ok(Box::pin(try_stream! { @@ -446,7 +450,8 @@ fn stream_ollama(response: Response, mut log: RequestLog) -> Result>, ) -> Result { let stream = response.bytes_stream().map_err(std::io::Error::other); @@ -195,7 +197,8 @@ pub fn stream_openai_compat( e.downcast::() .unwrap_or_else(ProviderError::stream_decode_error) )?; - log.write(&message, usage.as_ref().map(|f| f.usage).as_ref())?; + log.write(&message, usage.as_ref().map(|f| f.usage).as_ref()) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; yield (message, usage); } })) @@ -203,7 +206,7 @@ pub fn stream_openai_compat( pub fn stream_responses_compat( response: Response, - mut log: RequestLog, + mut log: Option>, ) -> Result { let stream = response.bytes_stream().map_err(std::io::Error::other); @@ -219,7 +222,8 @@ pub fn stream_responses_compat( e.downcast::() .unwrap_or_else(ProviderError::stream_decode_error) )?; - log.write(&message, usage.as_ref().map(|f| f.usage).as_ref())?; + log.write(&message, usage.as_ref().map(|f| f.usage).as_ref()) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; yield (message, usage); } })) diff --git a/crates/goose/src/providers/openrouter.rs b/crates/goose/src/providers/openrouter.rs index 2e42ed58b9c0..98b17445bdb7 100644 --- a/crates/goose/src/providers/openrouter.rs +++ b/crates/goose/src/providers/openrouter.rs @@ -8,12 +8,12 @@ use super::api_client::{ApiClient, AuthMethod}; use super::base::{ConfigKey, MessageStream, Provider, ProviderDef, ProviderMetadata}; use super::openai_compatible::{handle_status, stream_openai_compat}; use super::retry::ProviderRetry; -use super::utils::RequestLog; use crate::conversation::message::Message; use crate::providers::formats::openrouter as openrouter_format; use goose_providers::errors::ProviderError; use goose_providers::formats::openai::create_request; use goose_providers::model::ModelConfig; +use goose_providers::request_log::{start_log, LoggerHandleExt}; use rmcp::model::Tool; pub const OPENROUTER_PROVIDER_NAME: &str = "openrouter"; @@ -289,7 +289,8 @@ impl Provider for OpenRouterProvider { obj.insert("transforms".to_string(), json!(["middle-out"])); } - let mut log = RequestLog::start(model_config, &payload)?; + let mut log = start_log(model_config, &payload) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; let response = self .with_retry(|| async { diff --git a/crates/goose/src/providers/sagemaker_tgi.rs b/crates/goose/src/providers/sagemaker_tgi.rs index 0f01bba2dbff..8ae94ede0b8e 100644 --- a/crates/goose/src/providers/sagemaker_tgi.rs +++ b/crates/goose/src/providers/sagemaker_tgi.rs @@ -12,7 +12,6 @@ use smithy_transport_reqwest::ReqwestHttpClient; use super::base::{ConfigKey, MessageStream, Provider, ProviderDef, ProviderMetadata}; use super::retry::ProviderRetry; -use super::utils::RequestLog; use crate::conversation::message::{Message, MessageContent}; use crate::session_context::SESSION_ID_HEADER; use goose_providers::errors::ProviderError; @@ -21,6 +20,7 @@ use chrono::Utc; use futures::future::BoxFuture; use goose_providers::conversation::token_usage::{ProviderUsage, Usage}; use goose_providers::model::ModelConfig; +use goose_providers::request_log::{start_log, LoggerHandleExt}; use rmcp::model::Role; const SAGEMAKER_TGI_PROVIDER_NAME: &str = "sagemaker_tgi"; @@ -350,11 +350,13 @@ impl Provider for SageMakerTgiProvider { "messages": messages, "tools": tools }); - let mut log = RequestLog::start(&self.model, &debug_payload)?; + let mut log = start_log(&self.model, &debug_payload) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; log.write( &serde_json::to_value(&message).unwrap_or_default(), Some(&usage), - )?; + ) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; let provider_usage = ProviderUsage::new(model_name.to_string(), usage); Ok(super::base::stream_from_single_message( diff --git a/crates/goose/src/providers/snowflake.rs b/crates/goose/src/providers/snowflake.rs index 40df2fa65d90..6bd13c2bf3fa 100644 --- a/crates/goose/src/providers/snowflake.rs +++ b/crates/goose/src/providers/snowflake.rs @@ -10,13 +10,14 @@ use super::base::{ConfigKey, MessageStream, Provider, ProviderDef, ProviderMetad use super::formats::snowflake::{create_request, get_usage, response_to_message}; use super::openai_compatible::{map_http_error_to_provider_error, sanitize_url}; use super::retry::ProviderRetry; -use super::utils::{get_model, RequestLog}; +use super::utils::get_model; use crate::config::ConfigError; use crate::conversation::message::Message; use goose_providers::errors::ProviderError; use futures::future::BoxFuture; use goose_providers::model::ModelConfig; +use goose_providers::request_log::{start_log, LoggerHandleExt}; use rmcp::model::Tool; const SNOWFLAKE_PROVIDER_NAME: &str = "snowflake"; @@ -356,7 +357,8 @@ impl Provider for SnowflakeProvider { }; let payload = create_request(model_config, system, messages, tools)?; - let mut log = RequestLog::start(&self.model, &payload)?; + let mut log = start_log(&self.model, &payload) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; let response = self .with_retry(|| async { @@ -369,7 +371,8 @@ impl Provider for SnowflakeProvider { let usage = get_usage(&response)?; let response_model = get_model(&response); - log.write(&response, Some(&usage))?; + log.write(&response, Some(&usage)) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; let provider_usage = ProviderUsage::new(response_model, usage); Ok(super::base::stream_from_single_message( diff --git a/crates/goose/src/providers/tetrate.rs b/crates/goose/src/providers/tetrate.rs index c48b1a45eeb4..5eb1478f90bf 100644 --- a/crates/goose/src/providers/tetrate.rs +++ b/crates/goose/src/providers/tetrate.rs @@ -5,7 +5,6 @@ use super::openai_compatible::{ stream_openai_compat, }; use super::retry::ProviderRetry; -use super::utils::RequestLog; use crate::config::signup_tetrate::TETRATE_DEFAULT_MODEL; use crate::conversation::message::Message; use anyhow::Result; @@ -16,6 +15,7 @@ use goose_providers::images::ImageFormat; use goose_providers::formats::openai::create_request; use goose_providers::model::ModelConfig; +use goose_providers::request_log::{start_log, LoggerHandleExt}; use rmcp::model::Tool; use serde_json::Value; @@ -148,7 +148,8 @@ impl Provider for TetrateProvider { true, )?; - let mut log = RequestLog::start(model_config, &payload)?; + let mut log = start_log(model_config, &payload) + .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; let response = self .with_retry(|| async { diff --git a/crates/goose/src/providers/toolshim.rs b/crates/goose/src/providers/toolshim.rs index 4e2b87570edc..4bd98d8c1cdf 100644 --- a/crates/goose/src/providers/toolshim.rs +++ b/crates/goose/src/providers/toolshim.rs @@ -43,6 +43,7 @@ use futures::StreamExt; use goose_providers::errors::ProviderError; use goose_providers::formats::openai::create_request; use goose_providers::images::ImageFormat; +use goose_providers::model::ModelConfig; use reqwest::Client; use rmcp::model::{object, CallToolRequestParams, RawContent, Tool}; use serde_json::{json, Value}; diff --git a/crates/goose/src/providers/utils.rs b/crates/goose/src/providers/utils.rs index 93e285a4537b..e3b3638c53f0 100644 --- a/crates/goose/src/providers/utils.rs +++ b/crates/goose/src/providers/utils.rs @@ -1,12 +1,11 @@ use crate::config::paths::Paths; use anyhow::{anyhow, Result}; use fs_err::File; -use goose_providers::conversation::token_usage::Usage; use goose_providers::errors::{GoogleErrorCode, ProviderError}; +use goose_providers::request_log::{RequestLogHandle, RequestLogger}; use reqwest::{Response, StatusCode}; -use serde::Serialize; use serde_json::Value; -use std::fmt::Display; +use std::error::Error; use std::io::{BufWriter, Write}; use std::path::PathBuf; use std::time::Duration; @@ -212,19 +211,28 @@ fn unescape_json_values_in_place(value: &mut Value) { } } +pub const LOGS_TO_KEEP: usize = 10; + pub struct RequestLog { + logs_to_keep: usize, +} + +impl RequestLog { + pub fn new(logs_to_keep: usize) -> Result { + let logs_dir = Paths::in_state_dir("logs"); + fs_err::create_dir_all(&logs_dir)?; + Ok(Self { logs_to_keep }) + } +} + +struct FileLogHandle { writer: Option>, temp_path: PathBuf, + logs_to_keep: usize, } -pub const LOGS_TO_KEEP: usize = 10; - -impl RequestLog { - pub fn start(model_config: ModelConfig, payload: &Payload) -> Result - where - ModelConfig: Serialize, - Payload: Serialize, - { +impl RequestLogger for RequestLog { + fn start(&self) -> Result, Box> { let logs_dir = Paths::in_state_dir("logs"); fs_err::create_dir_all(&logs_dir)?; @@ -232,7 +240,7 @@ impl RequestLog { let temp_name = format!("llm_request.{request_id}.jsonl"); let temp_path = logs_dir.join(PathBuf::from(temp_name)); - let mut writer = BufWriter::new( + let writer = BufWriter::new( File::options() .write(true) .create(true) @@ -240,53 +248,38 @@ impl RequestLog { .open(&temp_path)?, ); - let data = serde_json::json!({ - "model_config": model_config, - "input": payload, - }); - writeln!(writer, "{}", serde_json::to_string(&data)?)?; - - Ok(Self { + Ok(Box::new(FileLogHandle { writer: Some(writer), temp_path, - }) + logs_to_keep: self.logs_to_keep, + })) } +} - fn write_json(&mut self, line: &serde_json::Value) -> Result<()> { +impl RequestLogHandle for FileLogHandle { + fn write(&mut self, s: &str) -> Result<(), Box> { let writer = self .writer .as_mut() .ok_or_else(|| anyhow!("logger is finished"))?; - writeln!(writer, "{}", serde_json::to_string(line)?)?; + writeln!(writer, "{}", s)?; Ok(()) } +} - pub fn error(&mut self, error: E) -> Result<()> - where - E: Display, - { - self.write_json(&serde_json::json!({ - "error": format!("{}", error), - })) - } - - pub fn write(&mut self, data: &Payload, usage: Option<&Usage>) -> Result<()> - where - Payload: Serialize, - { - self.write_json(&serde_json::json!({ - "data": data, - "usage": usage, - })) - } - +impl FileLogHandle { fn finish(&mut self) -> Result<()> { if let Some(mut writer) = self.writer.take() { writer.flush()?; let logs_dir = Paths::in_state_dir("logs"); let log_path = |i| logs_dir.join(format!("llm_request.{}.jsonl", i)); - for i in (0..LOGS_TO_KEEP - 1).rev() { + if self.logs_to_keep == 0 { + fs_err::remove_file(&self.temp_path)?; + return Ok(()); + } + + for i in (0..self.logs_to_keep.saturating_sub(1)).rev() { let _ = fs_err::rename(log_path(i), log_path(i + 1)); } @@ -296,7 +289,7 @@ impl RequestLog { } } -impl Drop for RequestLog { +impl Drop for FileLogHandle { fn drop(&mut self) { if std::thread::panicking() { return; @@ -310,24 +303,6 @@ mod tests { use super::*; use serde_json::json; - #[test] - fn test_request_log_start_creates_logs_dir() { - let _guard = env_lock::lock_env([("GOOSE_PATH_ROOT", None::<&str>)]); - let temp_dir = tempfile::tempdir().unwrap(); - std::env::set_var("GOOSE_PATH_ROOT", temp_dir.path()); - - let logs_dir = Paths::in_state_dir("logs"); - assert!(!logs_dir.exists(), "logs dir should not exist yet"); - - let log = RequestLog::start(json!({"name": "test"}), &json!({"model": "test"})) - .expect("RequestLog::start should create missing logs dir"); - drop(log); - - assert!(logs_dir.is_dir(), "logs dir should have been created"); - - std::env::remove_var("GOOSE_PATH_ROOT"); - } - #[test] fn unescape_json_values_with_object() { let value = json!({"text": "Hello\\nWorld"}); From 54ed08bad6b89edc3b9d225046c6e41e3664f212 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Wed, 17 Jun 2026 12:39:47 -0400 Subject: [PATCH 2/6] set up logging --- crates/goose-cli/src/logging.rs | 62 +++++++++----------------- crates/goose-server/src/logging.rs | 2 + crates/goose/src/providers/toolshim.rs | 1 - crates/goose/src/providers/utils.rs | 13 +++++- 4 files changed, 34 insertions(+), 44 deletions(-) diff --git a/crates/goose-cli/src/logging.rs b/crates/goose-cli/src/logging.rs index 68d435c3c240..a7f8af7c9512 100644 --- a/crates/goose-cli/src/logging.rs +++ b/crates/goose-cli/src/logging.rs @@ -1,53 +1,31 @@ use anyhow::Result; -use std::sync::Once; +use goose::providers::utils::init_goose_request_log; +use std::sync::OnceLock; // Used to ensure we only set up tracing once -static INIT: Once = Once::new(); +static INIT: OnceLock> = OnceLock::new(); /// Sets up the logging infrastructure for the CLI. /// Logs go to a JSON file only (no console output). -pub fn setup_logging(name: Option<&str>) -> Result<()> { - setup_logging_internal(name, false) -} - -fn setup_logging_internal(name: Option<&str>, force: bool) -> Result<()> { - let mut result = Ok(()); - - let mut setup = || { - result = (|| { - use tracing_subscriber::util::SubscriberInitExt; - - let config = goose::logging::LoggingConfig { - component: "cli", - name, - extra_directives: &["goose_cli=info"], - console: false, - json: true, - }; - let subscriber = goose::logging::build_logging_subscriber(&config)?; - - if force { - let _guard = subscriber.set_default(); - tracing::warn!("Test log entry from setup"); - tracing::info!("Another test log entry from setup"); - std::thread::sleep(std::time::Duration::from_millis(100)); - Ok(()) - } else { - subscriber - .try_init() - .map_err(|e| anyhow::anyhow!("Failed to set global subscriber: {}", e))?; - Ok(()) - } - })(); - }; +pub fn setup_logging(name: Option<&str>) -> &'static Result<()> { + INIT.get_or_init(|| { + use tracing_subscriber::util::SubscriberInitExt; - if force { - setup(); - } else { - INIT.call_once(setup); - } + init_goose_request_log()?; + let config = goose::logging::LoggingConfig { + component: "cli", + name, + extra_directives: &["goose_cli=info"], + console: false, + json: true, + }; + let subscriber = goose::logging::build_logging_subscriber(&config)?; - result + subscriber + .try_init() + .map_err(|e| anyhow::anyhow!("Failed to set global subscriber: {}", e))?; + Ok(()) + }) } #[cfg(test)] diff --git a/crates/goose-server/src/logging.rs b/crates/goose-server/src/logging.rs index 88af03127c50..499cd118453d 100644 --- a/crates/goose-server/src/logging.rs +++ b/crates/goose-server/src/logging.rs @@ -1,9 +1,11 @@ use anyhow::Result; +use goose::providers::utils::init_goose_request_log; use tracing_subscriber::util::SubscriberInitExt; /// Sets up the logging infrastructure for the server. /// Logs go to a JSON file and a pretty console layer on stderr. pub fn setup_logging(name: Option<&str>) -> Result<()> { + init_goose_request_log()?; let config = goose::logging::LoggingConfig { component: "server", name, diff --git a/crates/goose/src/providers/toolshim.rs b/crates/goose/src/providers/toolshim.rs index 4bd98d8c1cdf..4e2b87570edc 100644 --- a/crates/goose/src/providers/toolshim.rs +++ b/crates/goose/src/providers/toolshim.rs @@ -43,7 +43,6 @@ use futures::StreamExt; use goose_providers::errors::ProviderError; use goose_providers::formats::openai::create_request; use goose_providers::images::ImageFormat; -use goose_providers::model::ModelConfig; use reqwest::Client; use rmcp::model::{object, CallToolRequestParams, RawContent, Tool}; use serde_json::{json, Value}; diff --git a/crates/goose/src/providers/utils.rs b/crates/goose/src/providers/utils.rs index e3b3638c53f0..bf3ac7ad4984 100644 --- a/crates/goose/src/providers/utils.rs +++ b/crates/goose/src/providers/utils.rs @@ -2,12 +2,13 @@ use crate::config::paths::Paths; use anyhow::{anyhow, Result}; use fs_err::File; use goose_providers::errors::{GoogleErrorCode, ProviderError}; -use goose_providers::request_log::{RequestLogHandle, RequestLogger}; +use goose_providers::request_log::{install_logger, RequestLogHandle, RequestLogger}; use reqwest::{Response, StatusCode}; use serde_json::Value; use std::error::Error; use std::io::{BufWriter, Write}; use std::path::PathBuf; +use std::sync::OnceLock; use std::time::Duration; use uuid::Uuid; @@ -213,6 +214,16 @@ fn unescape_json_values_in_place(value: &mut Value) { pub const LOGS_TO_KEEP: usize = 10; +static INIT_LOGGER: OnceLock> = OnceLock::new(); + +pub fn init_goose_request_log() -> Result<()> { + INIT_LOGGER + .get_or_init(|| Ok(install_logger(RequestLog::new(LOGS_TO_KEEP)?)?)) + .as_ref() + .map_err(|e| anyhow::anyhow!("failed to set up logger: {}", e))?; + Ok(()) +} + pub struct RequestLog { logs_to_keep: usize, } From e99f94ee165ce37bda2241c197c83aa27048b325 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Wed, 17 Jun 2026 15:10:27 -0400 Subject: [PATCH 3/6] fixup: restore configkey --- crates/goose/src/providers/cursor_agent.rs | 9 +++------ crates/goose/src/providers/gemini_cli.rs | 6 +++--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/crates/goose/src/providers/cursor_agent.rs b/crates/goose/src/providers/cursor_agent.rs index 62ca9c86114e..e68d30ae7978 100644 --- a/crates/goose/src/providers/cursor_agent.rs +++ b/crates/goose/src/providers/cursor_agent.rs @@ -11,6 +11,7 @@ use super::base::{ stream_from_single_message, ConfigKey, MessageStream, Provider, ProviderDef, ProviderMetadata, }; use super::utils::filter_extensions_from_system_prompt; +use crate::config::base::CursorAgentCommand; use crate::config::search_path::SearchPaths; use crate::conversation::message::{Message, MessageContent}; use crate::subprocess::configure_subprocess; @@ -287,12 +288,8 @@ impl ProviderDef for CursorAgentProvider { CURSOR_AGENT_DEFAULT_MODEL, CURSOR_AGENT_KNOWN_MODELS.to_vec(), CURSOR_AGENT_DOC_URL, - vec![ConfigKey::new( - "CURSOR_AGENT_COMMAND", - true, - false, - Some("cursor-agent"), - true, + vec![ConfigKey::from_value_type::( + true, false, true, )], ) } diff --git a/crates/goose/src/providers/gemini_cli.rs b/crates/goose/src/providers/gemini_cli.rs index 13362dbf4838..0a2339b774ec 100644 --- a/crates/goose/src/providers/gemini_cli.rs +++ b/crates/goose/src/providers/gemini_cli.rs @@ -12,6 +12,7 @@ use super::base::{ }; use super::cli_common::{error_from_event, extract_usage_tokens}; use super::utils::filter_extensions_from_system_prompt; +use crate::config::base::GeminiCliCommand; use crate::config::search_path::SearchPaths; use crate::config::Config; use crate::conversation::message::{Message, MessageContent}; @@ -164,9 +165,8 @@ impl ProviderDef for GeminiCliProvider { GEMINI_CLI_DEFAULT_MODEL, GEMINI_CLI_KNOWN_MODELS.to_vec(), GEMINI_CLI_DOC_URL, - vec![ConfigKey::new( - "GEMINI_CLI_COMMAND", - true, false, Some("gemini"), true, + vec![ConfigKey::from_value_type::( + true, false, true, )], ) } From 6dd7b7a3c4ee2a07ab321357947c3f612a283b22 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Wed, 17 Jun 2026 15:47:04 -0400 Subject: [PATCH 4/6] over-eager find-replace --- crates/goose-providers/src/errors.rs | 8 ++++++++ crates/goose/src/providers/anthropic.rs | 6 ++---- crates/goose/src/providers/bedrock.rs | 3 +-- crates/goose/src/providers/codex.rs | 3 +-- crates/goose/src/providers/cursor_agent.rs | 6 ++---- crates/goose/src/providers/databricks.rs | 6 ++---- crates/goose/src/providers/databricks_v2.rs | 11 ++++------- crates/goose/src/providers/gcpvertexai.rs | 5 ++--- crates/goose/src/providers/gemini_oauth.rs | 5 ++--- crates/goose/src/providers/githubcopilot.rs | 12 ++++-------- crates/goose/src/providers/google.rs | 5 ++--- crates/goose/src/providers/kimicode.rs | 2 +- crates/goose/src/providers/litellm.rs | 6 ++---- crates/goose/src/providers/local_inference.rs | 5 ++--- crates/goose/src/providers/nanogpt.rs | 3 +-- crates/goose/src/providers/ollama.rs | 5 ++--- crates/goose/src/providers/openai.rs | 18 ++++++------------ .../goose/src/providers/openai_compatible.rs | 10 ++++------ crates/goose/src/providers/openrouter.rs | 3 +-- crates/goose/src/providers/sagemaker_tgi.rs | 6 ++---- crates/goose/src/providers/snowflake.rs | 6 ++---- crates/goose/src/providers/tetrate.rs | 3 +-- 22 files changed, 54 insertions(+), 83 deletions(-) diff --git a/crates/goose-providers/src/errors.rs b/crates/goose-providers/src/errors.rs index 839b7f4ce1c7..ed38de7e2542 100644 --- a/crates/goose-providers/src/errors.rs +++ b/crates/goose-providers/src/errors.rs @@ -2,6 +2,8 @@ use reqwest::StatusCode; use std::time::Duration; use thiserror::Error; +use crate::request_log::LogError; + #[derive(Error, Debug, Clone, PartialEq)] pub enum ProviderError { #[error("Authentication error: {0}")] @@ -142,6 +144,12 @@ impl From for ProviderError { } } +impl From for ProviderError { + fn from(value: LogError) -> Self { + ProviderError::ExecutionError(value.to_string()) + } +} + #[derive(Debug)] pub enum GoogleErrorCode { BadRequest = 400, diff --git a/crates/goose/src/providers/anthropic.rs b/crates/goose/src/providers/anthropic.rs index 5e5bb297aa7f..76bf83b8318f 100644 --- a/crates/goose/src/providers/anthropic.rs +++ b/crates/goose/src/providers/anthropic.rs @@ -330,8 +330,7 @@ impl Provider for AnthropicProvider { .insert("stream".to_string(), Value::Bool(true)); let conditional_headers = self.get_conditional_headers(); - let mut log = start_log(model_config, &payload) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + let mut log = start_log(model_config, &payload)?; let response = self .with_retry(|| async { @@ -357,8 +356,7 @@ impl Provider for AnthropicProvider { pin!(message_stream); while let Some(message) = futures::StreamExt::next(&mut message_stream).await { let (message, usage) = message.map_err(ProviderError::from_stream_error)?; - log.write(&message, usage.as_ref().map(|f| f.usage).as_ref()) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + log.write(&message, usage.as_ref().map(|f| f.usage).as_ref())?; yield (message, usage); } })) diff --git a/crates/goose/src/providers/bedrock.rs b/crates/goose/src/providers/bedrock.rs index 6436be36cf17..4b2a2a5993f6 100644 --- a/crates/goose/src/providers/bedrock.rs +++ b/crates/goose/src/providers/bedrock.rs @@ -526,8 +526,7 @@ impl BedrockProvider { "messages": messages, "tools": tools }); - let mut log = start_log(&self.model, &debug_payload) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + let mut log = start_log(&self.model, &debug_payload)?; log.write( &serde_json::to_value(&message).unwrap_or_default(), Some(&usage), diff --git a/crates/goose/src/providers/codex.rs b/crates/goose/src/providers/codex.rs index a394d7560e03..12684cf36444 100644 --- a/crates/goose/src/providers/codex.rs +++ b/crates/goose/src/providers/codex.rs @@ -718,8 +718,7 @@ impl Provider for CodexProvider { "messages_count": messages.len() }); - let mut log = start_log(model_config, &payload) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + let mut log = start_log(model_config, &payload)?; let response = json!({ "lines": lines.len(), diff --git a/crates/goose/src/providers/cursor_agent.rs b/crates/goose/src/providers/cursor_agent.rs index e68d30ae7978..680213498699 100644 --- a/crates/goose/src/providers/cursor_agent.rs +++ b/crates/goose/src/providers/cursor_agent.rs @@ -353,10 +353,8 @@ impl Provider for CursorAgentProvider { "usage": usage }); - let mut log = start_log(&self.model, &payload) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; - log.write(&response, Some(&usage)) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + let mut log = start_log(&self.model, &payload)?; + log.write(&response, Some(&usage))?; let provider_usage = ProviderUsage::new(model_config.model_name.clone(), usage); Ok(stream_from_single_message(message, provider_usage)) diff --git a/crates/goose/src/providers/databricks.rs b/crates/goose/src/providers/databricks.rs index 7e4b0c3cc39a..2e18dc0c18b6 100644 --- a/crates/goose/src/providers/databricks.rs +++ b/crates/goose/src/providers/databricks.rs @@ -653,8 +653,7 @@ impl Provider for DatabricksProvider { payload["client_request_id"] = Value::String(client_request_id.clone()); } - let mut log = start_log(model_config, &payload) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + let mut log = start_log(model_config, &payload)?; let response = self .with_retry(|| async { @@ -719,8 +718,7 @@ impl Provider for DatabricksProvider { .insert("stream_options".to_string(), json!({"include_usage": true})); } - let mut log = start_log(model_config, &payload) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + let mut log = start_log(model_config, &payload)?; let response = self .with_retry(|| async { let resp = self diff --git a/crates/goose/src/providers/databricks_v2.rs b/crates/goose/src/providers/databricks_v2.rs index 548b5e7fbec7..b23ccc4f88d8 100644 --- a/crates/goose/src/providers/databricks_v2.rs +++ b/crates/goose/src/providers/databricks_v2.rs @@ -231,8 +231,7 @@ impl DatabricksV2Provider { let mut payload = openai_responses::create_responses_request(model_config, system, messages, tools)?; payload["stream"] = Value::Bool(true); - let mut log = start_log(model_config, &payload) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + let mut log = start_log(model_config, &payload)?; let response = self .with_retry(|| async { @@ -269,8 +268,7 @@ impl DatabricksV2Provider { if payload.get("max_tokens").is_none() { payload["max_tokens"] = Value::from(model_config.max_output_tokens()); } - let mut log = start_log(model_config, &payload) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + let mut log = start_log(model_config, &payload)?; let response = self .with_retry(|| async { @@ -302,8 +300,7 @@ impl DatabricksV2Provider { ) -> Result { let mut payload = anthropic::create_request(model_config, system, messages, tools)?; payload["stream"] = Value::Bool(true); - let mut log = start_log(model_config, &payload) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + let mut log = start_log(model_config, &payload)?; let response = self .with_retry(|| async { @@ -334,7 +331,7 @@ impl DatabricksV2Provider { while let Some(message) = futures::StreamExt::next(&mut message_stream).await { let (message, usage) = message.map_err(ProviderError::from_stream_error)?; log.write(&message, usage.as_ref().map(|f| f.usage).as_ref()) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + ?; yield (message, usage); } })) diff --git a/crates/goose/src/providers/gcpvertexai.rs b/crates/goose/src/providers/gcpvertexai.rs index dbcd6a509769..cc2d7677c3f9 100644 --- a/crates/goose/src/providers/gcpvertexai.rs +++ b/crates/goose/src/providers/gcpvertexai.rs @@ -618,8 +618,7 @@ impl Provider for GcpVertexAIProvider { } } - let mut log = start_log(model_config, &request) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + let mut log = start_log(model_config, &request)?; let response = self .post_stream(Some(session_id), &request, &context) @@ -644,7 +643,7 @@ impl Provider for GcpVertexAIProvider { while let Some(message) = message_stream.next().await { let (message, usage) = message.map_err(ProviderError::from_stream_error)?; log.write(&message, usage.as_ref().map(|u| &u.usage)) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + ?; yield (message, usage); } })) diff --git a/crates/goose/src/providers/gemini_oauth.rs b/crates/goose/src/providers/gemini_oauth.rs index 3c2e84160c82..3aea76e0b793 100644 --- a/crates/goose/src/providers/gemini_oauth.rs +++ b/crates/goose/src/providers/gemini_oauth.rs @@ -995,8 +995,7 @@ impl Provider for GeminiOAuthProvider { tools: &[Tool], ) -> Result { let payload = create_request(model_config, system, messages, tools)?; - let mut log = start_log(model_config, &payload) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + let mut log = start_log(model_config, &payload)?; let response = self .with_retry(|| async { @@ -1026,7 +1025,7 @@ impl Provider for GeminiOAuthProvider { })?; if message.is_some() || usage.is_some() { log.write(&message, usage.as_ref().map(|f| f.usage).as_ref()) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + ?; } yield (message, usage); } diff --git a/crates/goose/src/providers/githubcopilot.rs b/crates/goose/src/providers/githubcopilot.rs index 00cefff0b0fe..f6d99a0ffa6c 100644 --- a/crates/goose/src/providers/githubcopilot.rs +++ b/crates/goose/src/providers/githubcopilot.rs @@ -404,8 +404,7 @@ impl GithubCopilotProvider { .map_err(|e| ProviderError::RequestFailed(e.to_string()))?; payload["stream"] = serde_json::Value::Bool(true); - let mut log = start_log(model_config, &payload) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + let mut log = start_log(model_config, &payload)?; let response = self .with_retry(|| async { @@ -453,8 +452,7 @@ impl GithubCopilotProvider { &ImageFormat::OpenAi, true, )?; - let mut log = start_log(model_config, &payload) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + let mut log = start_log(model_config, &payload)?; let response = self .with_retry(|| async { @@ -490,8 +488,7 @@ impl GithubCopilotProvider { &ImageFormat::OpenAi, false, )?; - let mut log = start_log(model_config, &payload) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + let mut log = start_log(model_config, &payload)?; let response = self .with_retry(|| async { @@ -516,8 +513,7 @@ impl GithubCopilotProvider { Usage::default() }); let response_model = get_model(&response); - log.write(&response, Some(&usage)) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + log.write(&response, Some(&usage))?; Ok(super::base::stream_from_single_message( message, diff --git a/crates/goose/src/providers/google.rs b/crates/goose/src/providers/google.rs index 338de7f5f6aa..adb114a6c968 100644 --- a/crates/goose/src/providers/google.rs +++ b/crates/goose/src/providers/google.rs @@ -192,8 +192,7 @@ impl Provider for GoogleProvider { tools: &[Tool], ) -> Result { let payload = create_request(model_config, system, messages, tools)?; - let mut log = start_log(model_config, &payload) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + let mut log = start_log(model_config, &payload)?; let response = self .with_retry(|| async { @@ -221,7 +220,7 @@ impl Provider for GoogleProvider { })?; if message.is_some() || usage.is_some() { log.write(&message, usage.as_ref().map(|f| f.usage).as_ref()) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + ?; } yield (message, usage); } diff --git a/crates/goose/src/providers/kimicode.rs b/crates/goose/src/providers/kimicode.rs index f4281bfa2702..a7048391eb5e 100644 --- a/crates/goose/src/providers/kimicode.rs +++ b/crates/goose/src/providers/kimicode.rs @@ -432,7 +432,7 @@ impl Provider for KimiCodeProvider { while let Some(message) = futures::StreamExt::next(&mut message_stream).await { let (message, usage) = message.map_err(ProviderError::from_stream_error)?; log.write(&message, usage.as_ref().map(|f| f.usage).as_ref()) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + ?; yield (message, usage); } })) diff --git a/crates/goose/src/providers/litellm.rs b/crates/goose/src/providers/litellm.rs index be8914ae87d6..c88d766f7e32 100644 --- a/crates/goose/src/providers/litellm.rs +++ b/crates/goose/src/providers/litellm.rs @@ -250,10 +250,8 @@ impl Provider for LiteLLMProvider { let message = goose_providers::formats::openai::response_to_message(&response)?; let usage = goose_providers::formats::openai::get_usage(&response); let response_model = get_model(&response); - let mut log = start_log(model_config, &payload) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; - log.write(&response, Some(&usage)) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + let mut log = start_log(model_config, &payload)?; + log.write(&response, Some(&usage))?; let provider_usage = ProviderUsage::new(response_model, usage); Ok(super::base::stream_from_single_message( message, diff --git a/crates/goose/src/providers/local_inference.rs b/crates/goose/src/providers/local_inference.rs index 99d974c3f6bf..b56647d19cfc 100644 --- a/crates/goose/src/providers/local_inference.rs +++ b/crates/goose/src/providers/local_inference.rs @@ -605,7 +605,7 @@ impl Provider for LocalInferenceProvider { backend_for_load.load_model(&model_id, &resolved_for_load, &settings_for_load) }) .await - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))??; + .map_err(|e| ProviderError::ExecutionError(e.to_string()))??; *model_lock = Some(loaded); } } @@ -651,8 +651,7 @@ impl Provider for LocalInferenceProvider { }, }); - let mut log = start_log(&self.model_config, &log_payload) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + let mut log = start_log(&self.model_config, &log_payload)?; let (tx, mut rx) = tokio::sync::mpsc::channel::< Result<(Option, Option), ProviderError>, diff --git a/crates/goose/src/providers/nanogpt.rs b/crates/goose/src/providers/nanogpt.rs index a65e799e8966..4905c5988ee6 100644 --- a/crates/goose/src/providers/nanogpt.rs +++ b/crates/goose/src/providers/nanogpt.rs @@ -184,8 +184,7 @@ impl Provider for NanoGptProvider { true, )?; - let mut log = start_log(model_config, &payload) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + let mut log = start_log(model_config, &payload)?; let response = self .with_retry(|| async { diff --git a/crates/goose/src/providers/ollama.rs b/crates/goose/src/providers/ollama.rs index 25e46dae96dc..b849066932c0 100644 --- a/crates/goose/src/providers/ollama.rs +++ b/crates/goose/src/providers/ollama.rs @@ -307,8 +307,7 @@ impl Provider for OllamaProvider { true, )?; apply_ollama_options(&mut payload, model_config); - let mut log = start_log(model_config, &payload) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + let mut log = start_log(model_config, &payload)?; let response = self .with_retry(|| async { @@ -451,7 +450,7 @@ fn stream_ollama( while let Some(message) = message_stream.next().await { let (message, usage) = message.map_err(ProviderError::from_stream_error)?; log.write(&message, usage.as_ref().map(|f| f.usage).as_ref()) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + ?; yield (message, usage); } })) diff --git a/crates/goose/src/providers/openai.rs b/crates/goose/src/providers/openai.rs index d1376d325643..a89179bc67f6 100644 --- a/crates/goose/src/providers/openai.rs +++ b/crates/goose/src/providers/openai.rs @@ -273,8 +273,7 @@ impl OpenAiProvider { parsed.host, auth, std::time::Duration::from_secs(timeout_secs), - ) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + )?; if !parsed.query_params.is_empty() { api_client = api_client.with_query(parsed.query_params); @@ -824,8 +823,7 @@ impl Provider for OpenAiProvider { let mut payload = create_responses_request(model_config, system, messages, tools)?; payload["stream"] = serde_json::Value::Bool(self.supports_streaming); - let mut log = start_log(model_config, &payload) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + let mut log = start_log(model_config, &payload)?; let response = self .with_retry(|| async { @@ -871,8 +869,7 @@ impl Provider for OpenAiProvider { log.write( &serde_json::to_value(&message).unwrap_or_default(), Some(&usage_data), - ) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + )?; Ok(super::base::stream_from_single_message(message, usage)) } @@ -887,11 +884,9 @@ impl Provider for OpenAiProvider { OpenAiFormatOptions { preserve_thinking_context: self.preserve_thinking_context, }, - ) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + )?; let payload = self.sanitize_request_for_compat(payload); - let mut log = start_log(model_config, &payload) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + let mut log = start_log(model_config, &payload)?; let response = self .with_retry(|| async { @@ -923,8 +918,7 @@ impl Provider for OpenAiProvider { log.write( &serde_json::to_value(&message).unwrap_or_default(), Some(&usage_data), - ) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + )?; Ok(super::base::stream_from_single_message(message, usage)) } diff --git a/crates/goose/src/providers/openai_compatible.rs b/crates/goose/src/providers/openai_compatible.rs index 5c3632777229..3a06966d66a4 100644 --- a/crates/goose/src/providers/openai_compatible.rs +++ b/crates/goose/src/providers/openai_compatible.rs @@ -128,8 +128,7 @@ impl Provider for OpenAiCompatibleProvider { tools, self.supports_streaming, )?; - let mut log = start_log(model_config, &payload) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + let mut log = start_log(model_config, &payload)?; let completions_path = format!("{}chat/completions", self.completions_prefix); let response = self @@ -162,8 +161,7 @@ impl Provider for OpenAiCompatibleProvider { log.write( &serde_json::to_value(&message).unwrap_or_default(), Some(&usage.usage), - ) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + )?; Ok(stream_from_single_message(message, usage)) } @@ -198,7 +196,7 @@ pub fn stream_openai_compat( .unwrap_or_else(ProviderError::stream_decode_error) )?; log.write(&message, usage.as_ref().map(|f| f.usage).as_ref()) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + ?; yield (message, usage); } })) @@ -223,7 +221,7 @@ pub fn stream_responses_compat( .unwrap_or_else(ProviderError::stream_decode_error) )?; log.write(&message, usage.as_ref().map(|f| f.usage).as_ref()) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + ?; yield (message, usage); } })) diff --git a/crates/goose/src/providers/openrouter.rs b/crates/goose/src/providers/openrouter.rs index 98b17445bdb7..6b3cf5d3b0b5 100644 --- a/crates/goose/src/providers/openrouter.rs +++ b/crates/goose/src/providers/openrouter.rs @@ -289,8 +289,7 @@ impl Provider for OpenRouterProvider { obj.insert("transforms".to_string(), json!(["middle-out"])); } - let mut log = start_log(model_config, &payload) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + let mut log = start_log(model_config, &payload)?; let response = self .with_retry(|| async { diff --git a/crates/goose/src/providers/sagemaker_tgi.rs b/crates/goose/src/providers/sagemaker_tgi.rs index 8ae94ede0b8e..6164e4f5f782 100644 --- a/crates/goose/src/providers/sagemaker_tgi.rs +++ b/crates/goose/src/providers/sagemaker_tgi.rs @@ -350,13 +350,11 @@ impl Provider for SageMakerTgiProvider { "messages": messages, "tools": tools }); - let mut log = start_log(&self.model, &debug_payload) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + let mut log = start_log(&self.model, &debug_payload)?; log.write( &serde_json::to_value(&message).unwrap_or_default(), Some(&usage), - ) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + )?; let provider_usage = ProviderUsage::new(model_name.to_string(), usage); Ok(super::base::stream_from_single_message( diff --git a/crates/goose/src/providers/snowflake.rs b/crates/goose/src/providers/snowflake.rs index 6bd13c2bf3fa..cb6c70616563 100644 --- a/crates/goose/src/providers/snowflake.rs +++ b/crates/goose/src/providers/snowflake.rs @@ -357,8 +357,7 @@ impl Provider for SnowflakeProvider { }; let payload = create_request(model_config, system, messages, tools)?; - let mut log = start_log(&self.model, &payload) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + let mut log = start_log(&self.model, &payload)?; let response = self .with_retry(|| async { @@ -371,8 +370,7 @@ impl Provider for SnowflakeProvider { let usage = get_usage(&response)?; let response_model = get_model(&response); - log.write(&response, Some(&usage)) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + log.write(&response, Some(&usage))?; let provider_usage = ProviderUsage::new(response_model, usage); Ok(super::base::stream_from_single_message( diff --git a/crates/goose/src/providers/tetrate.rs b/crates/goose/src/providers/tetrate.rs index 5eb1478f90bf..1eb4f3140de0 100644 --- a/crates/goose/src/providers/tetrate.rs +++ b/crates/goose/src/providers/tetrate.rs @@ -148,8 +148,7 @@ impl Provider for TetrateProvider { true, )?; - let mut log = start_log(model_config, &payload) - .map_err(|e| anyhow::anyhow!("failed to log: {}", e))?; + let mut log = start_log(model_config, &payload)?; let response = self .with_retry(|| async { From ed972ea8eb7058fb575929e83ec2797d09dea44a Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Wed, 17 Jun 2026 15:51:47 -0400 Subject: [PATCH 5/6] get rid of two unnecessary maps --- crates/goose/src/providers/bedrock.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/goose/src/providers/bedrock.rs b/crates/goose/src/providers/bedrock.rs index 4b2a2a5993f6..cfb8069e06b0 100644 --- a/crates/goose/src/providers/bedrock.rs +++ b/crates/goose/src/providers/bedrock.rs @@ -530,8 +530,7 @@ impl BedrockProvider { log.write( &serde_json::to_value(&message).unwrap_or_default(), Some(&usage), - ) - .map_err(anyhow::Error::from)?; + )?; let provider_usage = ProviderUsage::new(model_name.to_string(), usage); Ok(super::base::stream_from_single_message( @@ -811,7 +810,7 @@ impl Provider for BedrockProvider { "messages": messages, "tools": tools }); - let mut log = start_log(&self.model, &debug_payload).map_err(anyhow::Error::from)?; + let mut log = start_log(&self.model, &debug_payload)?; let mut event_stream = response.stream; From c3077737e439fd9a7c296bf731583ae887770c6a Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Wed, 17 Jun 2026 15:56:35 -0400 Subject: [PATCH 6/6] fmt didn't fix these --- crates/goose/src/providers/databricks_v2.rs | 3 +-- crates/goose/src/providers/gcpvertexai.rs | 3 +-- crates/goose/src/providers/gemini_oauth.rs | 3 +-- crates/goose/src/providers/google.rs | 3 +-- crates/goose/src/providers/kimicode.rs | 3 +-- crates/goose/src/providers/ollama.rs | 3 +-- crates/goose/src/providers/openai_compatible.rs | 6 ++---- 7 files changed, 8 insertions(+), 16 deletions(-) diff --git a/crates/goose/src/providers/databricks_v2.rs b/crates/goose/src/providers/databricks_v2.rs index b23ccc4f88d8..b07fe66a5e9b 100644 --- a/crates/goose/src/providers/databricks_v2.rs +++ b/crates/goose/src/providers/databricks_v2.rs @@ -330,8 +330,7 @@ impl DatabricksV2Provider { pin!(message_stream); while let Some(message) = futures::StreamExt::next(&mut message_stream).await { let (message, usage) = message.map_err(ProviderError::from_stream_error)?; - log.write(&message, usage.as_ref().map(|f| f.usage).as_ref()) - ?; + log.write(&message, usage.as_ref().map(|f| f.usage).as_ref())?; yield (message, usage); } })) diff --git a/crates/goose/src/providers/gcpvertexai.rs b/crates/goose/src/providers/gcpvertexai.rs index cc2d7677c3f9..0a595ec390db 100644 --- a/crates/goose/src/providers/gcpvertexai.rs +++ b/crates/goose/src/providers/gcpvertexai.rs @@ -642,8 +642,7 @@ impl Provider for GcpVertexAIProvider { while let Some(message) = message_stream.next().await { let (message, usage) = message.map_err(ProviderError::from_stream_error)?; - log.write(&message, usage.as_ref().map(|u| &u.usage)) - ?; + log.write(&message, usage.as_ref().map(|u| &u.usage))?; yield (message, usage); } })) diff --git a/crates/goose/src/providers/gemini_oauth.rs b/crates/goose/src/providers/gemini_oauth.rs index 3aea76e0b793..00ba4651eb70 100644 --- a/crates/goose/src/providers/gemini_oauth.rs +++ b/crates/goose/src/providers/gemini_oauth.rs @@ -1024,8 +1024,7 @@ impl Provider for GeminiOAuthProvider { .unwrap_or_else(ProviderError::stream_decode_error) })?; if message.is_some() || usage.is_some() { - log.write(&message, usage.as_ref().map(|f| f.usage).as_ref()) - ?; + log.write(&message, usage.as_ref().map(|f| f.usage).as_ref())?; } yield (message, usage); } diff --git a/crates/goose/src/providers/google.rs b/crates/goose/src/providers/google.rs index adb114a6c968..e158c1355da9 100644 --- a/crates/goose/src/providers/google.rs +++ b/crates/goose/src/providers/google.rs @@ -219,8 +219,7 @@ impl Provider for GoogleProvider { .unwrap_or_else(ProviderError::stream_decode_error) })?; if message.is_some() || usage.is_some() { - log.write(&message, usage.as_ref().map(|f| f.usage).as_ref()) - ?; + log.write(&message, usage.as_ref().map(|f| f.usage).as_ref())?; } yield (message, usage); } diff --git a/crates/goose/src/providers/kimicode.rs b/crates/goose/src/providers/kimicode.rs index a7048391eb5e..db9831c2ba46 100644 --- a/crates/goose/src/providers/kimicode.rs +++ b/crates/goose/src/providers/kimicode.rs @@ -431,8 +431,7 @@ impl Provider for KimiCodeProvider { pin!(message_stream); while let Some(message) = futures::StreamExt::next(&mut message_stream).await { let (message, usage) = message.map_err(ProviderError::from_stream_error)?; - log.write(&message, usage.as_ref().map(|f| f.usage).as_ref()) - ?; + log.write(&message, usage.as_ref().map(|f| f.usage).as_ref())?; yield (message, usage); } })) diff --git a/crates/goose/src/providers/ollama.rs b/crates/goose/src/providers/ollama.rs index b849066932c0..62c3bfc55335 100644 --- a/crates/goose/src/providers/ollama.rs +++ b/crates/goose/src/providers/ollama.rs @@ -449,8 +449,7 @@ fn stream_ollama( while let Some(message) = message_stream.next().await { let (message, usage) = message.map_err(ProviderError::from_stream_error)?; - log.write(&message, usage.as_ref().map(|f| f.usage).as_ref()) - ?; + log.write(&message, usage.as_ref().map(|f| f.usage).as_ref())?; yield (message, usage); } })) diff --git a/crates/goose/src/providers/openai_compatible.rs b/crates/goose/src/providers/openai_compatible.rs index 3a06966d66a4..dab5b6a2ff8e 100644 --- a/crates/goose/src/providers/openai_compatible.rs +++ b/crates/goose/src/providers/openai_compatible.rs @@ -195,8 +195,7 @@ pub fn stream_openai_compat( e.downcast::() .unwrap_or_else(ProviderError::stream_decode_error) )?; - log.write(&message, usage.as_ref().map(|f| f.usage).as_ref()) - ?; + log.write(&message, usage.as_ref().map(|f| f.usage).as_ref())?; yield (message, usage); } })) @@ -220,8 +219,7 @@ pub fn stream_responses_compat( e.downcast::() .unwrap_or_else(ProviderError::stream_decode_error) )?; - log.write(&message, usage.as_ref().map(|f| f.usage).as_ref()) - ?; + log.write(&message, usage.as_ref().map(|f| f.usage).as_ref())?; yield (message, usage); } }))