move formats/anthropic

This commit is contained in:
Jack Amadeo
2026-06-08 16:03:51 -04:00
parent 1fe7ac7386
commit 10e59c223e
12 changed files with 124 additions and 117 deletions
Generated
+1
View File
@@ -4773,6 +4773,7 @@ dependencies = [
"async-stream",
"base64 0.22.1",
"chrono",
"env-lock",
"futures",
"once_cell",
"regex",
+1
View File
@@ -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 }
+1
View File
@@ -1 +1,2 @@
pub mod anthropic;
pub mod openai;
@@ -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::<bool>(
"preserve_thinking_context",
"ANTHROPIC_PRESERVE_THINKING_CONTEXT",
)
.unwrap_or(self.preserve_thinking_context);
let preserve_unsigned_thinking = model_config
.get_config_param::<bool>(
"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<Usage> {
}
}
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<i32> {
let config = crate::config::Config::global();
for key in ["ANTHROPIC_THINKING_BUDGET", "CLAUDE_THINKING_BUDGET"] {
if let Ok(budget) = config.get_param::<i32>(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<Value> {
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(&params);
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", &params);
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", &params);
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(&params);
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<String, Value> {
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<String, Value>,
) -> 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
);
}
+9
View File
@@ -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<ThinkingEffort>,
@@ -11,3 +14,9 @@ pub struct ModelConfigParams<'a> {
pub max_tokens: Option<i32>,
pub request_params: Option<&'a HashMap<String, Value>>,
}
impl ModelConfigParams<'_> {
pub fn max_output_tokens(&'_ self) -> i32 {
self.max_tokens.unwrap_or(DEFAULT_MAX_OUTPUT_TOKENS)
}
}
+11
View File
@@ -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)]
+27 -24
View File
@@ -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<Vec<String>, 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::<bool>(
"preserve_thinking_context",
"ANTHROPIC_PRESERVE_THINKING_CONTEXT",
)
.unwrap_or(self.format_options.preserve_thinking_context);
let preserve_unsigned_thinking = model_config
.get_config_param::<bool>(
"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<MessageStream, ProviderError> {
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
})
+4 -2
View File
@@ -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<MessageStream, ProviderError> {
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)?;
@@ -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<Data
fn apply_claude_thinking_config(payload: &mut Value, model_config: &ModelConfig) {
let obj = payload.as_object_mut().unwrap();
match thinking_type(model_config) {
match thinking_type(&model_config.as_config_params()) {
ThinkingType::Adaptive => {
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(
@@ -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<Value> {
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()
@@ -1,4 +1,3 @@
pub mod anthropic;
#[cfg(feature = "aws-providers")]
pub mod bedrock;
pub mod databricks;
+2 -2
View File
@@ -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<MessageStream, ProviderError> {
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()