config: default agent settings and provider override

This commit is contained in:
Dhanji R. Prasanna
2026-01-14 20:14:33 +05:30
parent 38828c7757
commit f4562cd4c9
5 changed files with 595 additions and 396 deletions

View File

@@ -1,125 +1,82 @@
# g3 Configuration Example
#
# This file demonstrates the new provider configuration format.
# Provider references use the format: "<provider_type>.<config_name>"
# Most settings have sensible defaults. A minimal config only needs:
#
# [providers]
# default_provider = "anthropic.default"
#
# [providers.anthropic.default]
# api_key = "your-api-key"
# model = "claude-sonnet-4-5"
#
# Everything else below is optional.
[providers]
# Default provider used when no specific provider is specified
default_provider = "anthropic.default"
# Optional: Specify different providers for each mode
# If not specified, these fall back to default_provider
# planner = "anthropic.planner" # Provider for planning mode
# coach = "anthropic.default" # Provider for coach (code reviewer) in autonomous mode
# player = "anthropic.default" # Provider for player (code implementer) in autonomous mode
# coach = "anthropic.default" # Provider for coach in autonomous mode
# player = "anthropic.default" # Provider for player in autonomous mode
# Named Anthropic configurations
[providers.anthropic.default]
api_key = "your-anthropic-api-key"
model = "claude-sonnet-4-5"
max_tokens = 64000
temperature = 0.3
# max_tokens = 64000 # Optional (default: provider's max)
# temperature = 0.3 # Optional
# cache_config = "ephemeral" # Optional: Enable prompt caching
# enable_1m_context = true # Optional: Enable 1M context (costs extra)
# thinking_budget_tokens = 10000 # Optional: Enable extended thinking mode
# enable_1m_context = true # Optional: Enable 1M context (costs extra)
# thinking_budget_tokens = 10000 # Optional: Enable extended thinking mode
# Example: A separate config for planning mode with a more capable model
# [providers.anthropic.planner]
# api_key = "your-anthropic-api-key"
# model = "claude-opus-4-5"
# max_tokens = 64000
# thinking_budget_tokens = 16000
# Named Databricks configurations
[providers.databricks.default]
host = "https://your-workspace.cloud.databricks.com"
# token = "your-databricks-token" # Optional - will use OAuth if not provided
model = "databricks-claude-sonnet-4"
max_tokens = 4096
temperature = 0.1
use_oauth = true
# Databricks provider example
# [providers.databricks.default]
# host = "https://your-workspace.cloud.databricks.com"
# model = "databricks-claude-sonnet-4"
# use_oauth = true
# Named OpenAI configurations
# OpenAI provider example
# [providers.openai.default]
# api_key = "your-openai-api-key"
# model = "gpt-4-turbo"
# max_tokens = 4096
# temperature = 0.1
# Multiple OpenAI-compatible providers can be configured
# OpenAI-compatible providers (OpenRouter, Groq, etc.)
# [providers.openai_compatible.openrouter]
# api_key = "your-openrouter-api-key"
# model = "anthropic/claude-3.5-sonnet"
# base_url = "https://openrouter.ai/api/v1"
# max_tokens = 4096
# temperature = 0.1
# [providers.openai_compatible.groq]
# api_key = "your-groq-api-key"
# model = "llama-3.3-70b-versatile"
# base_url = "https://api.groq.com/openai/v1"
# max_tokens = 4096
# temperature = 0.1
# =============================================================================
# Agent settings (all optional - these are the defaults)
# =============================================================================
# [agent]
# fallback_default_max_tokens = 8192
# enable_streaming = true
# timeout_seconds = 120
# auto_compact = true
# max_retry_attempts = 3
# autonomous_max_retry_attempts = 6
# max_context_length = 200000 # Override context window size
[agent]
fallback_default_max_tokens = 8192
# max_context_length: Override the context window size for all providers
# This is the total size of conversation history, not per-request output limit
# max_context_length = 200000
enable_streaming = true
timeout_seconds = 60
max_retry_attempts = 3
autonomous_max_retry_attempts = 6
allow_multiple_tool_calls = true
# =============================================================================
# Computer control (all optional - enabled by default)
# =============================================================================
# [computer_control]
# enabled = true # Requires OS accessibility permissions
# require_confirmation = true
# max_actions_per_second = 5
# Retry Configuration for Planning/Autonomous Mode
#
# The retry infrastructure handles transient errors during LLM API calls:
# - Rate limits (HTTP 429)
# - Network errors (connection failures)
# - Server errors (HTTP 5xx)
# - Request timeouts
# - Model capacity issues (model busy)
#
# Default retry behavior:
# - max_retry_attempts: Used by default interactive mode (3 retries)
# - autonomous_max_retry_attempts: Used by planning/autonomous mode (6 retries)
#
# Note: The retry logic uses exponential backoff with longer delays in
# autonomous mode to handle rate limits gracefully.
#
# Example player retry config (in code):
# RetryConfig::planning("player") # Creates: max_retries=3, is_autonomous=true
# RetryConfig::planning("player").with_max_retries(6) # Override max retries
#
# Example coach retry config (in code):
# RetryConfig::planning("coach") # Creates: max_retries=3, is_autonomous=true
# RetryConfig::planning("coach").with_max_retries(6) # Override max retries
#
[computer_control]
enabled = false # Set to true to enable computer control (requires OS permissions)
require_confirmation = true
max_actions_per_second = 5
[webdriver]
enabled = false
safari_port = 4444
chrome_port = 9515
# Browser to use: "safari" (default) or "chrome-headless"
# Safari opens a visible browser window
# Chrome headless runs in the background without a visible window
browser = "safari"
# Optional: Path to Chrome binary (e.g., Chrome for Testing)
# If not set, ChromeDriver will use the default Chrome installation
# Use this to avoid version mismatch issues between Chrome and ChromeDriver
# Run: ./scripts/setup-chrome-for-testing.sh to install matching versions
# chrome_binary = "/Users/yourname/.chrome-for-testing/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"
# chrome_binary = "/Users/yourname/.chrome-for-testing/chrome-mac-x64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"
# Optional: Path to ChromeDriver binary
# If not set, looks for 'chromedriver' in PATH
# The setup script creates a symlink at ~/.local/bin/chromedriver
# chromedriver_binary = "/Users/yourname/.local/bin/chromedriver"
[macax]
enabled = false
# =============================================================================
# WebDriver browser automation (all optional)
# =============================================================================
# [webdriver]
# enabled = true
# browser = "chrome-headless" # Default. Alternative: "safari"
# chrome_binary = "/path/to/chrome" # Optional: custom Chrome path
# chromedriver_binary = "/path/to/driver" # Optional: custom ChromeDriver path

