Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ logs/
# g3 artifacts
requirements.md
todo.g3.md
.claude/
1 change: 1 addition & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ default_provider = "anthropic.default"
[providers.anthropic.default]
api_key = "your-anthropic-api-key"
model = "claude-sonnet-4-5"
# base_url = "https://api.anthropic.com/v1" # Optional: Custom API base URL
max_tokens = 64000
temperature = 0.3
# cache_config = "ephemeral" # Optional: Enable prompt caching
Expand Down
1 change: 1 addition & 0 deletions crates/g3-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ pub struct OpenAIConfig {
pub struct AnthropicConfig {
pub api_key: String,
pub model: String,
pub base_url: Option<String>,
pub max_tokens: Option<u32>,
pub temperature: Option<f32>,
pub cache_config: Option<String>,
Expand Down
1 change: 1 addition & 0 deletions crates/g3-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1220,6 +1220,7 @@ impl<W: UiWriter> Agent<W> {
format!("anthropic.{}", name),
anthropic_config.api_key.clone(),
Some(anthropic_config.model.clone()),
anthropic_config.base_url.clone(),
anthropic_config.max_tokens,
anthropic_config.temperature,
anthropic_config.cache_config.clone(),
Expand Down
1 change: 1 addition & 0 deletions crates/g3-core/tests/test_preflight_max_tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ fn create_test_config_with_thinking(thinking_budget: Option<u32>) -> Config {
anthropic_configs.insert("default".to_string(), g3_config::AnthropicConfig {
api_key: "test-key".to_string(),
model: "claude-sonnet-4-5".to_string(),
base_url: None,
max_tokens: Some(16000),
temperature: Some(0.1),
cache_config: None,
Expand Down
1 change: 1 addition & 0 deletions crates/g3-planner/src/llm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pub async fn create_planner_provider(
format!("anthropic.{}", config_name),
anthropic_config.api_key.clone(),
Some(anthropic_config.model.clone()),
anthropic_config.base_url.clone(),
anthropic_config.max_tokens,
anthropic_config.temperature,
anthropic_config.cache_config.clone(),
Expand Down
83 changes: 73 additions & 10 deletions crates/g3-providers/src/anthropic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
//! - Proper message format conversion between g3 and Anthropic formats
//! - Rate limiting and error handling
//! - Native tool calling support
//! - Custom base URL support for proxies or alternative API endpoints
//!
//! # Usage
//!
Expand All @@ -22,6 +23,7 @@
//! let provider = AnthropicProvider::new(
//! "your-api-key".to_string(),
//! Some("claude-3-5-sonnet-20241022".to_string()),
//! None, // base_url (uses default Anthropic API)
//! Some(4096),
//! Some(0.1),
//! None, // cache_config
Expand Down Expand Up @@ -60,9 +62,10 @@
//! async fn main() -> anyhow::Result<()> {
//! let provider = AnthropicProvider::new(
//! "your-api-key".to_string(),
//! None,
//! None,
//! None,
//! None, // model
//! None, // base_url
//! None, // max_tokens
//! None, // temperature
//! None, // cache_config
//! None, // enable_1m_context
//! None, // thinking_budget_tokens
Expand Down Expand Up @@ -114,7 +117,7 @@ use crate::{
MessageRole, Tool, ToolCall, Usage,
};

const ANTHROPIC_API_URL: &str = "https://api.anthropic.com/v1/messages";
const DEFAULT_ANTHROPIC_BASE_URL: &str = "https://api.anthropic.com/v1";
const ANTHROPIC_VERSION: &str = "2023-06-01";

#[derive(Debug, Clone)]
Expand All @@ -123,6 +126,7 @@ pub struct AnthropicProvider {
name: String,
api_key: String,
model: String,
base_url: String,
max_tokens: u32,
temperature: f32,
cache_config: Option<String>,
Expand All @@ -134,6 +138,7 @@ impl AnthropicProvider {
pub fn new(
api_key: String,
model: Option<String>,
base_url: Option<String>,
max_tokens: Option<u32>,
temperature: Option<f32>,
cache_config: Option<String>,
Expand All @@ -146,14 +151,16 @@ impl AnthropicProvider {
.map_err(|e| anyhow!("Failed to create HTTP client: {}", e))?;

let model = model.unwrap_or_else(|| "claude-3-5-sonnet-20241022".to_string());
let base_url = base_url.unwrap_or_else(|| DEFAULT_ANTHROPIC_BASE_URL.to_string());

debug!("Initialized Anthropic provider with model: {}", model);
debug!("Initialized Anthropic provider with model: {}, base_url: {}", model, base_url);

Ok(Self {
client,
name: "anthropic".to_string(),
api_key,
model,
base_url,
max_tokens: max_tokens.unwrap_or(4096),
temperature: temperature.unwrap_or(0.1),
cache_config,
Expand All @@ -167,6 +174,7 @@ impl AnthropicProvider {
name: String,
api_key: String,
model: Option<String>,
base_url: Option<String>,
max_tokens: Option<u32>,
temperature: Option<f32>,
cache_config: Option<String>,
Expand All @@ -179,14 +187,16 @@ impl AnthropicProvider {
.map_err(|e| anyhow!("Failed to create HTTP client: {}", e))?;

let model = model.unwrap_or_else(|| "claude-3-5-sonnet-20241022".to_string());
let base_url = base_url.unwrap_or_else(|| DEFAULT_ANTHROPIC_BASE_URL.to_string());

debug!("Initialized Anthropic provider '{}' with model: {}", name, model);
debug!("Initialized Anthropic provider '{}' with model: {}, base_url: {}", name, model, base_url);

Ok(Self {
client,
name,
api_key,
model,
base_url,
max_tokens: max_tokens.unwrap_or(4096),
temperature: temperature.unwrap_or(0.1),
cache_config,
Expand All @@ -196,9 +206,10 @@ impl AnthropicProvider {
}

fn create_request_builder(&self, streaming: bool) -> RequestBuilder {
let url = format!("{}/messages", self.base_url);
let mut builder = self
.client
.post(ANTHROPIC_API_URL)
.post(&url)
.header("x-api-key", &self.api_key)
.header("anthropic-version", ANTHROPIC_VERSION)
.header("content-type", "application/json");
Expand Down Expand Up @@ -980,7 +991,7 @@ 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, None).unwrap();

let messages = vec![
Message::new(
Expand All @@ -1004,6 +1015,7 @@ mod tests {
let provider = AnthropicProvider::new(
"test-key".to_string(),
Some("claude-3-haiku-20240307".to_string()),
None, // base_url
Some(1000),
Some(0.5),
None,
Expand All @@ -1029,7 +1041,7 @@ 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, None).unwrap();

let tools = vec![Tool {
name: "get_weather".to_string(),
Expand Down Expand Up @@ -1062,7 +1074,7 @@ 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, None).unwrap();

// Test message WITHOUT cache_control
let messages_without = vec![Message::new(MessageRole::User, "Hello".to_string())];
Expand Down Expand Up @@ -1111,6 +1123,7 @@ mod tests {
let provider_without = AnthropicProvider::new(
"test-key".to_string(),
Some("claude-sonnet-4-5".to_string()),
None, // base_url
Some(1000),
Some(0.5),
None,
Expand All @@ -1131,6 +1144,7 @@ mod tests {
let provider_with = AnthropicProvider::new(
"test-key".to_string(),
Some("claude-sonnet-4-5".to_string()),
None, // base_url
Some(20000), // Sufficient for thinking budget
Some(0.5),
None,
Expand Down Expand Up @@ -1161,6 +1175,7 @@ mod tests {
let provider = AnthropicProvider::new(
"test-key".to_string(),
Some("claude-sonnet-4-5".to_string()),
None, // base_url
Some(20000),
Some(0.5),
None,
Expand Down Expand Up @@ -1214,4 +1229,52 @@ mod tests {
assert_eq!(text_content.len(), 1);
assert_eq!(text_content[0], "Here is my response.");
}

#[test]
fn test_base_url_configuration() {
// Test with default base_url (None)
let provider_default = AnthropicProvider::new(
"test-key".to_string(),
None,
None, // base_url - should use default
None,
None,
None,
None,
None,
)
.unwrap();
assert_eq!(provider_default.base_url, DEFAULT_ANTHROPIC_BASE_URL);

// Test with custom base_url
let custom_url = "https://custom.api.example.com/v1";
let provider_custom = AnthropicProvider::new(
"test-key".to_string(),
None,
Some(custom_url.to_string()),
None,
None,
None,
None,
None,
)
.unwrap();
assert_eq!(provider_custom.base_url, custom_url);

// Test new_with_name with custom base_url
let provider_named = AnthropicProvider::new_with_name(
"anthropic.custom".to_string(),
"test-key".to_string(),
Some("claude-3-haiku-20240307".to_string()),
Some(custom_url.to_string()),
None,
None,
None,
None,
None,
)
.unwrap();
assert_eq!(provider_named.base_url, custom_url);
assert_eq!(provider_named.name, "anthropic.custom");
}
}