101 lines
3.2 KiB
Rust
101 lines
3.2 KiB
Rust
//! Anthropic I/O — the ONLY place in `service/anthropic/` that may perform
|
|
//! network requests or store operations.
|
|
//!
|
|
//! Every function here is a thin adapter that converts lower-level errors
|
|
//! into the typed [`super::Error`] variants. No business logic or branching
|
|
//! lives here; that belongs in `mod.rs`.
|
|
|
|
use super::{Error, ModelSummary, ModelsResponse};
|
|
use crate::store::StoreOps;
|
|
use reqwest::header::{HeaderMap, HeaderValue};
|
|
|
|
/// Store key for the Anthropic API key — shared with `llm::chat`.
|
|
pub(crate) const KEY_ANTHROPIC_API_KEY: &str = "anthropic_api_key";
|
|
|
|
const ANTHROPIC_VERSION: &str = "2023-06-01";
|
|
|
|
/// Return whether a non-empty API key is stored.
|
|
pub(super) fn api_key_exists(store: &dyn StoreOps) -> bool {
|
|
match store.get(KEY_ANTHROPIC_API_KEY) {
|
|
Some(value) => value.as_str().map(|k| !k.is_empty()).unwrap_or(false),
|
|
None => false,
|
|
}
|
|
}
|
|
|
|
/// Read the stored API key, returning a typed error when absent or invalid.
|
|
pub(super) fn get_api_key(store: &dyn StoreOps) -> Result<String, Error> {
|
|
match store.get(KEY_ANTHROPIC_API_KEY) {
|
|
Some(value) => {
|
|
if let Some(key) = value.as_str() {
|
|
if key.is_empty() {
|
|
Err(Error::Validation(
|
|
"Anthropic API key is empty. Please set your API key.".to_string(),
|
|
))
|
|
} else {
|
|
Ok(key.to_string())
|
|
}
|
|
} else {
|
|
Err(Error::Validation(
|
|
"Stored API key is not a string".to_string(),
|
|
))
|
|
}
|
|
}
|
|
None => Err(Error::Validation(
|
|
"Anthropic API key not found. Please set your API key.".to_string(),
|
|
)),
|
|
}
|
|
}
|
|
|
|
/// Persist a new API key to the store.
|
|
pub(super) fn save_api_key(store: &dyn StoreOps, api_key: &str) -> Result<(), String> {
|
|
store.set(KEY_ANTHROPIC_API_KEY, serde_json::json!(api_key));
|
|
store.save()
|
|
}
|
|
|
|
/// Fetch models from the Anthropic API at `url`.
|
|
pub(super) async fn fetch_models(api_key: &str, url: &str) -> Result<Vec<ModelSummary>, Error> {
|
|
let client = reqwest::Client::new();
|
|
let mut headers = HeaderMap::new();
|
|
headers.insert(
|
|
"x-api-key",
|
|
HeaderValue::from_str(api_key)
|
|
.map_err(|e| Error::Validation(format!("Invalid API key header value: {e}")))?,
|
|
);
|
|
headers.insert(
|
|
"anthropic-version",
|
|
HeaderValue::from_static(ANTHROPIC_VERSION),
|
|
);
|
|
|
|
let response = client
|
|
.get(url)
|
|
.headers(headers)
|
|
.send()
|
|
.await
|
|
.map_err(|e| Error::UpstreamApi(e.to_string()))?;
|
|
|
|
if !response.status().is_success() {
|
|
let status = response.status();
|
|
let error_text = response
|
|
.text()
|
|
.await
|
|
.unwrap_or_else(|_| "Unknown error".to_string());
|
|
return Err(Error::UpstreamApi(format!(
|
|
"Anthropic API error {status}: {error_text}"
|
|
)));
|
|
}
|
|
|
|
let body = response
|
|
.json::<ModelsResponse>()
|
|
.await
|
|
.map_err(|e| Error::Internal(format!("Failed to parse response: {e}")))?;
|
|
|
|
Ok(body
|
|
.data
|
|
.into_iter()
|
|
.map(|m| ModelSummary {
|
|
id: m.id,
|
|
context_window: m.context_window,
|
|
})
|
|
.collect())
|
|
}
|