config: default agent settings and provider override
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>,
|
||||
|
||||
|
||||
@@ -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
@@ -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.");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user