feat: add anthropic-custom: prefix for Anthropic-compatible endpoints
Add support for custom Anthropic-compatible API endpoints via anthropic-custom: prefix
This commit is contained in:
parent
8694c2e2d2
commit
f8aef8bd62
2 changed files with 77 additions and 2 deletions
|
|
@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub struct AnthropicProvider {
|
pub struct AnthropicProvider {
|
||||||
credential: Option<String>,
|
credential: Option<String>,
|
||||||
|
base_url: String,
|
||||||
client: Client,
|
client: Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,11 +37,20 @@ struct ContentBlock {
|
||||||
|
|
||||||
impl AnthropicProvider {
|
impl AnthropicProvider {
|
||||||
pub fn new(api_key: Option<&str>) -> Self {
|
pub fn new(api_key: Option<&str>) -> Self {
|
||||||
|
Self::with_base_url(api_key, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_base_url(api_key: Option<&str>, base_url: Option<&str>) -> Self {
|
||||||
|
let base_url = base_url
|
||||||
|
.map(|u| u.trim_end_matches('/'))
|
||||||
|
.unwrap_or("https://api.anthropic.com")
|
||||||
|
.to_string();
|
||||||
Self {
|
Self {
|
||||||
credential: api_key
|
credential: api_key
|
||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
.filter(|k| !k.is_empty())
|
.filter(|k| !k.is_empty())
|
||||||
.map(ToString::to_string),
|
.map(ToString::to_string),
|
||||||
|
base_url,
|
||||||
client: Client::builder()
|
client: Client::builder()
|
||||||
.timeout(std::time::Duration::from_secs(120))
|
.timeout(std::time::Duration::from_secs(120))
|
||||||
.connect_timeout(std::time::Duration::from_secs(10))
|
.connect_timeout(std::time::Duration::from_secs(10))
|
||||||
|
|
@ -82,7 +92,7 @@ impl Provider for AnthropicProvider {
|
||||||
|
|
||||||
let mut request = self
|
let mut request = self
|
||||||
.client
|
.client
|
||||||
.post("https://api.anthropic.com/v1/messages")
|
.post(format!("{}/v1/messages", self.base_url))
|
||||||
.header("anthropic-version", "2023-06-01")
|
.header("anthropic-version", "2023-06-01")
|
||||||
.header("content-type", "application/json")
|
.header("content-type", "application/json")
|
||||||
.json(&request);
|
.json(&request);
|
||||||
|
|
@ -119,12 +129,14 @@ mod tests {
|
||||||
let p = AnthropicProvider::new(Some("sk-ant-test123"));
|
let p = AnthropicProvider::new(Some("sk-ant-test123"));
|
||||||
assert!(p.credential.is_some());
|
assert!(p.credential.is_some());
|
||||||
assert_eq!(p.credential.as_deref(), Some("sk-ant-test123"));
|
assert_eq!(p.credential.as_deref(), Some("sk-ant-test123"));
|
||||||
|
assert_eq!(p.base_url, "https://api.anthropic.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn creates_without_key() {
|
fn creates_without_key() {
|
||||||
let p = AnthropicProvider::new(None);
|
let p = AnthropicProvider::new(None);
|
||||||
assert!(p.credential.is_none());
|
assert!(p.credential.is_none());
|
||||||
|
assert_eq!(p.base_url, "https://api.anthropic.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -140,6 +152,25 @@ mod tests {
|
||||||
assert_eq!(p.credential.as_deref(), Some("sk-ant-test123"));
|
assert_eq!(p.credential.as_deref(), Some("sk-ant-test123"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn creates_with_custom_base_url() {
|
||||||
|
let p = AnthropicProvider::with_base_url(Some("sk-ant-test"), Some("https://api.example.com"));
|
||||||
|
assert_eq!(p.base_url, "https://api.example.com");
|
||||||
|
assert_eq!(p.credential.as_deref(), Some("sk-ant-test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_base_url_trims_trailing_slash() {
|
||||||
|
let p = AnthropicProvider::with_base_url(None, Some("https://api.example.com/"));
|
||||||
|
assert_eq!(p.base_url, "https://api.example.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_base_url_when_none_provided() {
|
||||||
|
let p = AnthropicProvider::with_base_url(None, None);
|
||||||
|
assert_eq!(p.base_url, "https://api.anthropic.com");
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn chat_fails_without_key() {
|
async fn chat_fails_without_key() {
|
||||||
let p = AnthropicProvider::new(None);
|
let p = AnthropicProvider::new(None);
|
||||||
|
|
|
||||||
|
|
@ -251,9 +251,22 @@ pub fn create_provider(name: &str, api_key: Option<&str>) -> anyhow::Result<Box<
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Anthropic-compatible custom endpoints ───────────
|
||||||
|
// Format: "anthropic-custom:https://your-api.com"
|
||||||
|
name if name.starts_with("anthropic-custom:") => {
|
||||||
|
let base_url = name.strip_prefix("anthropic-custom:").unwrap_or("");
|
||||||
|
if base_url.is_empty() {
|
||||||
|
anyhow::bail!("Anthropic-custom provider requires a URL. Format: anthropic-custom:https://your-api.com");
|
||||||
|
}
|
||||||
|
Ok(Box::new(anthropic::AnthropicProvider::with_base_url(
|
||||||
|
api_key, Some(base_url),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
_ => anyhow::bail!(
|
_ => anyhow::bail!(
|
||||||
"Unknown provider: {name}. Check README for supported providers or run `zeroclaw onboard --interactive` to reconfigure.\n\
|
"Unknown provider: {name}. Check README for supported providers or run `zeroclaw onboard --interactive` to reconfigure.\n\
|
||||||
Tip: Use \"custom:https://your-api.com\" for any OpenAI-compatible endpoint."
|
Tip: Use \"custom:https://your-api.com\" for OpenAI-compatible endpoints.\n\
|
||||||
|
Tip: Use \"anthropic-custom:https://your-api.com\" for Anthropic-compatible endpoints."
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -489,6 +502,37 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Anthropic-compatible custom endpoints ─────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn factory_anthropic_custom_url() {
|
||||||
|
let p = create_provider("anthropic-custom:https://api.example.com", Some("key"));
|
||||||
|
assert!(p.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn factory_anthropic_custom_trailing_slash() {
|
||||||
|
let p = create_provider("anthropic-custom:https://api.example.com/", Some("key"));
|
||||||
|
assert!(p.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn factory_anthropic_custom_no_key() {
|
||||||
|
let p = create_provider("anthropic-custom:https://api.example.com", None);
|
||||||
|
assert!(p.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn factory_anthropic_custom_empty_url_errors() {
|
||||||
|
match create_provider("anthropic-custom:", None) {
|
||||||
|
Err(e) => assert!(
|
||||||
|
e.to_string().contains("requires a URL"),
|
||||||
|
"Expected 'requires a URL', got: {e}"
|
||||||
|
),
|
||||||
|
Ok(_) => panic!("Expected error for empty anthropic-custom URL"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Error cases ──────────────────────────────────────────
|
// ── Error cases ──────────────────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue