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.
This commit is contained in:
Slavi Pantaleev
2025-02-27 09:31:53 +02:00
parent 05902f4c17
commit 692d61b239
6 changed files with 102 additions and 97 deletions

95
Cargo.lock generated
View File

@@ -73,17 +73,25 @@ dependencies = [
] ]
[[package]] [[package]]
name = "anthropic-rs" name = "anthropic"
version = "0.1.7" version = "0.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96dcc49d94d0021ec12ffa3ab9a2cfadc1e36f0a4c8a02c788f27bd2af02968a" checksum = "092086afb8afa8a2bf24820213e22a57662eace03bd6b3c2680f6baf022ef6ba"
dependencies = [ dependencies = [
"futures-util", "backoff",
"reqwest 0.12.12", "config",
"derive_builder 0.12.0",
"lazy_static",
"log",
"reqwest 0.11.27",
"reqwest-eventsource 0.4.0",
"rustc_version",
"serde", "serde",
"serde_derive",
"serde_json", "serde_json",
"thiserror 1.0.69", "thiserror 1.0.69",
"tokio", "tokio",
"tokio-stream",
] ]
[[package]] [[package]]
@@ -299,7 +307,7 @@ dependencies = [
name = "baibot" name = "baibot"
version = "1.4.1" version = "1.4.1"
dependencies = [ dependencies = [
"anthropic-rs", "anthropic",
"anyhow", "anyhow",
"async-openai 0.27.2", "async-openai 0.27.2",
"base64 0.22.1", "base64 0.22.1",
@@ -321,6 +329,12 @@ dependencies = [
"url", "url",
] ]
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.21.7" version = "0.21.7"
@@ -534,6 +548,20 @@ dependencies = [
"crossbeam-utils", "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]] [[package]]
name = "const-oid" name = "const-oid"
version = "0.9.6" version = "0.9.6"
@@ -2476,6 +2504,12 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pathdiff"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]] [[package]]
name = "pbkdf2" name = "pbkdf2"
version = "0.12.2" version = "0.12.2"
@@ -2868,7 +2902,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"sync_wrapper 0.1.2", "sync_wrapper 0.1.2",
"system-configuration 0.5.1", "system-configuration",
"tokio", "tokio",
"tokio-rustls 0.24.1", "tokio-rustls 0.24.1",
"tokio-util", "tokio-util",
@@ -2890,7 +2924,6 @@ dependencies = [
"async-compression", "async-compression",
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
"encoding_rs",
"futures-core", "futures-core",
"futures-util", "futures-util",
"h2 0.4.8", "h2 0.4.8",
@@ -2919,7 +2952,6 @@ dependencies = [
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"sync_wrapper 1.0.2", "sync_wrapper 1.0.2",
"system-configuration 0.6.1",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
"tokio-rustls 0.26.1", "tokio-rustls 0.26.1",
@@ -3002,6 +3034,17 @@ dependencies = [
"serde", "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]] [[package]]
name = "ruma" name = "ruma"
version = "0.12.1" version = "0.12.1"
@@ -3508,15 +3551,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 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]] [[package]]
name = "signature" name = "signature"
version = "2.2.0" version = "2.2.0"
@@ -3641,18 +3675,7 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"core-foundation 0.9.4", "core-foundation 0.9.4",
"system-configuration-sys 0.5.0", "system-configuration-sys",
]
[[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",
] ]
[[package]] [[package]]
@@ -3665,16 +3688,6 @@ dependencies = [
"libc", "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]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.17.1" version = "3.17.1"
@@ -3822,9 +3835,7 @@ dependencies = [
"bytes", "bytes",
"libc", "libc",
"mio", "mio",
"parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry",
"socket2", "socket2",
"tokio-macros", "tokio-macros",
"windows-sys 0.52.0", "windows-sys 0.52.0",

View File

@@ -15,7 +15,7 @@ name = "baibot"
path = "src/lib.rs" path = "src/lib.rs"
[dependencies] [dependencies]
anthropic-rs = "0.1.*" anthropic = "=0.0.8"
anyhow = "1.0.*" anyhow = "1.0.*"
async-openai = "0.27.*" async-openai = "0.27.*"
base64 = "0.22.*" base64 = "0.22.*"

View File

@@ -1,7 +1,7 @@
base_url: https://api.anthropic.com/v1 base_url: https://api.anthropic.com/v1
api_key: YOUR_API_KEY_HERE api_key: YOUR_API_KEY_HERE
text_generation: 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 }}." 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 temperature: 1.0
max_response_tokens: 8192 max_response_tokens: 8192

View File

@@ -1,7 +1,5 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use anthropic_rs::models::claude::ClaudeModel;
use crate::agent::{default_prompt, provider::ConfigTrait}; use crate::agent::{default_prompt, provider::ConfigTrait};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -28,6 +26,9 @@ impl ConfigTrait for Config {
if self.base_url.is_empty() { if self.base_url.is_empty() {
return Err("The base URL must not be empty.".to_owned()); 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() { if self.api_key.is_empty() {
return Err("The API key must not be empty.".to_owned()); 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 { fn default_text_model_id() -> String {
ClaudeModel::Claude35Sonnet.as_str().to_owned() "claude-3-7-sonnet-20250219".to_owned()
} }

View File

@@ -1,12 +1,8 @@
use std::fmt::Debug; use std::fmt::Debug;
use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use anthropic_rs::completion::message::{ContentType, System}; use anthropic::client::{Client, ClientBuilder};
use anthropic_rs::{ use anthropic::types::ContentBlock;
client::Client as AnthropicClient, config::Config as AnthropicConfig,
models::claude::ClaudeModel,
};
use super::super::ControllerTrait; use super::super::ControllerTrait;
use crate::agent::AgentPurpose; use crate::agent::AgentPurpose;
@@ -24,7 +20,7 @@ use crate::strings;
use super::config::Config; use super::config::Config;
struct ControllerInner { struct ControllerInner {
client: AnthropicClient, client: Client,
} }
#[derive(Clone)] #[derive(Clone)]
@@ -43,18 +39,20 @@ impl Debug for Controller {
impl Controller { impl Controller {
pub fn new(config: Config) -> anyhow::Result<Self> { pub fn new(config: Config) -> anyhow::Result<Self> {
let anthropic_config = // The previous library that we used expected a base URL that ends with "/v1"
AnthropicConfig::new(config.api_key.clone()).with_base_url(config.base_url.clone()); // (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) { let base_url = &config.base_url[..config.base_url.len() - 3];
Ok(client) => client, let client = ClientBuilder::default()
Err(err) => { .api_base(base_url.to_string())
return Err(anyhow::anyhow!( .api_key(config.api_key.clone())
"Failed to create Anthropic client: {}", .build()?;
err.to_string()
));
}
};
Ok(Self { Ok(Self {
config, config,
@@ -142,29 +140,17 @@ impl ControllerTrait for Controller {
let mut request = super::utils::create_anthropic_message_request(conversation_messages); 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 let temperature = params
.temperature_override .temperature_override
.unwrap_or(text_generation_config.temperature); .unwrap_or(text_generation_config.temperature);
if let Some(prompt_message) = prompt_message { 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.model = text_generation_config.model_id.clone();
request.temperature = Some(temperature); request.temperature = Some(temperature as f64);
request.max_tokens = text_generation_config.max_response_tokens; request.max_tokens = text_generation_config.max_response_tokens as usize;
if let Ok(request_as_json) = serde_json::to_string(&request) { if let Ok(request_as_json) = serde_json::to_string(&request) {
tracing::trace!( 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"); 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 // response.content usually contains a single element, but we support handling multiple to account for all possibilities
let mut text_parts = vec![]; let mut text_parts = vec![];
for content in response.content { for content in response.content {
let content_type = content.content_type; match content {
ContentBlock::Text { text } => {
match content_type { text_parts.push(text);
ContentType::Text => { }
text_parts.push(content.text); ContentBlock::Image {
} // There are no other content types to handle yet, but there may be in the future source,
media_type,
data: _,
} => {
text_parts.push(format!(
"The model responded with an image of type {}: {}",
media_type, source
));
}
} }
} }

View File

@@ -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}; use crate::conversation::llm::{Author as LLMAuthor, Message as LLMMessage};
pub(super) fn create_anthropic_message_request(llm_messages: Vec<LLMMessage>) -> MessageRequest { pub(super) fn create_anthropic_message_request(llm_messages: Vec<LLMMessage>) -> MessagesRequest {
let mut messages = vec![]; let mut messages = vec![];
for message in llm_messages { for message in llm_messages {
@@ -14,8 +14,7 @@ pub(super) fn create_anthropic_message_request(llm_messages: Vec<LLMMessage>) ->
} }
}; };
let content = vec![Content { let content = vec![ContentBlock::Text {
content_type: ContentType::Text,
text: message.message_text, text: message.message_text,
}]; }];
@@ -24,9 +23,9 @@ pub(super) fn create_anthropic_message_request(llm_messages: Vec<LLMMessage>) ->
messages.push(message); messages.push(message);
} }
MessageRequest { MessagesRequestBuilder::default()
stream: false, .messages(messages)
messages, .stream(false)
..Default::default() .build()
} .expect("Failed to build messages request")
} }