refactor the request logger out of providers (#9847)
Some checks failed
Canary / Prepare Version (push) Waiting to run
Canary / build-cli (push) Blocked by required conditions
Canary / Upload Install Script (push) Blocked by required conditions
Canary / bundle-desktop (push) Blocked by required conditions
Canary / bundle-desktop-intel (push) Blocked by required conditions
Canary / bundle-desktop-linux (push) Blocked by required conditions
Canary / bundle-desktop-windows (push) Blocked by required conditions
Canary / bundle-desktop-windows-cuda (push) Blocked by required conditions
Canary / Release (push) Blocked by required conditions
Cargo Deny / deny (push) Waiting to run
Unused Dependencies / machete (push) Waiting to run
CI / changes (push) Waiting to run
CI / Check Rust Code Format (push) Blocked by required conditions
CI / Build and Test Rust Project (push) Blocked by required conditions
CI / Build Rust Project on Windows (push) Waiting to run
CI / Check MSRV (push) Blocked by required conditions
CI / Lint Rust Code (push) Blocked by required conditions
CI / Check Generated Schemas are Up-to-Date (push) Blocked by required conditions
CI / Test and Lint Electron Desktop App (push) Blocked by required conditions
Create Minor Release PR / check-version-bump-pr (push) Waiting to run
Create Minor Release PR / release (push) Blocked by required conditions
Live Provider Tests / check-fork (push) Waiting to run
Live Provider Tests / changes (push) Blocked by required conditions
Live Provider Tests / Build Binary (push) Blocked by required conditions
Live Provider Tests / Smoke Tests (push) Blocked by required conditions
Live Provider Tests / Smoke Tests (Code Execution) (push) Blocked by required conditions
Live Provider Tests / Compaction Tests (push) Blocked by required conditions
Live Provider Tests / goose server HTTP integration tests (push) Blocked by required conditions
Publish Docker Image / docker (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
Deploy Documentation / deploy (push) Has been cancelled
Publish Ask AI Bot Docker Image / docker (push) Has been cancelled

This commit is contained in:
Jack Amadeo 2026-06-18 20:46:45 -04:00 committed by GitHub
parent 08fb6d45f3
commit 0f1846a439
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 286 additions and 172 deletions

View file

@ -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<Result<()>> = 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)
}
pub fn setup_logging(name: Option<&str>) -> &'static Result<()> {
INIT.get_or_init(|| {
use tracing_subscriber::util::SubscriberInitExt;
fn setup_logging_internal(name: Option<&str>, force: bool) -> Result<()> {
let mut result = Ok(());
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)?;
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(())
}
})();
};
if force {
setup();
} else {
INIT.call_once(setup);
}
result
subscriber
.try_init()
.map_err(|e| anyhow::anyhow!("Failed to set global subscriber: {}", e))?;
Ok(())
})
}
#[cfg(test)]

View file

@ -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 {

View file

@ -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<reqwest::Error> for ProviderError {
}
}
impl From<LogError> for ProviderError {
fn from(value: LogError) -> Self {
ProviderError::ExecutionError(value.to_string())
}
}
#[derive(Debug)]
pub enum GoogleErrorCode {
BadRequest = 400,

View file

@ -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;

View file

@ -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<dyn Error + Send + Sync>;
static LOGGER: OnceLock<Arc<dyn RequestLogger>> = 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: RequestLogger + 'static>(r: R) -> Result<(), LoggerAlreadyInstalled> {
LOGGER.set(Arc::new(r)).map_err(|_| LoggerAlreadyInstalled)
}
pub trait RequestLogger: Send + Sync {
fn start(&self) -> Result<Box<dyn RequestLogHandle>, 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<RequestLogError> for LogError {
fn from(value: RequestLogError) -> Self {
Self::LoggerError(value.to_string())
}
}
fn serialize(v: &serde_json::Value) -> Result<String, LogError> {
serde_json::to_string(v).map_err(LogError::SerializeError)
}
pub fn start_log<M, P>(
model_config: M,
payload: P,
) -> Result<Option<Box<dyn RequestLogHandle>>, 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<Payload>(&mut self, data: &Payload, usage: Option<&Usage>) -> Result<(), LogError>
where
Payload: Serialize;
fn error<E>(&mut self, error: E) -> Result<(), LogError>
where
E: Display;
}
impl LoggerHandleExt for Option<Box<dyn RequestLogHandle>> {
fn write<Payload>(&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<E>(&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())?)
}
}

View file

@ -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,

View file

@ -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,7 @@ 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)?;
let response = self
.with_retry(|| async {
@ -362,6 +362,7 @@ impl Provider for AnthropicProvider {
}))
}
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -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::*;

View file

