config: default agent settings and provider override
This commit is contained in:
@@ -1,125 +1,82 @@
|
|||||||
# g3 Configuration Example
|
# g3 Configuration Example
|
||||||
#
|
#
|
||||||
# This file demonstrates the new provider configuration format.
|
# Most settings have sensible defaults. A minimal config only needs:
|
||||||
# Provider references use the format: "<provider_type>.<config_name>"
|
#
|
||||||
|
# [providers]
|
||||||
|
# default_provider = "anthropic.default"
|
||||||
|
#
|
||||||
|
# [providers.anthropic.default]
|
||||||
|
# api_key = "your-api-key"
|
||||||
|
# model = "claude-sonnet-4-5"
|
||||||
|
#
|
||||||
|
# Everything else below is optional.
|
||||||
|
|
||||||
[providers]
|
[providers]
|
||||||
# Default provider used when no specific provider is specified
|
|
||||||
default_provider = "anthropic.default"
|
default_provider = "anthropic.default"
|
||||||
|
|
||||||
# Optional: Specify different providers for each mode
|
# Optional: Specify different providers for each mode
|
||||||
# If not specified, these fall back to default_provider
|
# If not specified, these fall back to default_provider
|
||||||
# planner = "anthropic.planner" # Provider for planning mode
|
# planner = "anthropic.planner" # Provider for planning mode
|
||||||
# coach = "anthropic.default" # Provider for coach (code reviewer) in autonomous mode
|
# coach = "anthropic.default" # Provider for coach in autonomous mode
|
||||||
# player = "anthropic.default" # Provider for player (code implementer) in autonomous mode
|
# player = "anthropic.default" # Provider for player in autonomous mode
|
||||||
|
|
||||||
# Named Anthropic configurations
|
|
||||||
[providers.anthropic.default]
|
[providers.anthropic.default]
|
||||||
api_key = "your-anthropic-api-key"
|
api_key = "your-anthropic-api-key"
|
||||||
model = "claude-sonnet-4-5"
|
model = "claude-sonnet-4-5"
|
||||||
max_tokens = 64000
|
# max_tokens = 64000 # Optional (default: provider's max)
|
||||||
temperature = 0.3
|
# temperature = 0.3 # Optional
|
||||||
# cache_config = "ephemeral" # Optional: Enable prompt caching
|
# cache_config = "ephemeral" # Optional: Enable prompt caching
|
||||||
# enable_1m_context = true # Optional: Enable 1M context (costs extra)
|
# enable_1m_context = true # Optional: Enable 1M context (costs extra)
|
||||||
# thinking_budget_tokens = 10000 # Optional: Enable extended thinking mode
|
# thinking_budget_tokens = 10000 # Optional: Enable extended thinking mode
|
||||||
|
|
||||||
# Example: A separate config for planning mode with a more capable model
|
# Example: A separate config for planning mode with a more capable model
|
||||||
# [providers.anthropic.planner]
|
# [providers.anthropic.planner]
|
||||||
# api_key = "your-anthropic-api-key"
|
# api_key = "your-anthropic-api-key"
|
||||||
# model = "claude-opus-4-5"
|
# model = "claude-opus-4-5"
|
||||||
# max_tokens = 64000
|
|
||||||
# thinking_budget_tokens = 16000
|
# thinking_budget_tokens = 16000
|
||||||
|
|
||||||
# Named Databricks configurations
|
# Databricks provider example
|
||||||
[providers.databricks.default]
|
# [providers.databricks.default]
|
||||||
host = "https://your-workspace.cloud.databricks.com"
|
# host = "https://your-workspace.cloud.databricks.com"
|
||||||
# token = "your-databricks-token" # Optional - will use OAuth if not provided
|
# model = "databricks-claude-sonnet-4"
|
||||||
model = "databricks-claude-sonnet-4"
|
# use_oauth = true
|
||||||
max_tokens = 4096
|
|
||||||
temperature = 0.1
|
|
||||||
use_oauth = true
|
|
||||||
|
|
||||||
# Named OpenAI configurations
|
# OpenAI provider example
|
||||||
# [providers.openai.default]
|
# [providers.openai.default]
|
||||||
# api_key = "your-openai-api-key"
|
# api_key = "your-openai-api-key"
|
||||||
# model = "gpt-4-turbo"
|
# 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]
|
# [providers.openai_compatible.openrouter]
|
||||||
# api_key = "your-openrouter-api-key"
|
# api_key = "your-openrouter-api-key"
|
||||||
# model = "anthropic/claude-3.5-sonnet"
|
# model = "anthropic/claude-3.5-sonnet"
|
||||||
# base_url = "https://openrouter.ai/api/v1"
|
# base_url = "https://openrouter.ai/api/v1"
|
||||||
# max_tokens = 4096
|
|
||||||
# temperature = 0.1
|
|
||||||
|
|
||||||
# [providers.openai_compatible.groq]
|
# =============================================================================
|
||||||
# api_key = "your-groq-api-key"
|
# Agent settings (all optional - these are the defaults)
|
||||||
# model = "llama-3.3-70b-versatile"
|
# =============================================================================
|
||||||
# base_url = "https://api.groq.com/openai/v1"
|
# [agent]
|
||||||
# max_tokens = 4096
|
# fallback_default_max_tokens = 8192
|
||||||
# temperature = 0.1
|
# 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
|
# Computer control (all optional - enabled by default)
|
||||||
# max_context_length: Override the context window size for all providers
|
# =============================================================================
|
||||||
# This is the total size of conversation history, not per-request output limit
|
# [computer_control]
|
||||||
# max_context_length = 200000
|
# enabled = true # Requires OS accessibility permissions
|
||||||
enable_streaming = true
|
# require_confirmation = true
|
||||||
timeout_seconds = 60
|
# max_actions_per_second = 5
|
||||||
max_retry_attempts = 3
|
|
||||||
autonomous_max_retry_attempts = 6
|
|
||||||
allow_multiple_tool_calls = true
|
|
||||||
|
|
||||||
# Retry Configuration for Planning/Autonomous Mode
|
# =============================================================================
|
||||||
#
|
# WebDriver browser automation (all optional)
|
||||||
# The retry infrastructure handles transient errors during LLM API calls:
|
# =============================================================================
|
||||||
# - Rate limits (HTTP 429)
|
# [webdriver]
|
||||||
# - Network errors (connection failures)
|
# enabled = true
|
||||||
# - Server errors (HTTP 5xx)
|
# browser = "chrome-headless" # Default. Alternative: "safari"
|
||||||
# - Request timeouts
|
# chrome_binary = "/path/to/chrome" # Optional: custom Chrome path
|
||||||
# - Model capacity issues (model busy)
|
# chromedriver_binary = "/path/to/driver" # Optional: custom ChromeDriver path
|
||||||
#
|
|
||||||
# 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
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ pub struct Cli {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub chat: bool,
|
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")]
|
#[arg(long, value_name = "PROVIDER")]
|
||||||
pub provider: Option<String>,
|
pub provider: Option<String>,
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ use std::path::Path;
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub providers: ProvidersConfig,
|
pub providers: ProvidersConfig,
|
||||||
|
#[serde(default)]
|
||||||
pub agent: AgentConfig,
|
pub agent: AgentConfig,
|
||||||
|
#[serde(default)]
|
||||||
pub computer_control: ComputerControlConfig,
|
pub computer_control: ComputerControlConfig,
|
||||||
|
#[serde(default)]
|
||||||
pub webdriver: WebDriverConfig,
|
pub webdriver: WebDriverConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,32 +20,32 @@ pub struct Config {
|
|||||||
pub struct ProvidersConfig {
|
pub struct ProvidersConfig {
|
||||||
/// Default provider in format "<provider_type>.<config_name>"
|
/// Default provider in format "<provider_type>.<config_name>"
|
||||||
pub default_provider: String,
|
pub default_provider: String,
|
||||||
|
|
||||||
/// Provider for planner mode (optional, falls back to default_provider)
|
/// Provider for planner mode (optional, falls back to default_provider)
|
||||||
pub planner: Option<String>,
|
pub planner: Option<String>,
|
||||||
|
|
||||||
/// Provider for coach in autonomous mode (optional, falls back to default_provider)
|
/// Provider for coach in autonomous mode (optional, falls back to default_provider)
|
||||||
pub coach: Option<String>,
|
pub coach: Option<String>,
|
||||||
|
|
||||||
/// Provider for player in autonomous mode (optional, falls back to default_provider)
|
/// Provider for player in autonomous mode (optional, falls back to default_provider)
|
||||||
pub player: Option<String>,
|
pub player: Option<String>,
|
||||||
|
|
||||||
/// Named Anthropic provider configs
|
/// Named Anthropic provider configs
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub anthropic: HashMap<String, AnthropicConfig>,
|
pub anthropic: HashMap<String, AnthropicConfig>,
|
||||||
|
|
||||||
/// Named OpenAI provider configs
|
/// Named OpenAI provider configs
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub openai: HashMap<String, OpenAIConfig>,
|
pub openai: HashMap<String, OpenAIConfig>,
|
||||||
|
|
||||||
/// Named Databricks provider configs
|
/// Named Databricks provider configs
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub databricks: HashMap<String, DatabricksConfig>,
|
pub databricks: HashMap<String, DatabricksConfig>,
|
||||||
|
|
||||||
/// Named embedded provider configs
|
/// Named embedded provider configs
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub embedded: HashMap<String, EmbeddedConfig>,
|
pub embedded: HashMap<String, EmbeddedConfig>,
|
||||||
|
|
||||||
/// Multiple named OpenAI-compatible providers (e.g., openrouter, groq, etc.)
|
/// Multiple named OpenAI-compatible providers (e.g., openrouter, groq, etc.)
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub openai_compatible: HashMap<String, OpenAIConfig>,
|
pub openai_compatible: HashMap<String, OpenAIConfig>,
|
||||||
@@ -92,24 +95,59 @@ pub struct EmbeddedConfig {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AgentConfig {
|
pub struct AgentConfig {
|
||||||
pub max_context_length: Option<u32>,
|
pub max_context_length: Option<u32>,
|
||||||
|
#[serde(default = "default_fallback_max_tokens")]
|
||||||
pub fallback_default_max_tokens: usize,
|
pub fallback_default_max_tokens: usize,
|
||||||
|
#[serde(default = "default_true")]
|
||||||
pub enable_streaming: bool,
|
pub enable_streaming: bool,
|
||||||
|
#[serde(default = "default_timeout_seconds")]
|
||||||
pub timeout_seconds: u64,
|
pub timeout_seconds: u64,
|
||||||
|
#[serde(default = "default_true")]
|
||||||
pub auto_compact: bool,
|
pub auto_compact: bool,
|
||||||
|
#[serde(default = "default_max_retry_attempts")]
|
||||||
pub max_retry_attempts: u32,
|
pub max_retry_attempts: u32,
|
||||||
|
#[serde(default = "default_autonomous_max_retry_attempts")]
|
||||||
pub autonomous_max_retry_attempts: u32,
|
pub autonomous_max_retry_attempts: u32,
|
||||||
#[serde(default = "default_check_todo_staleness")]
|
#[serde(default = "default_check_todo_staleness")]
|
||||||
pub check_todo_staleness: bool,
|
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 {
|
fn default_check_todo_staleness() -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
fn default_safari_port() -> u16 {
|
||||||
|
4444
|
||||||
|
}
|
||||||
|
fn default_chrome_port() -> u16 {
|
||||||
|
9515
|
||||||
|
}
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ComputerControlConfig {
|
pub struct ComputerControlConfig {
|
||||||
|
#[serde(default = "default_true")]
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
|
#[serde(default = "default_false")]
|
||||||
pub require_confirmation: bool,
|
pub require_confirmation: bool,
|
||||||
|
#[serde(default = "default_max_actions_per_second")]
|
||||||
pub max_actions_per_second: u32,
|
pub max_actions_per_second: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,17 +155,19 @@ pub struct ComputerControlConfig {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum WebDriverBrowser {
|
pub enum WebDriverBrowser {
|
||||||
#[default]
|
|
||||||
Safari,
|
Safari,
|
||||||
|
#[default]
|
||||||
#[serde(rename = "chrome-headless")]
|
#[serde(rename = "chrome-headless")]
|
||||||
ChromeHeadless,
|
ChromeHeadless,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct WebDriverConfig {
|
pub struct WebDriverConfig {
|
||||||
|
#[serde(default = "default_true")]
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
|
#[serde(default = "default_safari_port")]
|
||||||
pub safari_port: u16,
|
pub safari_port: u16,
|
||||||
#[serde(default)]
|
#[serde(default = "default_chrome_port")]
|
||||||
pub chrome_port: u16,
|
pub chrome_port: u16,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
/// Optional path to Chrome binary (e.g., Chrome for Testing)
|
/// Optional path to Chrome binary (e.g., Chrome for Testing)
|
||||||
@@ -141,24 +181,25 @@ pub struct WebDriverConfig {
|
|||||||
pub browser: WebDriverBrowser,
|
pub browser: WebDriverBrowser,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for WebDriverConfig {
|
impl Default for AgentConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
enabled: true,
|
max_context_length: None,
|
||||||
safari_port: 4444,
|
fallback_default_max_tokens: 8192,
|
||||||
chrome_port: 9515,
|
enable_streaming: true,
|
||||||
chrome_binary: None,
|
timeout_seconds: 120,
|
||||||
chromedriver_binary: None,
|
auto_compact: true,
|
||||||
browser: WebDriverBrowser::Safari,
|
max_retry_attempts: 3,
|
||||||
|
autonomous_max_retry_attempts: 6,
|
||||||
|
check_todo_staleness: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ComputerControlConfig {
|
impl Default for ComputerControlConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
enabled: false,
|
enabled: true,
|
||||||
require_confirmation: true,
|
require_confirmation: false,
|
||||||
max_actions_per_second: 5,
|
max_actions_per_second: 5,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -296,17 +337,17 @@ impl Config {
|
|||||||
if let Some(path) = config_path_to_load {
|
if let Some(path) = config_path_to_load {
|
||||||
// Read and parse the config file
|
// Read and parse the config file
|
||||||
let config_content = std::fs::read_to_string(&path)?;
|
let config_content = std::fs::read_to_string(&path)?;
|
||||||
|
|
||||||
// Check for old format (direct provider config without named configs)
|
// Check for old format (direct provider config without named configs)
|
||||||
if Self::is_old_format(&config_content) {
|
if Self::is_old_format(&config_content) {
|
||||||
anyhow::bail!("{}", OLD_CONFIG_FORMAT_ERROR);
|
anyhow::bail!("{}", OLD_CONFIG_FORMAT_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
let config: Config = toml::from_str(&config_content)?;
|
let config: Config = toml::from_str(&config_content)?;
|
||||||
|
|
||||||
// Validate the default_provider format
|
// Validate the default_provider format
|
||||||
config.validate_provider_reference(&config.providers.default_provider)?;
|
config.validate_provider_reference(&config.providers.default_provider)?;
|
||||||
|
|
||||||
return Ok(config);
|
return Ok(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,7 +358,7 @@ impl Config {
|
|||||||
fn is_old_format(content: &str) -> bool {
|
fn is_old_format(content: &str) -> bool {
|
||||||
// Old format has [providers.anthropic] with api_key directly
|
// Old format has [providers.anthropic] with api_key directly
|
||||||
// New format has [providers.anthropic.<name>] with api_key
|
// New format has [providers.anthropic.<name>] with api_key
|
||||||
|
|
||||||
// Parse as TOML value to inspect structure
|
// Parse as TOML value to inspect structure
|
||||||
if let Ok(value) = content.parse::<toml::Value>() {
|
if let Ok(value) = content.parse::<toml::Value>() {
|
||||||
if let Some(providers) = value.get("providers") {
|
if let Some(providers) = value.get("providers") {
|
||||||
@@ -445,20 +486,26 @@ impl Config {
|
|||||||
|
|
||||||
// Apply provider override
|
// Apply provider override
|
||||||
if let Some(provider) = 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.validate_provider_reference(&provider)?;
|
||||||
config.providers.default_provider = provider;
|
config.providers.default_provider = provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply model override to the active provider
|
// Apply model override to the active provider
|
||||||
if let Some(model) = model_override {
|
if let Some(model) = model_override {
|
||||||
let (provider_type, config_name) = Self::parse_provider_reference(
|
let (provider_type, config_name) =
|
||||||
&config.providers.default_provider
|
Self::parse_provider_reference(&config.providers.default_provider)?;
|
||||||
)?;
|
|
||||||
|
|
||||||
match provider_type.as_str() {
|
match provider_type.as_str() {
|
||||||
"anthropic" => {
|
"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;
|
anthropic_config.model = model;
|
||||||
} else {
|
} else {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
@@ -468,7 +515,9 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"databricks" => {
|
"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;
|
databricks_config.model = model;
|
||||||
} else {
|
} else {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
@@ -478,7 +527,9 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"embedded" => {
|
"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;
|
embedded_config.model_path = model;
|
||||||
} else {
|
} else {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
@@ -488,7 +539,9 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"openai" => {
|
"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;
|
openai_config.model = model;
|
||||||
} else {
|
} else {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
@@ -499,13 +552,12 @@ impl Config {
|
|||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Check openai_compatible
|
// 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;
|
compat_config.model = model;
|
||||||
} else {
|
} else {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!("Unknown provider type: {}", provider_type));
|
||||||
"Unknown provider type: {}",
|
|
||||||
provider_type
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -585,36 +637,42 @@ impl Config {
|
|||||||
|
|
||||||
/// Get the current default provider's config
|
/// Get the current default provider's config
|
||||||
pub fn get_default_provider_config(&self) -> Result<ProviderConfigRef<'_>> {
|
pub fn get_default_provider_config(&self) -> Result<ProviderConfigRef<'_>> {
|
||||||
let (provider_type, config_name) = Self::parse_provider_reference(
|
let (provider_type, config_name) =
|
||||||
&self.providers.default_provider
|
Self::parse_provider_reference(&self.providers.default_provider)?;
|
||||||
)?;
|
|
||||||
|
|
||||||
match provider_type.as_str() {
|
match provider_type.as_str() {
|
||||||
"anthropic" => {
|
"anthropic" => self
|
||||||
self.providers.anthropic.get(&config_name)
|
.providers
|
||||||
.map(ProviderConfigRef::Anthropic)
|
.anthropic
|
||||||
.ok_or_else(|| anyhow::anyhow!("Anthropic config '{}' not found", config_name))
|
.get(&config_name)
|
||||||
}
|
.map(ProviderConfigRef::Anthropic)
|
||||||
"openai" => {
|
.ok_or_else(|| anyhow::anyhow!("Anthropic config '{}' not found", config_name)),
|
||||||
self.providers.openai.get(&config_name)
|
"openai" => self
|
||||||
.map(ProviderConfigRef::OpenAI)
|
.providers
|
||||||
.ok_or_else(|| anyhow::anyhow!("OpenAI config '{}' not found", config_name))
|
.openai
|
||||||
}
|
.get(&config_name)
|
||||||
"databricks" => {
|
.map(ProviderConfigRef::OpenAI)
|
||||||
self.providers.databricks.get(&config_name)
|
.ok_or_else(|| anyhow::anyhow!("OpenAI config '{}' not found", config_name)),
|
||||||
.map(ProviderConfigRef::Databricks)
|
"databricks" => self
|
||||||
.ok_or_else(|| anyhow::anyhow!("Databricks config '{}' not found", config_name))
|
.providers
|
||||||
}
|
.databricks
|
||||||
"embedded" => {
|
.get(&config_name)
|
||||||
self.providers.embedded.get(&config_name)
|
.map(ProviderConfigRef::Databricks)
|
||||||
.map(ProviderConfigRef::Embedded)
|
.ok_or_else(|| anyhow::anyhow!("Databricks config '{}' not found", config_name)),
|
||||||
.ok_or_else(|| anyhow::anyhow!("Embedded config '{}' not found", config_name))
|
"embedded" => self
|
||||||
}
|
.providers
|
||||||
_ => {
|
.embedded
|
||||||
self.providers.openai_compatible.get(&provider_type)
|
.get(&config_name)
|
||||||
.map(ProviderConfigRef::OpenAICompatible)
|
.map(ProviderConfigRef::Embedded)
|
||||||
.ok_or_else(|| anyhow::anyhow!("OpenAI compatible config '{}' not found", provider_type))
|
.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 tracing::{debug, error};
|
||||||
|
|
||||||
use crate::{
|
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,
|
CompletionChunk, CompletionRequest, CompletionResponse, CompletionStream, LLMProvider, Message,
|
||||||
MessageRole, Tool, ToolCall, Usage,
|
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";
|
const ANTHROPIC_API_URL: &str = "https://api.anthropic.com/v1/messages";
|
||||||
@@ -156,7 +159,7 @@ impl AnthropicProvider {
|
|||||||
name: "anthropic".to_string(),
|
name: "anthropic".to_string(),
|
||||||
api_key,
|
api_key,
|
||||||
model,
|
model,
|
||||||
max_tokens: max_tokens.unwrap_or(4096),
|
max_tokens: max_tokens.unwrap_or(32768),
|
||||||
temperature: temperature.unwrap_or(0.1),
|
temperature: temperature.unwrap_or(0.1),
|
||||||
cache_config,
|
cache_config,
|
||||||
enable_1m_context: enable_1m_context.unwrap_or(false),
|
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());
|
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 {
|
Ok(Self {
|
||||||
client,
|
client,
|
||||||
name,
|
name,
|
||||||
api_key,
|
api_key,
|
||||||
model,
|
model,
|
||||||
max_tokens: max_tokens.unwrap_or(4096),
|
max_tokens: max_tokens.unwrap_or(32768),
|
||||||
temperature: temperature.unwrap_or(0.1),
|
temperature: temperature.unwrap_or(0.1),
|
||||||
cache_config,
|
cache_config,
|
||||||
enable_1m_context: enable_1m_context.unwrap_or(false),
|
enable_1m_context: enable_1m_context.unwrap_or(false),
|
||||||
@@ -277,7 +283,7 @@ impl AnthropicProvider {
|
|||||||
MessageRole::User => {
|
MessageRole::User => {
|
||||||
// Build content blocks - images first, then text
|
// Build content blocks - images first, then text
|
||||||
let mut content_blocks: Vec<AnthropicContent> = Vec::new();
|
let mut content_blocks: Vec<AnthropicContent> = Vec::new();
|
||||||
|
|
||||||
// Add any images attached to this message
|
// Add any images attached to this message
|
||||||
for image in &message.images {
|
for image in &message.images {
|
||||||
content_blocks.push(AnthropicContent::Image {
|
content_blocks.push(AnthropicContent::Image {
|
||||||
@@ -288,13 +294,16 @@ impl AnthropicProvider {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add text content
|
// Add text content
|
||||||
content_blocks.push(AnthropicContent::Text {
|
content_blocks.push(AnthropicContent::Text {
|
||||||
text: message.content.clone(),
|
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 {
|
anthropic_messages.push(AnthropicMessage {
|
||||||
role: "user".to_string(),
|
role: "user".to_string(),
|
||||||
content: content_blocks,
|
content: content_blocks,
|
||||||
@@ -427,7 +436,10 @@ impl AnthropicProvider {
|
|||||||
if let Some(data) = line.strip_prefix("data: ") {
|
if let Some(data) = line.strip_prefix("data: ") {
|
||||||
if data == "[DONE]" {
|
if data == "[DONE]" {
|
||||||
debug!("Received stream completion marker");
|
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() {
|
if tx.send(Ok(final_chunk)).await.is_err() {
|
||||||
debug!("Receiver dropped, stopping stream");
|
debug!("Receiver dropped, stopping stream");
|
||||||
}
|
}
|
||||||
@@ -491,7 +503,8 @@ impl AnthropicProvider {
|
|||||||
{
|
{
|
||||||
// We have complete arguments, send the tool call immediately
|
// We have complete arguments, send the tool call immediately
|
||||||
debug!("Tool call has complete args, sending immediately: {:?}", tool_call);
|
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() {
|
if tx.send(Ok(chunk)).await.is_err() {
|
||||||
debug!("Receiver dropped, stopping stream");
|
debug!("Receiver dropped, stopping stream");
|
||||||
return accumulated_usage;
|
return accumulated_usage;
|
||||||
@@ -575,7 +588,8 @@ impl AnthropicProvider {
|
|||||||
|
|
||||||
// Send the complete tool call
|
// Send the complete tool call
|
||||||
if !current_tool_calls.is_empty() {
|
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() {
|
if tx.send(Ok(chunk)).await.is_err() {
|
||||||
debug!("Receiver dropped, stopping stream");
|
debug!("Receiver dropped, stopping stream");
|
||||||
return accumulated_usage;
|
return accumulated_usage;
|
||||||
@@ -597,7 +611,11 @@ impl AnthropicProvider {
|
|||||||
"message_stop" => {
|
"message_stop" => {
|
||||||
debug!("Received message stop event");
|
debug!("Received message stop event");
|
||||||
message_stopped = true;
|
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() {
|
if tx.send(Ok(final_chunk)).await.is_err() {
|
||||||
debug!("Receiver dropped, stopping stream");
|
debug!("Receiver dropped, stopping stream");
|
||||||
}
|
}
|
||||||
@@ -826,7 +844,10 @@ struct ThinkingConfig {
|
|||||||
|
|
||||||
impl ThinkingConfig {
|
impl ThinkingConfig {
|
||||||
fn enabled(budget_tokens: u32) -> Self {
|
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,
|
input: serde_json::Value,
|
||||||
},
|
},
|
||||||
#[serde(rename = "image")]
|
#[serde(rename = "image")]
|
||||||
Image {
|
Image { source: AnthropicImageSource },
|
||||||
source: AnthropicImageSource,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Image source for Anthropic API
|
/// Image source for Anthropic API
|
||||||
@@ -899,8 +918,8 @@ enum AnthropicContent {
|
|||||||
struct AnthropicImageSource {
|
struct AnthropicImageSource {
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
source_type: String, // Always "base64"
|
source_type: String, // Always "base64"
|
||||||
media_type: String, // e.g., "image/png", "image/jpeg"
|
media_type: String, // e.g., "image/png", "image/jpeg"
|
||||||
data: String, // Base64-encoded image data
|
data: String, // Base64-encoded image data
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -962,7 +981,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_message_conversion() {
|
fn test_message_conversion() {
|
||||||
let provider =
|
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![
|
let messages = vec![
|
||||||
Message::new(
|
Message::new(
|
||||||
@@ -1011,7 +1031,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_tool_conversion() {
|
fn test_tool_conversion() {
|
||||||
let provider =
|
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 {
|
let tools = vec![Tool {
|
||||||
name: "get_weather".to_string(),
|
name: "get_weather".to_string(),
|
||||||
@@ -1044,7 +1065,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_cache_control_serialization() {
|
fn test_cache_control_serialization() {
|
||||||
let provider =
|
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
|
// Test message WITHOUT cache_control
|
||||||
let messages_without = vec![Message::new(MessageRole::User, "Hello".to_string())];
|
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)
|
.create_request_body(&messages, None, false, 1000, 0.5, false)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let json_without = serde_json::to_string(&request_without).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
|
// Test WITH thinking parameter - max_tokens must be > budget_tokens + 1024
|
||||||
// Using budget=10000 requires max_tokens > 11024
|
// Using budget=10000 requires max_tokens > 11024
|
||||||
let provider_with = AnthropicProvider::new(
|
let provider_with = AnthropicProvider::new(
|
||||||
"test-key".to_string(),
|
"test-key".to_string(),
|
||||||
Some("claude-sonnet-4-5".to_string()),
|
Some("claude-sonnet-4-5".to_string()),
|
||||||
Some(20000), // Sufficient for thinking budget
|
Some(20000), // Sufficient for thinking budget
|
||||||
Some(0.5),
|
Some(0.5),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
@@ -1125,16 +1150,28 @@ mod tests {
|
|||||||
.create_request_body(&messages, None, false, 20000, 0.5, false)
|
.create_request_body(&messages, None, false, 20000, 0.5, false)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let json_with = serde_json::to_string(&request_with).unwrap();
|
let json_with = serde_json::to_string(&request_with).unwrap();
|
||||||
assert!(json_with.contains("thinking"), "JSON should contain 'thinking' field when configured");
|
assert!(
|
||||||
assert!(json_with.contains("\"type\":\"enabled\""), "JSON should contain type: enabled");
|
json_with.contains("thinking"),
|
||||||
assert!(json_with.contains("\"budget_tokens\":10000"), "JSON should contain budget_tokens: 10000");
|
"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
|
// Test WITH thinking parameter but INSUFFICIENT max_tokens - thinking should be disabled
|
||||||
let request_insufficient = provider_with
|
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();
|
.unwrap();
|
||||||
let json_insufficient = serde_json::to_string(&request_insufficient).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]
|
#[test]
|
||||||
@@ -1152,20 +1189,26 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let messages = vec![Message::new(MessageRole::User, "Test message".to_string())];
|
let messages = vec![Message::new(MessageRole::User, "Test message".to_string())];
|
||||||
|
|
||||||
// With disable_thinking=false, thinking should be enabled (max_tokens is sufficient)
|
// With disable_thinking=false, thinking should be enabled (max_tokens is sufficient)
|
||||||
let request_with_thinking = provider
|
let request_with_thinking = provider
|
||||||
.create_request_body(&messages, None, false, 20000, 0.5, false)
|
.create_request_body(&messages, None, false, 20000, 0.5, false)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let json_with = serde_json::to_string(&request_with_thinking).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
|
// With disable_thinking=true, thinking should be disabled even with sufficient max_tokens
|
||||||
let request_without_thinking = provider
|
let request_without_thinking = provider
|
||||||
.create_request_body(&messages, None, false, 20000, 0.5, true)
|
.create_request_body(&messages, None, false, 20000, 0.5, true)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let json_without = serde_json::to_string(&request_without_thinking).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]
|
#[test]
|
||||||
@@ -1183,16 +1226,20 @@ mod tests {
|
|||||||
|
|
||||||
let response: AnthropicResponse = serde_json::from_str(json_response)
|
let response: AnthropicResponse = serde_json::from_str(json_response)
|
||||||
.expect("Should be able to deserialize response with thinking block");
|
.expect("Should be able to deserialize response with thinking block");
|
||||||
|
|
||||||
assert_eq!(response.content.len(), 2);
|
assert_eq!(response.content.len(), 2);
|
||||||
assert_eq!(response.model, "claude-sonnet-4-5");
|
assert_eq!(response.model, "claude-sonnet-4-5");
|
||||||
|
|
||||||
// Extract only text content (thinking should be filtered out)
|
// Extract only text content (thinking should be filtered out)
|
||||||
let text_content: Vec<_> = response.content.iter().filter_map(|c| match c {
|
let text_content: Vec<_> = response
|
||||||
AnthropicContent::Text { text, .. } => Some(text.as_str()),
|
.content
|
||||||
_ => None,
|
.iter()
|
||||||
}).collect();
|
.filter_map(|c| match c {
|
||||||
|
AnthropicContent::Text { text, .. } => Some(text.as_str()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
assert_eq!(text_content.len(), 1);
|
assert_eq!(text_content.len(), 1);
|
||||||
assert_eq!(text_content[0], "Here is my response.");
|
assert_eq!(text_content[0], "Here is my response.");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user