feat: add Anthropic setup-token flow

Implements Anthropic setup-token flow from PR #103. All 907 tests pass.
This commit is contained in:
Argenis 2026-02-15 10:02:40 -05:00 committed by GitHub
parent b208cc940e
commit be135e07cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 102 additions and 15 deletions

View file

@ -4,7 +4,7 @@ use reqwest::Client;
use serde::{Deserialize, Serialize};
pub struct AnthropicProvider {
api_key: Option<String>,
credential: Option<String>,
client: Client,
}
@ -37,7 +37,10 @@ struct ContentBlock {
impl AnthropicProvider {
pub fn new(api_key: Option<&str>) -> Self {
Self {
api_key: api_key.map(ToString::to_string),
credential: api_key
.map(str::trim)
.filter(|k| !k.is_empty())
.map(ToString::to_string),
client: Client::builder()
.timeout(std::time::Duration::from_secs(120))
.connect_timeout(std::time::Duration::from_secs(10))
@ -45,6 +48,10 @@ impl AnthropicProvider {
.unwrap_or_else(|_| Client::new()),
}
}
fn is_setup_token(token: &str) -> bool {
token.starts_with("sk-ant-oat01-")
}
}
#[async_trait]
@ -56,8 +63,10 @@ impl Provider for AnthropicProvider {
model: &str,
temperature: f64,
) -> anyhow::Result<String> {
let api_key = self.api_key.as_ref().ok_or_else(|| {
anyhow::anyhow!("Anthropic API key not set. Set ANTHROPIC_API_KEY or edit config.toml.")
let credential = self.credential.as_ref().ok_or_else(|| {
anyhow::anyhow!(
"Anthropic credentials not set. Set ANTHROPIC_API_KEY or ANTHROPIC_OAUTH_TOKEN (setup-token)."
)
})?;
let request = ChatRequest {
@ -71,15 +80,20 @@ impl Provider for AnthropicProvider {
temperature,
};
let response = self
let mut request = self
.client
.post("https://api.anthropic.com/v1/messages")
.header("x-api-key", api_key)
.header("anthropic-version", "2023-06-01")
.header("content-type", "application/json")
.json(&request)
.send()
.await?;
.json(&request);
if Self::is_setup_token(credential) {
request = request.header("Authorization", format!("Bearer {credential}"));
} else {
request = request.header("x-api-key", credential);
}
let response = request.send().await?;
if !response.status().is_success() {
return Err(super::api_error("Anthropic", response).await);
@ -103,21 +117,27 @@ mod tests {
#[test]
fn creates_with_key() {
let p = AnthropicProvider::new(Some("sk-ant-test123"));
assert!(p.api_key.is_some());
assert_eq!(p.api_key.as_deref(), Some("sk-ant-test123"));
assert!(p.credential.is_some());
assert_eq!(p.credential.as_deref(), Some("sk-ant-test123"));
}
#[test]
fn creates_without_key() {
let p = AnthropicProvider::new(None);
assert!(p.api_key.is_none());
assert!(p.credential.is_none());
}
#[test]
fn creates_with_empty_key() {
let p = AnthropicProvider::new(Some(""));
assert!(p.api_key.is_some());
assert_eq!(p.api_key.as_deref(), Some(""));
assert!(p.credential.is_none());
}
#[test]
fn creates_with_whitespace_key() {
let p = AnthropicProvider::new(Some(" sk-ant-test123 "));
assert!(p.credential.is_some());
assert_eq!(p.credential.as_deref(), Some("sk-ant-test123"));
}
#[tokio::test]
@ -129,11 +149,17 @@ mod tests {
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("API key not set"),
err.contains("credentials not set"),
"Expected key error, got: {err}"
);
}
#[test]
fn setup_token_detection_works() {
assert!(AnthropicProvider::is_setup_token("sk-ant-oat01-abcdef"));
assert!(!AnthropicProvider::is_setup_token("sk-ant-api-key"));
}
#[tokio::test]
async fn chat_with_system_fails_without_key() {
let p = AnthropicProvider::new(None);