@ -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,7 +526,7 @@ impl BedrockProvider {
"messages": messages,
"tools": tools
});
let mut log = RequestLog::start(&self.model, &debug_payload)?;
let mut log = start_log(&self.model, &debug_payload)?;
log.write(
&serde_json::to_value(&message).unwrap_or_default(),
Some(&usage),
@ -775,7 +775,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 +810,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)?;
let mut event_stream = response.stream;

View file

@ -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,7 @@ 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)?;
let response = json!({
"lines": lines.len(),

View file

@ -10,7 +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 super::utils::filter_extensions_from_system_prompt;
use crate::config::base::CursorAgentCommand;
use crate::config::search_path::SearchPaths;
use crate::conversation::message::{Message, MessageContent};
@ -19,6 +19,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";
@ -352,7 +353,7 @@ impl Provider for CursorAgentProvider {
"usage": usage
});
let mut log = RequestLog::start(&self.model, &payload)?;
let mut log = start_log(&self.model, &payload)?;
log.write(&response, Some(&usage))?;
let provider_usage = ProviderUsage::new(model_config.model_name.clone(), usage);

View file

@ -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,7 @@ 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)?;
let response = self
.with_retry(|| async {
@ -718,7 +718,7 @@ 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)?;
let response = self
.with_retry(|| async {
let resp = self

View file

@ -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,7 @@ 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)?;
let response = self
.with_retry(|| async {
@ -268,7 +268,7 @@ 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)?;
let response = self
.with_retry(|| async {
@ -300,7 +300,7 @@ impl DatabricksV2Provider {
) -> Result<MessageStream, ProviderError> {
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)?;
let response = self
.with_retry(|| async {

View file

@ -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,7 @@ impl Provider for GcpVertexAIProvider {
}
}
let mut log = RequestLog::start(model_config, &request)?;
let mut log = start_log(model_config, &request)?;
let response = self
.post_stream(Some(session_id), &request, &context)

View file

@ -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,7 @@ impl Provider for GeminiOAuthProvider {
tools: &[Tool],
) -> Result<MessageStream, ProviderError> {
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)?;
let response = self
.with_retry(|| async {

View file

@ -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,7 @@ 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)?;
let response = self
.with_retry(|| async {
@ -451,7 +452,7 @@ impl GithubCopilotProvider {
&ImageFormat::OpenAi,
true,
)?;
let mut log = RequestLog::start(model_config, &payload)?;
let mut log = start_log(model_config, &payload)?;
let response = self
.with_retry(|| async {
@ -487,7 +488,7 @@ impl GithubCopilotProvider {
&ImageFormat::OpenAi,
false,
)?;
let mut log = RequestLog::start(model_config, &payload)?;
let mut log = start_log(model_config, &payload)?;
let response = self
.with_retry(|| async {

View file

@ -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,7 @@ impl Provider for GoogleProvider {
tools: &[Tool],
) -> Result<MessageStream, ProviderError> {
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)?;
let response = self
.with_retry(|| async {

View file

@ -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

View file

@ -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,7 +250,7 @@ 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)?;
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(

View file

@ -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<Box<dyn RequestLogHandle>>,
model_name: String,
path_label: &str,
prompt_token_count: usize,
@ -651,8 +651,7 @@ 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)?;
let (tx, mut rx) = tokio::sync::mpsc::channel::<
Result<(Option<Message>, Option<ProviderUsage>), ProviderError>,

View file

@ -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<std::path::PathBuf>,
pub message_id: &'a str,
pub tx: &'a StreamSender,
pub log: &'a mut RequestLog,
pub log: &'a mut Option<Box<dyn RequestLogHandle>>,
}
pub(super) trait LocalInferenceBackend: Send + Sync {

View file

@ -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<Box<dyn RequestLogHandle>>,
pub images: &'a [ExtractedImage],
}

View file

@ -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,7 @@ impl Provider for NanoGptProvider {
true,
)?;
let mut log = RequestLog::start(model_config, &payload)?;
let mut log = start_log(model_config, &payload)?;
let response = self
.with_retry(|| async {

View file

@ -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,7 @@ 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)?;
let response = self
.with_retry(|| async {
@ -431,7 +431,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<MessageStream, ProviderError> {
fn stream_ollama(
response: Response,
mut log: Option<Box<dyn RequestLogHandle>>,
) -> Result<MessageStream, ProviderError> {
let stream = response.bytes_stream().map_err(std::io::Error::other);
Ok(Box::pin(try_stream! {

View file

@ -21,11 +21,11 @@ use goose_providers::formats::openai::{
create_request_with_options, get_usage, response_to_message, OpenAiFormatOptions,
};
use goose_providers::images::ImageFormat;
use goose_providers::request_log::{start_log, LoggerHandleExt};
use reqwest::StatusCode;
use std::collections::HashMap;
use crate::providers::base::MessageStream;
use crate::providers::utils::RequestLog;
use goose_providers::model::ModelConfig;
use rmcp::model::Tool;
@ -823,7 +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 = RequestLog::start(model_config, &payload)?;
let mut log = start_log(model_config, &payload)?;
let response = self
.with_retry(|| async {
@ -886,7 +886,7 @@ impl Provider for OpenAiProvider {
},
)?;
let payload = self.sanitize_request_for_compat(payload);
let mut log = RequestLog::start(model_config, &payload)?;
let mut log = start_log(model_config, &payload)?;
let response = self
.with_retry(|| async {

View file

@ -15,7 +15,6 @@ use tokio_util::io::StreamReader;
use super::api_client::ApiClient;
use super::base::{stream_from_single_message, MessageStream, Provider};
use super::retry::ProviderRetry;
use super::utils::RequestLog;
use crate::conversation::message::Message;
use crate::providers::formats::openai_responses::responses_api_to_streaming_message;
use goose_providers::errors::ProviderError;
@ -23,6 +22,7 @@ use goose_providers::formats::openai::{
create_request, get_usage, response_to_message, response_to_streaming_message,
};
use goose_providers::model::ModelConfig;
use goose_providers::request_log::{start_log, LoggerHandleExt, RequestLogHandle};
use rmcp::model::Tool;
pub struct OpenAiCompatibleProvider {
@ -128,7 +128,7 @@ impl Provider for OpenAiCompatibleProvider {
tools,
self.supports_streaming,
)?;
let mut log = RequestLog::start(model_config, &payload)?;
let mut log = start_log(model_config, &payload)?;
let completions_path = format!("{}chat/completions", self.completions_prefix);
let response = self
@ -179,7 +179,7 @@ pub use super::http_status::handle_response as handle_response_openai_compat;
pub fn stream_openai_compat(
response: Response,
mut log: RequestLog,
mut log: Option<Box<dyn RequestLogHandle>>,
) -> Result<MessageStream, ProviderError> {
let stream = response.bytes_stream().map_err(std::io::Error::other);
@ -203,7 +203,7 @@ pub fn stream_openai_compat(
pub fn stream_responses_compat(
response: Response,
mut log: RequestLog,
mut log: Option<Box<dyn RequestLogHandle>>,
) -> Result<MessageStream, ProviderError> {
let stream = response.bytes_stream().map_err(std::io::Error::other);

View file

@ -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,7 @@ 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)?;
let response = self
.with_retry(|| async {

View file

@ -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,7 +350,7 @@ 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)?;
log.write(
&serde_json::to_value(&message).unwrap_or_default(),
Some(&usage),

View file

@ -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,7 @@ 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)?;
let response = self
.with_retry(|| async {

View file

@ -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,7 @@ impl Provider for TetrateProvider {
true,
)?;
let mut log = RequestLog::start(model_config, &payload)?;
let mut log = start_log(model_config, &payload)?;
let response = self
.with_retry(|| async {

View file

@ -1,14 +1,14 @@
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::{install_logger, 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::sync::OnceLock;
use std::time::Duration;
use uuid::Uuid;
@ -212,19 +212,38 @@ fn unescape_json_values_in_place(value: &mut Value) {
}
}
pub struct RequestLog {
writer: Option<BufWriter<File>>,
temp_path: PathBuf,
}
pub const LOGS_TO_KEEP: usize = 10;
static INIT_LOGGER: OnceLock<Result<()>> = 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,
}
impl RequestLog {
pub fn start<ModelConfig, Payload>(model_config: ModelConfig, payload: &Payload) -> Result<Self>
where
ModelConfig: Serialize,
Payload: Serialize,
{
pub fn new(logs_to_keep: usize) -> Result<Self> {
let logs_dir = Paths::in_state_dir("logs");
fs_err::create_dir_all(&logs_dir)?;
Ok(Self { logs_to_keep })
}
}
struct FileLogHandle {
writer: Option<BufWriter<File>>,
temp_path: PathBuf,
logs_to_keep: usize,
}
impl RequestLogger for RequestLog {
fn start(&self) -> Result<Box<dyn RequestLogHandle>, Box<dyn Error + Send + Sync>> {
let logs_dir = Paths::in_state_dir("logs");
fs_err::create_dir_all(&logs_dir)?;
@ -232,7 +251,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 +259,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<dyn Error + Send + Sync>> {
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<E>(&mut self, error: E) -> Result<()>
where
E: Display,
{
self.write_json(&serde_json::json!({
"error": format!("{}", error),
}))
}
pub fn write<Payload>(&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 +300,7 @@ impl RequestLog {
}
}
impl Drop for RequestLog {
impl Drop for FileLogHandle {
fn drop(&mut self) {
if std::thread::panicking() {
return;
@ -310,24 +314,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"});