View File

@@ -55,7 +55,7 @@ pub struct Cli {
#[arg(long)]
pub chat: bool,
/// Override the configured provider (anthropic, databricks, embedded, openai)
/// Override the configured provider (e.g., 'openai' or 'openai.default')
#[arg(long, value_name = "PROVIDER")]
pub provider: Option<String>,

View File

@@ -7,8 +7,11 @@ use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub providers: ProvidersConfig,
#[serde(default)]
pub agent: AgentConfig,
#[serde(default)]
pub computer_control: ComputerControlConfig,
#[serde(default)]
pub webdriver: WebDriverConfig,
}
@@ -17,32 +20,32 @@ pub struct Config {
pub struct ProvidersConfig {
/// Default provider in format "<provider_type>.<config_name>"
pub default_provider: String,
/// Provider for planner mode (optional, falls back to default_provider)
pub planner: Option<String>,
/// Provider for coach in autonomous mode (optional, falls back to default_provider)
pub coach: Option<String>,
/// Provider for player in autonomous mode (optional, falls back to default_provider)
pub player: Option<String>,
/// Named Anthropic provider configs
#[serde(default)]
pub anthropic: HashMap<String, AnthropicConfig>,
/// Named OpenAI provider configs
#[serde(default)]
pub openai: HashMap<String, OpenAIConfig>,
/// Named Databricks provider configs
#[serde(default)]
pub databricks: HashMap<String, DatabricksConfig>,
/// Named embedded provider configs
#[serde(default)]
pub embedded: HashMap<String, EmbeddedConfig>,
/// Multiple named OpenAI-compatible providers (e.g., openrouter, groq, etc.)
#[serde(default)]
pub openai_compatible: HashMap<String, OpenAIConfig>,
@@ -92,24 +95,59 @@ pub struct EmbeddedConfig {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentConfig {
pub max_context_length: Option<u32>,
#[serde(default = "default_fallback_max_tokens")]
pub fallback_default_max_tokens: usize,
#[serde(default = "default_true")]
pub enable_streaming: bool,
#[serde(default = "default_timeout_seconds")]
pub timeout_seconds: u64,
#[serde(default = "default_true")]
pub auto_compact: bool,
#[serde(default = "default_max_retry_attempts")]
pub max_retry_attempts: u32,
#[serde(default = "default_autonomous_max_retry_attempts")]
pub autonomous_max_retry_attempts: u32,
#[serde(default = "default_check_todo_staleness")]
pub check_todo_staleness: bool,
}
fn default_fallback_max_tokens() -> usize {
8192
}
fn default_true() -> bool {
true
}
fn default_false() -> bool {
false
}
fn default_timeout_seconds() -> u64 {
120
}
fn default_max_retry_attempts() -> u32 {
3
}
fn default_autonomous_max_retry_attempts() -> u32 {
6
}
fn default_max_actions_per_second() -> u32 {
5
}
fn default_check_todo_staleness() -> bool {
true
}
fn default_safari_port() -> u16 {
4444
}
fn default_chrome_port() -> u16 {
9515
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComputerControlConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_false")]
pub require_confirmation: bool,
#[serde(default = "default_max_actions_per_second")]
pub max_actions_per_second: u32,
}
@@ -117,17 +155,19 @@ pub struct ComputerControlConfig {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "lowercase")]
pub enum WebDriverBrowser {
#[default]
Safari,
#[default]
#[serde(rename = "chrome-headless")]
ChromeHeadless,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct WebDriverConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_safari_port")]
pub safari_port: u16,
#[serde(default)]
#[serde(default = "default_chrome_port")]
pub chrome_port: u16,
#[serde(default)]
/// Optional path to Chrome binary (e.g., Chrome for Testing)
@@ -141,24 +181,25 @@ pub struct WebDriverConfig {
pub browser: WebDriverBrowser,
}
impl Default for WebDriverConfig {
impl Default for AgentConfig {
fn default() -> Self {
Self {
enabled: true,
safari_port: 4444,
chrome_port: 9515,
chrome_binary: None,
chromedriver_binary: None,
browser: WebDriverBrowser::Safari,
max_context_length: None,
fallback_default_max_tokens: 8192,
enable_streaming: true,
timeout_seconds: 120,
auto_compact: true,
max_retry_attempts: 3,
autonomous_max_retry_attempts: 6,
check_todo_staleness: true,
}
}
}
impl Default for ComputerControlConfig {
fn default() -> Self {
Self {
enabled: false,
require_confirmation: true,
enabled: true,
require_confirmation: false,
max_actions_per_second: 5,
}
}
@@ -296,17 +337,17 @@ impl Config {
if let Some(path) = config_path_to_load {
// Read and parse the config file
let config_content = std::fs::read_to_string(&path)?;
// Check for old format (direct provider config without named configs)
if Self::is_old_format(&config_content) {
anyhow::bail!("{}", OLD_CONFIG_FORMAT_ERROR);
}
let config: Config = toml::from_str(&config_content)?;
// Validate the default_provider format
config.validate_provider_reference(&config.providers.default_provider)?;
return Ok(config);
}
@@ -317,7 +358,7 @@ impl Config {
fn is_old_format(content: &str) -> bool {
// Old format has [providers.anthropic] with api_key directly
// New format has [providers.anthropic.<name>] with api_key
// Parse as TOML value to inspect structure
if let Ok(value) = content.parse::<toml::Value>() {
if let Some(providers) = value.get("providers") {
@@ -445,20 +486,26 @@ impl Config {
// Apply provider override
if let Some(provider) = provider_override {
// Validate the override
// If provider doesn't contain '.', assume '.default'
let provider = if provider.contains('.') {
provider
} else {
format!("{}.default", provider)
};
config.validate_provider_reference(&provider)?;
config.providers.default_provider = provider;
}
// Apply model override to the active provider
if let Some(model) = model_override {
let (provider_type, config_name) = Self::parse_provider_reference(
&config.providers.default_provider
)?;
let (provider_type, config_name) =
Self::parse_provider_reference(&config.providers.default_provider)?;
match provider_type.as_str() {
"anthropic" => {
if let Some(ref mut anthropic_config) = config.providers.anthropic.get_mut(&config_name) {
if let Some(ref mut anthropic_config) =
config.providers.anthropic.get_mut(&config_name)
{
anthropic_config.model = model;
} else {
return Err(anyhow::anyhow!(
@@ -468,7 +515,9 @@ impl Config {
}
}
"databricks" => {
if let Some(ref mut databricks_config) = config.providers.databricks.get_mut(&config_name) {
if let Some(ref mut databricks_config) =
config.providers.databricks.get_mut(&config_name)
{
databricks_config.model = model;
} else {
return Err(anyhow::anyhow!(
@@ -478,7 +527,9 @@ impl Config {
}
}
"embedded" => {
if let Some(ref mut embedded_config) = config.providers.embedded.get_mut(&config_name) {
if let Some(ref mut embedded_config) =
config.providers.embedded.get_mut(&config_name)
{
embedded_config.model_path = model;
} else {
return Err(anyhow::anyhow!(
@@ -488,7 +539,9 @@ impl Config {
}
}
"openai" => {
if let Some(ref mut openai_config) = config.providers.openai.get_mut(&config_name) {
if let Some(ref mut openai_config) =
config.providers.openai.get_mut(&config_name)
{
openai_config.model = model;
} else {
return Err(anyhow::anyhow!(
@@ -499,13 +552,12 @@ impl Config {
}
_ => {
// Check openai_compatible
if let Some(ref mut compat_config) = config.providers.openai_compatible.get_mut(&provider_type) {
if let Some(ref mut compat_config) =
config.providers.openai_compatible.get_mut(&provider_type)
{
compat_config.model = model;
} else {
return Err(anyhow::anyhow!(
"Unknown provider type: {}",
provider_type
));
return Err(anyhow::anyhow!("Unknown provider type: {}", provider_type));
}
}
}
@@ -585,36 +637,42 @@ impl Config {
/// Get the current default provider's config
pub fn get_default_provider_config(&self) -> Result<ProviderConfigRef<'_>> {
let (provider_type, config_name) = Self::parse_provider_reference(
&self.providers.default_provider
)?;
let (provider_type, config_name) =
Self::parse_provider_reference(&self.providers.default_provider)?;
match provider_type.as_str() {
"anthropic" => {
self.providers.anthropic.get(&config_name)
.map(ProviderConfigRef::Anthropic)
.ok_or_else(|| anyhow::anyhow!("Anthropic config '{}' not found", config_name))
}
"openai" => {
self.providers.openai.get(&config_name)
.map(ProviderConfigRef::OpenAI)
.ok_or_else(|| anyhow::anyhow!("OpenAI config '{}' not found", config_name))
}
"databricks" => {
self.providers.databricks.get(&config_name)
.map(ProviderConfigRef::Databricks)
.ok_or_else(|| anyhow::anyhow!("Databricks config '{}' not found", config_name))
}
"embedded" => {
self.providers.embedded.get(&config_name)
.map(ProviderConfigRef::Embedded)
.ok_or_else(|| anyhow::anyhow!("Embedded config '{}' not found", config_name))
}
_ => {
self.providers.openai_compatible.get(&provider_type)
.map(ProviderConfigRef::OpenAICompatible)
.ok_or_else(|| anyhow::anyhow!("OpenAI compatible config '{}' not found", provider_type))
}
"anthropic" => self
.providers
.anthropic
.get(&config_name)
.map(ProviderConfigRef::Anthropic)
.ok_or_else(|| anyhow::anyhow!("Anthropic config '{}' not found", config_name)),
"openai" => self
.providers
.openai
.get(&config_name)
.map(ProviderConfigRef::OpenAI)
.ok_or_else(|| anyhow::anyhow!("OpenAI config '{}' not found", config_name)),
"databricks" => self
.providers
.databricks
.get(&config_name)
.map(ProviderConfigRef::Databricks)
.ok_or_else(|| anyhow::anyhow!("Databricks config '{}' not found", config_name)),
"embedded" => self
.providers
.embedded
.get(&config_name)
.map(ProviderConfigRef::Embedded)
.ok_or_else(|| anyhow::anyhow!("Embedded config '{}' not found", config_name)),
_ => self
.providers
.openai_compatible
.get(&provider_type)
.map(ProviderConfigRef::OpenAICompatible)
.ok_or_else(|| {
anyhow::anyhow!("OpenAI compatible config '{}' not found", provider_type)
}),
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -110,9 +110,12 @@ use tokio_stream::wrappers::ReceiverStream;
use tracing::{debug, error};
use crate::{
streaming::{
decode_utf8_streaming, make_final_chunk, make_final_chunk_with_reason, make_text_chunk,
make_tool_chunk,
},
CompletionChunk, CompletionRequest, CompletionResponse, CompletionStream, LLMProvider, Message,
MessageRole, Tool, ToolCall, Usage,
streaming::{decode_utf8_streaming, make_final_chunk, make_final_chunk_with_reason, make_text_chunk, make_tool_chunk},
};
const ANTHROPIC_API_URL: &str = "https://api.anthropic.com/v1/messages";
@@ -156,7 +159,7 @@ impl AnthropicProvider {
name: "anthropic".to_string(),
api_key,
model,
max_tokens: max_tokens.unwrap_or(4096),
max_tokens: max_tokens.unwrap_or(32768),
temperature: temperature.unwrap_or(0.1),
cache_config,
enable_1m_context: enable_1m_context.unwrap_or(false),
@@ -182,14 +185,17 @@ impl AnthropicProvider {
let model = model.unwrap_or_else(|| "claude-3-5-sonnet-20241022".to_string());
debug!("Initialized Anthropic provider '{}' with model: {}", name, model);
debug!(
"Initialized Anthropic provider '{}' with model: {}",
name, model
);
Ok(Self {
client,
name,
api_key,
model,
max_tokens: max_tokens.unwrap_or(4096),
max_tokens: max_tokens.unwrap_or(32768),
temperature: temperature.unwrap_or(0.1),
cache_config,
enable_1m_context: enable_1m_context.unwrap_or(false),
@@ -277,7 +283,7 @@ impl AnthropicProvider {
MessageRole::User => {
// Build content blocks - images first, then text
let mut content_blocks: Vec<AnthropicContent> = Vec::new();
// Add any images attached to this message
for image in &message.images {
content_blocks.push(AnthropicContent::Image {
@@ -288,13 +294,16 @@ impl AnthropicProvider {
},
});
}
// Add text content
content_blocks.push(AnthropicContent::Text {
text: message.content.clone(),
cache_control: message.cache_control.as_ref().map(Self::convert_cache_control),
cache_control: message
.cache_control
.as_ref()
.map(Self::convert_cache_control),
});
anthropic_messages.push(AnthropicMessage {
role: "user".to_string(),
content: content_blocks,
@@ -427,7 +436,10 @@ impl AnthropicProvider {
if let Some(data) = line.strip_prefix("data: ") {
if data == "[DONE]" {
debug!("Received stream completion marker");
let final_chunk = make_final_chunk(current_tool_calls.clone(), accumulated_usage.clone());
let final_chunk = make_final_chunk(
current_tool_calls.clone(),
accumulated_usage.clone(),
);
if tx.send(Ok(final_chunk)).await.is_err() {
debug!("Receiver dropped, stopping stream");
}
@@ -491,7 +503,8 @@ impl AnthropicProvider {
{
// We have complete arguments, send the tool call immediately
debug!("Tool call has complete args, sending immediately: {:?}", tool_call);
let chunk = make_tool_chunk(vec![tool_call]);
let chunk =
make_tool_chunk(vec![tool_call]);
if tx.send(Ok(chunk)).await.is_err() {
debug!("Receiver dropped, stopping stream");
return accumulated_usage;
@@ -575,7 +588,8 @@ impl AnthropicProvider {
// Send the complete tool call
if !current_tool_calls.is_empty() {
let chunk = make_tool_chunk(current_tool_calls.clone());
let chunk =
make_tool_chunk(current_tool_calls.clone());
if tx.send(Ok(chunk)).await.is_err() {
debug!("Receiver dropped, stopping stream");
return accumulated_usage;
@@ -597,7 +611,11 @@ impl AnthropicProvider {
"message_stop" => {
debug!("Received message stop event");
message_stopped = true;
let final_chunk = make_final_chunk_with_reason(current_tool_calls.clone(), accumulated_usage.clone(), stop_reason.clone());
let final_chunk = make_final_chunk_with_reason(
current_tool_calls.clone(),
accumulated_usage.clone(),
stop_reason.clone(),
);
if tx.send(Ok(final_chunk)).await.is_err() {
debug!("Receiver dropped, stopping stream");
}
@@ -826,7 +844,10 @@ struct ThinkingConfig {
impl ThinkingConfig {
fn enabled(budget_tokens: u32) -> Self {
Self { thinking_type: "enabled".to_string(), budget_tokens }
Self {
thinking_type: "enabled".to_string(),
budget_tokens,
}
}
}
@@ -889,9 +910,7 @@ enum AnthropicContent {
input: serde_json::Value,
},
#[serde(rename = "image")]
Image {
source: AnthropicImageSource,
},
Image { source: AnthropicImageSource },
}
/// Image source for Anthropic API
@@ -899,8 +918,8 @@ enum AnthropicContent {
struct AnthropicImageSource {
#[serde(rename = "type")]
source_type: String, // Always "base64"
media_type: String, // e.g., "image/png", "image/jpeg"
data: String, // Base64-encoded image data
media_type: String, // e.g., "image/png", "image/jpeg"
data: String, // Base64-encoded image data
}
#[derive(Debug, Deserialize)]
@@ -962,7 +981,8 @@ mod tests {
#[test]
fn test_message_conversion() {
let provider =
AnthropicProvider::new("test-key".to_string(), None, None, None, None, None, None).unwrap();
AnthropicProvider::new("test-key".to_string(), None, None, None, None, None, None)
.unwrap();
let messages = vec![
Message::new(
@@ -1011,7 +1031,8 @@ mod tests {
#[test]
fn test_tool_conversion() {
let provider =
AnthropicProvider::new("test-key".to_string(), None, None, None, None, None, None).unwrap();
AnthropicProvider::new("test-key".to_string(), None, None, None, None, None, None)
.unwrap();
let tools = vec![Tool {
name: "get_weather".to_string(),
@@ -1044,7 +1065,8 @@ mod tests {
#[test]
fn test_cache_control_serialization() {
let provider =
AnthropicProvider::new("test-key".to_string(), None, None, None, None, None, None).unwrap();
AnthropicProvider::new("test-key".to_string(), None, None, None, None, None, None)
.unwrap();
// Test message WITHOUT cache_control
let messages_without = vec![Message::new(MessageRole::User, "Hello".to_string())];
@@ -1106,14 +1128,17 @@ mod tests {
.create_request_body(&messages, None, false, 1000, 0.5, false)
.unwrap();
let json_without = serde_json::to_string(&request_without).unwrap();
assert!(!json_without.contains("thinking"), "JSON should not contain 'thinking' field when not configured");
assert!(
!json_without.contains("thinking"),
"JSON should not contain 'thinking' field when not configured"
);
// Test WITH thinking parameter - max_tokens must be > budget_tokens + 1024
// Using budget=10000 requires max_tokens > 11024
let provider_with = AnthropicProvider::new(
"test-key".to_string(),
Some("claude-sonnet-4-5".to_string()),
Some(20000), // Sufficient for thinking budget
Some(20000), // Sufficient for thinking budget
Some(0.5),
None,
None,
@@ -1125,16 +1150,28 @@ mod tests {
.create_request_body(&messages, None, false, 20000, 0.5, false)
.unwrap();
let json_with = serde_json::to_string(&request_with).unwrap();
assert!(json_with.contains("thinking"), "JSON should contain 'thinking' field when configured");
assert!(json_with.contains("\"type\":\"enabled\""), "JSON should contain type: enabled");
assert!(json_with.contains("\"budget_tokens\":10000"), "JSON should contain budget_tokens: 10000");
assert!(
json_with.contains("thinking"),
"JSON should contain 'thinking' field when configured"
);
assert!(
json_with.contains("\"type\":\"enabled\""),
"JSON should contain type: enabled"
);
assert!(
json_with.contains("\"budget_tokens\":10000"),
"JSON should contain budget_tokens: 10000"
);
// Test WITH thinking parameter but INSUFFICIENT max_tokens - thinking should be disabled
let request_insufficient = provider_with
.create_request_body(&messages, None, false, 5000, 0.5, false) // Less than budget + 1024
.create_request_body(&messages, None, false, 5000, 0.5, false) // Less than budget + 1024
.unwrap();
let json_insufficient = serde_json::to_string(&request_insufficient).unwrap();
assert!(!json_insufficient.contains("thinking"), "JSON should NOT contain 'thinking' field when max_tokens is insufficient");
assert!(
!json_insufficient.contains("thinking"),
"JSON should NOT contain 'thinking' field when max_tokens is insufficient"
);
}
#[test]
@@ -1152,20 +1189,26 @@ mod tests {
.unwrap();
let messages = vec![Message::new(MessageRole::User, "Test message".to_string())];
// With disable_thinking=false, thinking should be enabled (max_tokens is sufficient)
let request_with_thinking = provider
.create_request_body(&messages, None, false, 20000, 0.5, false)
.unwrap();
let json_with = serde_json::to_string(&request_with_thinking).unwrap();
assert!(json_with.contains("thinking"), "JSON should contain 'thinking' field when not disabled");
assert!(
json_with.contains("thinking"),
"JSON should contain 'thinking' field when not disabled"
);
// With disable_thinking=true, thinking should be disabled even with sufficient max_tokens
let request_without_thinking = provider
.create_request_body(&messages, None, false, 20000, 0.5, true)
.unwrap();
let json_without = serde_json::to_string(&request_without_thinking).unwrap();
assert!(!json_without.contains("thinking"), "JSON should NOT contain 'thinking' field when explicitly disabled");
assert!(
!json_without.contains("thinking"),
"JSON should NOT contain 'thinking' field when explicitly disabled"
);
}
#[test]
@@ -1183,16 +1226,20 @@ mod tests {
let response: AnthropicResponse = serde_json::from_str(json_response)
.expect("Should be able to deserialize response with thinking block");
assert_eq!(response.content.len(), 2);
assert_eq!(response.model, "claude-sonnet-4-5");
// Extract only text content (thinking should be filtered out)
let text_content: Vec<_> = response.content.iter().filter_map(|c| match c {
AnthropicContent::Text { text, .. } => Some(text.as_str()),
_ => None,
}).collect();
let text_content: Vec<_> = response
.content
.iter()
.filter_map(|c| match c {
AnthropicContent::Text { text, .. } => Some(text.as_str()),
_ => None,
})
.collect();
assert_eq!(text_content.len(), 1);
assert_eq!(text_content[0], "Here is my response.");
}