From 10e59c223ef73cb2687efa407eb6f86ec158dca2 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Mon, 8 Jun 2026 16:03:51 -0400 Subject: [PATCH] move formats/anthropic --- Cargo.lock | 1 + crates/goose-providers/Cargo.toml | 1 + crates/goose-providers/src/formats.rs | 1 + .../src}/formats/anthropic.rs | 142 ++++++++---------- crates/goose-providers/src/models.rs | 9 ++ crates/goose/src/model.rs | 11 ++ crates/goose/src/providers/anthropic.rs | 51 ++++--- crates/goose/src/providers/databricks_v2.rs | 6 +- .../goose/src/providers/formats/databricks.rs | 8 +- .../src/providers/formats/gcpvertexai.rs | 6 +- crates/goose/src/providers/formats/mod.rs | 1 - crates/goose/src/providers/kimicode.rs | 4 +- 12 files changed, 124 insertions(+), 117 deletions(-) rename crates/{goose/src/providers => goose-providers/src}/formats/anthropic.rs (94%) diff --git a/Cargo.lock b/Cargo.lock index 6ce7a570c9..4afc97e94e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4773,6 +4773,7 @@ dependencies = [ "async-stream", "base64 0.22.1", "chrono", + "env-lock", "futures", "once_cell", "regex", diff --git a/crates/goose-providers/Cargo.toml b/crates/goose-providers/Cargo.toml index 267d2b91c8..f87d98db8c 100644 --- a/crates/goose-providers/Cargo.toml +++ b/crates/goose-providers/Cargo.toml @@ -30,6 +30,7 @@ utoipa = { workspace = true, features = ["chrono"] } uuid = { workspace = true, features = ["v4", "std"] } [dev-dependencies] +env-lock = { workspace = true } test-case = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true } diff --git a/crates/goose-providers/src/formats.rs b/crates/goose-providers/src/formats.rs index d8c308735b..943612b1e2 100644 --- a/crates/goose-providers/src/formats.rs +++ b/crates/goose-providers/src/formats.rs @@ -1 +1,2 @@ +pub mod anthropic; pub mod openai; diff --git a/crates/goose/src/providers/formats/anthropic.rs b/crates/goose-providers/src/formats/anthropic.rs similarity index 94% rename from crates/goose/src/providers/formats/anthropic.rs rename to crates/goose-providers/src/formats/anthropic.rs index ac7b50675d..66a5188493 100644 --- a/crates/goose/src/providers/formats/anthropic.rs +++ b/crates/goose-providers/src/formats/anthropic.rs @@ -1,11 +1,11 @@ use crate::conversation::message::{Message, MessageContent}; +use crate::conversation::token_usage::{ProviderUsage, Usage}; +use crate::errors::ProviderError; +use crate::images::{convert_image, ImageFormat}; use crate::mcp_utils::extract_text_from_resource; -use crate::model::ModelConfig; +use crate::models::ModelConfigParams; +use crate::thinking::ThinkingEffort; use anyhow::{anyhow, Result}; -use goose_providers::conversation::token_usage::{ProviderUsage, Usage}; -use goose_providers::errors::ProviderError; -use goose_providers::images::{convert_image, ImageFormat}; -use goose_providers::thinking::ThinkingEffort; use rmcp::model::{object, CallToolRequestParams, ErrorCode, ErrorData, JsonObject, Role, Tool}; use rmcp::object as json_object; use serde_json::{json, Value}; @@ -45,46 +45,19 @@ pub struct AnthropicFormatOptions { pub preserve_thinking_context: bool, } -impl AnthropicFormatOptions { - fn for_model(self, model_config: &ModelConfig) -> Self { - let preserve_thinking_context = model_config - .get_config_param::( - "preserve_thinking_context", - "ANTHROPIC_PRESERVE_THINKING_CONTEXT", - ) - .unwrap_or(self.preserve_thinking_context); - let preserve_unsigned_thinking = model_config - .get_config_param::( - "preserve_unsigned_thinking", - "ANTHROPIC_PRESERVE_UNSIGNED_THINKING", - ) - .unwrap_or(self.preserve_unsigned_thinking) - || preserve_thinking_context; - - Self { - preserve_unsigned_thinking, - preserve_thinking_context, - } - } -} - pub fn supports_adaptive_thinking(model_name: &str) -> bool { let lower = model_name.to_lowercase(); lower.contains("claude-opus-4-6") || lower.contains("claude-sonnet-4-6") } -pub fn thinking_type(model_config: &ModelConfig) -> ThinkingType { +pub fn thinking_type(model_config: &ModelConfigParams) -> ThinkingType { let model_lower = model_config.model_name.to_lowercase(); if !model_lower.contains("claude") { return ThinkingType::Disabled; } let is_adaptive_model = supports_adaptive_thinking(&model_config.model_name); - let effort = model_config.thinking_effort(); - - if effort.is_none() && legacy_thinking_budget_tokens().is_some() { - return ThinkingType::Enabled; - } + let effort = model_config.thinking_effort; match effort.unwrap_or(ThinkingEffort::Off) { ThinkingEffort::Off => ThinkingType::Disabled, @@ -507,13 +480,11 @@ pub fn get_usage(data: &Value) -> Result { } } -pub fn thinking_effort(model_config: &ModelConfig) -> ThinkingEffort { - model_config - .thinking_effort() - .unwrap_or(ThinkingEffort::High) +pub fn thinking_effort(model_config: &ModelConfigParams) -> ThinkingEffort { + model_config.thinking_effort.unwrap_or(ThinkingEffort::High) } -pub fn thinking_budget_tokens(model_config: &ModelConfig) -> i32 { +pub fn thinking_budget_tokens(model_config: &ModelConfigParams) -> i32 { if let Some(request_param) = model_config .request_params .as_ref() @@ -523,13 +494,7 @@ pub fn thinking_budget_tokens(model_config: &ModelConfig) -> i32 { return request_param.max(1024); } - if let Some(budget) = legacy_thinking_budget_tokens() { - return budget; - } - - let effort = model_config - .thinking_effort() - .unwrap_or(ThinkingEffort::High); + let effort = model_config.thinking_effort.unwrap_or(ThinkingEffort::High); match effort { ThinkingEffort::Off => 1024, ThinkingEffort::Low => 4000, @@ -539,19 +504,9 @@ pub fn thinking_budget_tokens(model_config: &ModelConfig) -> i32 { } } -fn legacy_thinking_budget_tokens() -> Option { - let config = crate::config::Config::global(); - for key in ["ANTHROPIC_THINKING_BUDGET", "CLAUDE_THINKING_BUDGET"] { - if let Ok(budget) = config.get_param::(key) { - return Some(budget.max(1024)); - } - } - None -} - fn apply_thinking_config( payload: &mut Value, - model_config: &ModelConfig, + model_config: &ModelConfigParams, max_tokens: i32, options: AnthropicFormatOptions, ) { @@ -598,7 +553,7 @@ fn apply_thinking_config( /// Create a complete request payload for Anthropic's API pub fn create_request( - model_config: &ModelConfig, + model_config: &ModelConfigParams, system: &str, messages: &[Message], tools: &[Tool], @@ -613,13 +568,12 @@ pub fn create_request( } pub fn create_request_with_options( - model_config: &ModelConfig, + model_config: &ModelConfigParams, system: &str, messages: &[Message], tools: &[Tool], options: AnthropicFormatOptions, ) -> Result { - let options = options.for_model(model_config); let anthropic_messages = format_messages_with_options(messages, options); let tool_specs = format_tools(tools); let system_spec = format_system(system); @@ -921,9 +875,10 @@ where mod tests { use super::*; use crate::conversation::message::Message; - use crate::model::ModelConfig; + use crate::models::ModelConfigParams; use rmcp::object; use serde_json::json; + use std::collections::HashMap; #[test] fn test_parse_text_response() -> Result<()> { @@ -1186,12 +1141,12 @@ mod tests { fn test_create_request_adaptive_thinking_for_46_models() -> Result<()> { let _guard = env_lock::lock_env([("GOOSE_THINKING_EFFORT", None::<&str>)]); - let mut params = std::collections::HashMap::new(); + let mut params = HashMap::new(); params.insert("thinking_effort".to_string(), json!("high")); let mut config = cfg("claude-opus-4-6"); config.max_tokens = Some(4096); - config.request_params = Some(params); + config.request_params = Some(¶ms); let messages = vec![Message::user().with_text("Hello")]; let payload = create_request(&config, "system", &messages, &[])?; @@ -1209,7 +1164,8 @@ mod tests { ("ANTHROPIC_PRESERVE_THINKING_CONTEXT", None::<&str>), ]); - let mut config = cfg_with_effort("claude-3-7-sonnet-20250219", "high"); + let params = effort_params("high"); + let mut config = cfg_with_params("claude-3-7-sonnet-20250219", ¶ms); config.max_tokens = Some(4096); let messages = vec![Message::user().with_text("Hello")]; @@ -1230,7 +1186,8 @@ mod tests { ("ANTHROPIC_PRESERVE_THINKING_CONTEXT", None::<&str>), ]); - let config = cfg_with_effort("claude-sonnet-4-20250514", "off"); + let params = effort_params("off"); + let config = cfg_with_params("claude-sonnet-4-20250514", ¶ms); let messages = vec![Message::user().with_text("Hello")]; let payload = create_request(&config, "system", &messages, &[])?; @@ -1297,7 +1254,7 @@ mod tests { params.insert("preserve_thinking_context".to_string(), json!(true)); let mut config = cfg("glm-4.7"); - config.request_params = Some(params); + config.request_params = Some(¶ms); let messages = vec![ Message::assistant().with_content(MessageContent::thinking("internal", "")), Message::user().with_text("Continue"), @@ -1490,18 +1447,25 @@ mod tests { assert_eq!(input, &json!({})); } - fn cfg(name: &str) -> ModelConfig { - ModelConfig { - model_name: name.to_string(), + fn cfg<'a>(name: &'a str) -> ModelConfigParams<'a> { + ModelConfigParams { + model_name: name, ..Default::default() } } - fn cfg_with_effort(name: &str, effort: &str) -> ModelConfig { + fn effort_params(effort: &str) -> HashMap { let mut params = std::collections::HashMap::new(); params.insert("thinking_effort".to_string(), json!(effort)); - ModelConfig { - model_name: name.to_string(), + params + } + + fn cfg_with_params<'a>( + name: &'a str, + params: &'a HashMap, + ) -> ModelConfigParams<'a> { + ModelConfigParams { + model_name: name, request_params: Some(params), ..Default::default() } @@ -1512,22 +1476,28 @@ mod tests { let _guard = env_lock::lock_env([("GOOSE_THINKING_EFFORT", None::<&str>)]); // Adaptive model with effort → adaptive assert_eq!( - thinking_type(&cfg_with_effort("claude-opus-4-6", "high")), + thinking_type(&cfg_with_params("claude-opus-4-6", &effort_params("high"))), ThinkingType::Adaptive ); // Adaptive model with off → disabled assert_eq!( - thinking_type(&cfg_with_effort("claude-opus-4-6", "off")), + thinking_type(&cfg_with_params("claude-opus-4-6", &effort_params("off"))), ThinkingType::Disabled ); // Non-adaptive Claude with effort → enabled assert_eq!( - thinking_type(&cfg_with_effort("claude-3-7-sonnet-20250219", "high")), + thinking_type(&cfg_with_params( + "claude-3-7-sonnet-20250219", + &effort_params("high") + )), ThinkingType::Enabled ); // Non-adaptive Claude with off → disabled assert_eq!( - thinking_type(&cfg_with_effort("claude-3-7-sonnet-20250219", "off")), + thinking_type(&cfg_with_params( + "claude-3-7-sonnet-20250219", + &effort_params("off") + )), ThinkingType::Disabled ); } @@ -1539,18 +1509,23 @@ mod tests { ("ANTHROPIC_THINKING_BUDGET", Some("8192")), ("CLAUDE_THINKING_BUDGET", None::<&str>), ]); - let config = cfg_with_effort("claude-3-7-sonnet-20250219", "high"); - assert_eq!(thinking_budget_tokens(&config), 8192); + assert_eq!( + thinking_budget_tokens(&cfg_with_params( + "claude-3-7-sonnet-20250219", + &effort_params("high") + )), + 8192 + ); } #[test] fn test_thinking_type_non_claude_always_disabled() { assert_eq!( - thinking_type(&cfg_with_effort("gpt-4o", "off")), + thinking_type(&cfg_with_params("gpt-4o", &effort_params("off"))), ThinkingType::Disabled ); assert_eq!( - thinking_type(&cfg_with_effort("gpt-4o", "high")), + thinking_type(&cfg_with_params("gpt-4o", &effort_params("high"))), ThinkingType::Disabled ); } @@ -1558,11 +1533,14 @@ mod tests { #[test] fn test_thinking_type_off_means_disabled() { assert_eq!( - thinking_type(&cfg_with_effort("claude-opus-4-6", "off")), + thinking_type(&cfg_with_params("claude-opus-4-6", &effort_params("off"))), ThinkingType::Disabled ); assert_eq!( - thinking_type(&cfg_with_effort("claude-3-7-sonnet-20250219", "off")), + thinking_type(&cfg_with_params( + "claude-3-7-sonnet-20250219", + &effort_params("off") + )), ThinkingType::Disabled ); } diff --git a/crates/goose-providers/src/models.rs b/crates/goose-providers/src/models.rs index 2fce4a4c93..24bdd9c293 100644 --- a/crates/goose-providers/src/models.rs +++ b/crates/goose-providers/src/models.rs @@ -4,6 +4,9 @@ use serde_json::Value; use crate::thinking::ThinkingEffort; +const DEFAULT_MAX_OUTPUT_TOKENS: i32 = 8192; + +#[derive(Default)] pub struct ModelConfigParams<'a> { pub model_name: &'a str, pub thinking_effort: Option, @@ -11,3 +14,9 @@ pub struct ModelConfigParams<'a> { pub max_tokens: Option, pub request_params: Option<&'a HashMap>, } + +impl ModelConfigParams<'_> { + pub fn max_output_tokens(&'_ self) -> i32 { + self.max_tokens.unwrap_or(DEFAULT_MAX_OUTPUT_TOKENS) + } +} diff --git a/crates/goose/src/model.rs b/crates/goose/src/model.rs index 739777cad8..06932192d7 100644 --- a/crates/goose/src/model.rs +++ b/crates/goose/src/model.rs @@ -1,4 +1,5 @@ use goose_providers::formats::openai::{extract_reasoning_effort, is_openai_responses_model}; +use goose_providers::models::ModelConfigParams; use goose_providers::thinking::ThinkingEffort; use once_cell::sync::Lazy; use serde::de::Deserializer; @@ -494,6 +495,16 @@ impl ModelConfig { ModelConfig::new(model_name) .unwrap_or_else(|_| panic!("Failed to create model config for {}", model_name)) } + + pub fn as_config_params<'a>(&'a self) -> ModelConfigParams<'a> { + ModelConfigParams { + model_name: self.model_name.as_str(), + thinking_effort: self.thinking_effort(), + temperature: self.temperature, + max_tokens: self.max_tokens, + request_params: self.request_params.as_ref(), + } + } } #[cfg(test)] diff --git a/crates/goose/src/providers/anthropic.rs b/crates/goose/src/providers/anthropic.rs index 37f1290578..f272d1a022 100644 --- a/crates/goose/src/providers/anthropic.rs +++ b/crates/goose/src/providers/anthropic.rs @@ -11,10 +11,6 @@ use tokio_util::io::StreamReader; use super::api_client::{ApiClient, AuthMethod}; use super::base::{ConfigKey, MessageStream, ModelInfo, Provider, ProviderDef, ProviderMetadata}; -use super::formats::anthropic::{ - create_request_with_options, response_to_streaming_message, thinking_type, - AnthropicFormatOptions, ThinkingType, -}; use super::inventory::{config_secret_value, serialize_string_map, InventoryIdentityInput}; use super::openai_compatible::handle_status; use super::openai_compatible::map_http_error_to_provider_error; @@ -24,6 +20,9 @@ use crate::conversation::message::Message; use crate::model::ModelConfig; use crate::providers::utils::RequestLog; use futures::future::BoxFuture; +use goose_providers::formats::anthropic::{ + create_request_with_options, response_to_streaming_message, AnthropicFormatOptions, +}; use rmcp::model::Tool; const ANTHROPIC_PROVIDER_NAME: &str = "anthropic"; @@ -177,19 +176,6 @@ impl AnthropicProvider { } } - fn get_conditional_headers(&self) -> Vec<(&str, &str)> { - let mut headers = Vec::new(); - - if self.model.model_name.starts_with("claude-3-7-sonnet-") { - if thinking_type(&self.model) == ThinkingType::Enabled { - headers.push(("anthropic-beta", "output-128k-2025-02-19")); - } - headers.push(("anthropic-beta", "token-efficient-tools-2025-02-19")); - } - - headers - } - async fn fetch_models_from_api(&self) -> Result, ProviderError> { let response = self.api_client.request(None, "v1/models").api_get().await?; @@ -226,6 +212,27 @@ impl AnthropicProvider { models.sort(); Ok(models) } + + fn format_options(&self, model_config: &ModelConfig) -> AnthropicFormatOptions { + let preserve_thinking_context = model_config + .get_config_param::( + "preserve_thinking_context", + "ANTHROPIC_PRESERVE_THINKING_CONTEXT", + ) + .unwrap_or(self.format_options.preserve_thinking_context); + let preserve_unsigned_thinking = model_config + .get_config_param::( + "preserve_unsigned_thinking", + "ANTHROPIC_PRESERVE_UNSIGNED_THINKING", + ) + .unwrap_or(self.format_options.preserve_unsigned_thinking) + || preserve_thinking_context; + + AnthropicFormatOptions { + preserve_unsigned_thinking, + preserve_thinking_context, + } + } } impl ProviderDef for AnthropicProvider { @@ -342,26 +349,22 @@ impl Provider for AnthropicProvider { tools: &[Tool], ) -> Result { let mut payload = create_request_with_options( - model_config, + &model_config.as_config_params(), system, messages, tools, - self.format_options, + self.format_options(model_config), )?; payload .as_object_mut() .unwrap() .insert("stream".to_string(), Value::Bool(true)); - let conditional_headers = self.get_conditional_headers(); let mut log = RequestLog::start(model_config, &payload)?; let response = self .with_retry(|| async { - let mut request = self.api_client.request(Some(session_id), "v1/messages"); - for (key, value) in &conditional_headers { - request = request.header(key, value)?; - } + let request = self.api_client.request(Some(session_id), "v1/messages"); let resp = request.response_post(&payload).await?; handle_status(resp).await }) diff --git a/crates/goose/src/providers/databricks_v2.rs b/crates/goose/src/providers/databricks_v2.rs index 1afe47188f..db03684c41 100644 --- a/crates/goose/src/providers/databricks_v2.rs +++ b/crates/goose/src/providers/databricks_v2.rs @@ -20,7 +20,7 @@ use super::base::{ DEFAULT_PROVIDER_TIMEOUT_SECS, }; use super::databricks_auth::{DatabricksAuth, DatabricksAuthProvider}; -use super::formats::{anthropic, openai_responses}; +use super::formats::openai_responses; use super::openai_compatible::{handle_status, stream_openai_compat, stream_responses_compat}; use super::retry::ProviderRetry; use super::utils::RequestLog; @@ -32,6 +32,7 @@ use crate::providers::retry::{ DEFAULT_MAX_RETRIES, DEFAULT_MAX_RETRY_INTERVAL_MS, }; use goose_providers::errors::ProviderError; +use goose_providers::formats::anthropic; use rmcp::model::Tool; const DATABRICKS_V2_PROVIDER_NAME: &str = "databricks_v2"; @@ -296,7 +297,8 @@ impl DatabricksV2Provider { messages: &[Message], tools: &[Tool], ) -> Result { - let mut payload = anthropic::create_request(model_config, system, messages, tools)?; + let mut payload = + anthropic::create_request(&model_config.as_config_params(), system, messages, tools)?; payload["stream"] = Value::Bool(true); let mut log = RequestLog::start(model_config, &payload)?; diff --git a/crates/goose/src/providers/formats/databricks.rs b/crates/goose/src/providers/formats/databricks.rs index aaa352ae6c..c3e77da9a8 100644 --- a/crates/goose/src/providers/formats/databricks.rs +++ b/crates/goose/src/providers/formats/databricks.rs @@ -1,6 +1,6 @@ use crate::conversation::message::{Message, MessageContent}; use crate::model::ModelConfig; -use crate::providers::formats::anthropic::{ +use goose_providers::formats::anthropic::{ thinking_budget_tokens, thinking_effort, thinking_type, ThinkingType, }; @@ -246,12 +246,12 @@ fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec { obj.insert("thinking".to_string(), json!({ "type": "adaptive" })); obj.insert( "output_config".to_string(), - json!({ "effort": thinking_effort(model_config).to_string() }), + json!({ "effort": thinking_effort(&model_config.as_config_params()).to_string() }), ); obj.insert( "max_completion_tokens".to_string(), @@ -259,7 +259,7 @@ fn apply_claude_thinking_config(payload: &mut Value, model_config: &ModelConfig) ); } ThinkingType::Enabled => { - let budget_tokens = thinking_budget_tokens(model_config); + let budget_tokens = thinking_budget_tokens(&model_config.as_config_params()); let max_tokens = model_config.max_output_tokens() + budget_tokens; obj.insert("max_tokens".to_string(), json!(max_tokens)); obj.insert( diff --git a/crates/goose/src/providers/formats/gcpvertexai.rs b/crates/goose/src/providers/formats/gcpvertexai.rs index 6e064eee0e..7419ac1e30 100644 --- a/crates/goose/src/providers/formats/gcpvertexai.rs +++ b/crates/goose/src/providers/formats/gcpvertexai.rs @@ -1,8 +1,9 @@ -use super::{anthropic, google}; +use super::google; use crate::conversation::message::Message; use crate::model::ModelConfig; use anyhow::{Context, Result}; use goose_providers::conversation::token_usage::{ProviderUsage, Usage}; +use goose_providers::formats::anthropic; use rmcp::model::Tool; use serde_json::Value; @@ -220,7 +221,8 @@ fn create_anthropic_request( messages: &[Message], tools: &[Tool], ) -> Result { - let mut request = anthropic::create_request(model_config, system, messages, tools)?; + let mut request = + anthropic::create_request(&model_config.as_config_params(), system, messages, tools)?; let obj = request .as_object_mut() diff --git a/crates/goose/src/providers/formats/mod.rs b/crates/goose/src/providers/formats/mod.rs index abfbf25a60..f6ad1d4a7e 100644 --- a/crates/goose/src/providers/formats/mod.rs +++ b/crates/goose/src/providers/formats/mod.rs @@ -1,4 +1,3 @@ -pub mod anthropic; #[cfg(feature = "aws-providers")] pub mod bedrock; pub mod databricks; diff --git a/crates/goose/src/providers/kimicode.rs b/crates/goose/src/providers/kimicode.rs index 5cb6e42754..9d32f380fc 100644 --- a/crates/goose/src/providers/kimicode.rs +++ b/crates/goose/src/providers/kimicode.rs @@ -20,7 +20,6 @@ use super::base::{ ConfigKey, MessageStream, Provider, ProviderDef, ProviderMetadata, DEFAULT_PROVIDER_TIMEOUT_SECS, }; -use super::formats::anthropic::{create_request, response_to_streaming_message}; use super::oauth_device_flow::{ refresh_device_flow_token, run_device_flow, DeviceFlowConfig, DeviceFlowTokens, RequestEncoding, }; @@ -31,6 +30,7 @@ use crate::conversation::message::Message; use crate::model::ModelConfig; use futures::future::BoxFuture; use goose_providers::errors::ProviderError; +use goose_providers::formats::anthropic::{create_request, response_to_streaming_message}; use rmcp::model::Tool; const KIMI_CODE_PROVIDER_NAME: &str = "kimi_code"; @@ -393,7 +393,7 @@ impl Provider for KimiCodeProvider { messages: &[Message], tools: &[Tool], ) -> Result { - let mut payload = create_request(model_config, system, messages, tools) + let mut payload = create_request(&model_config.as_config_params(), system, messages, tools) .map_err(|e| ProviderError::RequestFailed(e.to_string()))?; payload .as_object_mut()