From 692d61b2398f073b81d32d4cbe8145ab3929e48c Mon Sep 17 00:00:00 2001 From: Slavi Pantaleev Date: Thu, 27 Feb 2025 09:31:53 +0200 Subject: [PATCH] Replace Anthropic library (`anthropic-rs` -> `anthropic`) and switch default recommended model (`claude-3-5-sonnet-20240620` -> `claude-3-7-sonnet-20250219`) Fixes https://github.com/etkecc/baibot/issues/22 Ultimate related to `anthropic-rs` hardcoding models as enumeration variants in the code and not updating them. See: - https://github.com/roushou/mesh/issues/1 - https://github.com/roushou/mesh/pull/2 https://github.com/cortesi/misanthropy was also considered as an alternative, but it did not allow configuring the base API URL like our old Anthropic library (`anthropic-rs`) and like our new choice (`anthropic`). We'd rather not lose support for this, so we're going with the `anthropic` library. --- Cargo.lock | 95 ++++++++++++---------- Cargo.toml | 2 +- docs/sample-provider-configs/anthropic.yml | 2 +- src/agent/provider/anthropic/config.rs | 7 +- src/agent/provider/anthropic/controller.rs | 76 ++++++++--------- src/agent/provider/anthropic/utils.rs | 17 ++-- 6 files changed, 102 insertions(+), 97 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e8359bb..dbf1a32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,17 +73,25 @@ dependencies = [ ] [[package]] -name = "anthropic-rs" -version = "0.1.7" +name = "anthropic" +version = "0.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96dcc49d94d0021ec12ffa3ab9a2cfadc1e36f0a4c8a02c788f27bd2af02968a" +checksum = "092086afb8afa8a2bf24820213e22a57662eace03bd6b3c2680f6baf022ef6ba" dependencies = [ - "futures-util", - "reqwest 0.12.12", + "backoff", + "config", + "derive_builder 0.12.0", + "lazy_static", + "log", + "reqwest 0.11.27", + "reqwest-eventsource 0.4.0", + "rustc_version", "serde", + "serde_derive", "serde_json", "thiserror 1.0.69", "tokio", + "tokio-stream", ] [[package]] @@ -299,7 +307,7 @@ dependencies = [ name = "baibot" version = "1.4.1" dependencies = [ - "anthropic-rs", + "anthropic", "anyhow", "async-openai 0.27.2", "base64 0.22.1", @@ -321,6 +329,12 @@ dependencies = [ "url", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.7" @@ -534,6 +548,20 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "config" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23738e11972c7643e4ec947840fc463b6a571afcd3e735bdfce7d03c7a784aca" +dependencies = [ + "async-trait", + "lazy_static", + "nom", + "pathdiff", + "ron", + "serde", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -2476,6 +2504,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "pbkdf2" version = "0.12.2" @@ -2868,7 +2902,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper 0.1.2", - "system-configuration 0.5.1", + "system-configuration", "tokio", "tokio-rustls 0.24.1", "tokio-util", @@ -2890,7 +2924,6 @@ dependencies = [ "async-compression", "base64 0.22.1", "bytes", - "encoding_rs", "futures-core", "futures-util", "h2 0.4.8", @@ -2919,7 +2952,6 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper 1.0.2", - "system-configuration 0.6.1", "tokio", "tokio-native-tls", "tokio-rustls 0.26.1", @@ -3002,6 +3034,17 @@ dependencies = [ "serde", ] +[[package]] +name = "ron" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" +dependencies = [ + "base64 0.13.1", + "bitflags 1.3.2", + "serde", +] + [[package]] name = "ruma" version = "0.12.1" @@ -3508,15 +3551,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - [[package]] name = "signature" version = "2.2.0" @@ -3641,18 +3675,7 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", - "system-configuration-sys 0.5.0", -] - -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags 2.8.0", - "core-foundation 0.9.4", - "system-configuration-sys 0.6.0", + "system-configuration-sys", ] [[package]] @@ -3665,16 +3688,6 @@ dependencies = [ "libc", ] -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "tempfile" version = "3.17.1" @@ -3822,9 +3835,7 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot", "pin-project-lite", - "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.52.0", diff --git a/Cargo.toml b/Cargo.toml index f5f79d1..7107e88 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ name = "baibot" path = "src/lib.rs" [dependencies] -anthropic-rs = "0.1.*" +anthropic = "=0.0.8" anyhow = "1.0.*" async-openai = "0.27.*" base64 = "0.22.*" diff --git a/docs/sample-provider-configs/anthropic.yml b/docs/sample-provider-configs/anthropic.yml index 9918f6b..efac69d 100644 --- a/docs/sample-provider-configs/anthropic.yml +++ b/docs/sample-provider-configs/anthropic.yml @@ -1,7 +1,7 @@ base_url: https://api.anthropic.com/v1 api_key: YOUR_API_KEY_HERE text_generation: - model_id: claude-3-5-sonnet-20240620 + model_id: claude-3-7-sonnet-20250219 prompt: "You are a brief, but helpful bot called {{ baibot_name }} powered by the {{ baibot_model_id }} model. The date/time of this conversation's start is: {{ baibot_conversation_start_time_utc }}." temperature: 1.0 max_response_tokens: 8192 diff --git a/src/agent/provider/anthropic/config.rs b/src/agent/provider/anthropic/config.rs index 059899c..122a803 100644 --- a/src/agent/provider/anthropic/config.rs +++ b/src/agent/provider/anthropic/config.rs @@ -1,7 +1,5 @@ use serde::{Deserialize, Serialize}; -use anthropic_rs::models::claude::ClaudeModel; - use crate::agent::{default_prompt, provider::ConfigTrait}; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -28,6 +26,9 @@ impl ConfigTrait for Config { if self.base_url.is_empty() { return Err("The base URL must not be empty.".to_owned()); } + if !self.base_url.ends_with("/v1") { + return Err("The base URL must end with '/v1'.".to_owned()); + } if self.api_key.is_empty() { return Err("The API key must not be empty.".to_owned()); } @@ -67,5 +68,5 @@ impl Default for TextGenerationConfig { } fn default_text_model_id() -> String { - ClaudeModel::Claude35Sonnet.as_str().to_owned() + "claude-3-7-sonnet-20250219".to_owned() } diff --git a/src/agent/provider/anthropic/controller.rs b/src/agent/provider/anthropic/controller.rs index 3856382..2966211 100644 --- a/src/agent/provider/anthropic/controller.rs +++ b/src/agent/provider/anthropic/controller.rs @@ -1,12 +1,8 @@ use std::fmt::Debug; -use std::str::FromStr; use std::sync::Arc; -use anthropic_rs::completion::message::{ContentType, System}; -use anthropic_rs::{ - client::Client as AnthropicClient, config::Config as AnthropicConfig, - models::claude::ClaudeModel, -}; +use anthropic::client::{Client, ClientBuilder}; +use anthropic::types::ContentBlock; use super::super::ControllerTrait; use crate::agent::AgentPurpose; @@ -24,7 +20,7 @@ use crate::strings; use super::config::Config; struct ControllerInner { - client: AnthropicClient, + client: Client, } #[derive(Clone)] @@ -43,18 +39,20 @@ impl Debug for Controller { impl Controller { pub fn new(config: Config) -> anyhow::Result { - let anthropic_config = - AnthropicConfig::new(config.api_key.clone()).with_base_url(config.base_url.clone()); + // The previous library that we used expected a base URL that ends with "/v1" + // (e.g. "https://api.anthropic.com/v1"), while the new one doesn't. + // + // To keep backward compatibility, we don't ask people to change their configuration + // and rather adapt by removing the "/v1" from the base URL. + if !config.base_url.ends_with("/v1") { + return Err(anyhow::anyhow!("base_url must end with '/v1'")); + } - let client = match AnthropicClient::new(anthropic_config) { - Ok(client) => client, - Err(err) => { - return Err(anyhow::anyhow!( - "Failed to create Anthropic client: {}", - err.to_string() - )); - } - }; + let base_url = &config.base_url[..config.base_url.len() - 3]; + let client = ClientBuilder::default() + .api_base(base_url.to_string()) + .api_key(config.api_key.clone()) + .build()?; Ok(Self { config, @@ -142,29 +140,17 @@ impl ControllerTrait for Controller { let mut request = super::utils::create_anthropic_message_request(conversation_messages); - let model = match ClaudeModel::from_str(&text_generation_config.model_id) { - Ok(model) => model, - Err(err) => { - tracing::debug!(?err, "Failed to parse model ID"); - - return Err(anyhow::anyhow!( - "Failed to parse model ID: {}", - &text_generation_config.model_id - )); - } - }; - let temperature = params .temperature_override .unwrap_or(text_generation_config.temperature); if let Some(prompt_message) = prompt_message { - request.system = Some(System::Text(prompt_message.message_text)); + request.system = prompt_message.message_text; } - request.model = model; - request.temperature = Some(temperature); - request.max_tokens = text_generation_config.max_response_tokens; + request.model = text_generation_config.model_id.clone(); + request.temperature = Some(temperature as f64); + request.max_tokens = text_generation_config.max_response_tokens as usize; if let Ok(request_as_json) = serde_json::to_string(&request) { tracing::trace!( @@ -175,19 +161,27 @@ impl ControllerTrait for Controller { ); } - let response = self.inner.client.create_message(request).await?; + let response = self.inner.client.messages(request).await?; tracing::trace!(?response, "Got response from Anthropic create message API"); // response.content usually contains a single element, but we support handling multiple to account for all possibilities let mut text_parts = vec![]; for content in response.content { - let content_type = content.content_type; - - match content_type { - ContentType::Text => { - text_parts.push(content.text); - } // There are no other content types to handle yet, but there may be in the future + match content { + ContentBlock::Text { text } => { + text_parts.push(text); + } + ContentBlock::Image { + source, + media_type, + data: _, + } => { + text_parts.push(format!( + "The model responded with an image of type {}: {}", + media_type, source + )); + } } } diff --git a/src/agent/provider/anthropic/utils.rs b/src/agent/provider/anthropic/utils.rs index a4c2e88..25a0745 100644 --- a/src/agent/provider/anthropic/utils.rs +++ b/src/agent/provider/anthropic/utils.rs @@ -1,8 +1,8 @@ -use anthropic_rs::completion::message::{Content, ContentType, Message, MessageRequest, Role}; +use anthropic::types::{ContentBlock, Message, MessagesRequest, MessagesRequestBuilder, Role}; use crate::conversation::llm::{Author as LLMAuthor, Message as LLMMessage}; -pub(super) fn create_anthropic_message_request(llm_messages: Vec) -> MessageRequest { +pub(super) fn create_anthropic_message_request(llm_messages: Vec) -> MessagesRequest { let mut messages = vec![]; for message in llm_messages { @@ -14,8 +14,7 @@ pub(super) fn create_anthropic_message_request(llm_messages: Vec) -> } }; - let content = vec![Content { - content_type: ContentType::Text, + let content = vec![ContentBlock::Text { text: message.message_text, }]; @@ -24,9 +23,9 @@ pub(super) fn create_anthropic_message_request(llm_messages: Vec) -> messages.push(message); } - MessageRequest { - stream: false, - messages, - ..Default::default() - } + MessagesRequestBuilder::default() + .messages(messages) + .stream(false) + .build() + .expect("Failed to build messages request") }