//! 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 { 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, 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::() .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()) }