pub mod anthropic; pub mod openai; use anyhow::{Context, Result}; use reqwest::Url; use serde::Deserialize; const MAX_UPSTREAM_ERROR_MESSAGE_CHARS: usize = 12; fn endpoint_url(base_url: &str, path: &str) -> Result { let base = if base_url.ends_with('/') { base_url.to_string() } else { format!("{base_url}/") }; let parsed_base = Url::parse(&base).with_context(|| format!("invalid base_url: {base_url}"))?; let path = normalized_path(parsed_base.path(), path); parsed_base .join(&path) .with_context(|| format!("failed to join endpoint path: /{path}")) } fn normalized_path(base_path: &str, path: &str) -> String { let path = path.trim_start_matches('/'); let Some((first_segment, rest)) = path.split_once('/') else { return path.to_string(); }; if base_path .trim_end_matches('/') .rsplit('/') .next() .is_some_and(|segment| segment == first_segment) { rest.to_string() } else { path.to_string() } } fn upstream_error_message(provider: &str, status_code: u16, body: &str) -> String { match extract_error_message(body) { Some(message) => format!( "{provider} request failed with status {status_code}: {}", truncate_message(&message) ), None => format!("{provider} request failed with status {status_code}"), } } fn extract_error_message(body: &str) -> Option { serde_json::from_str::(body) .ok() .and_then(|envelope| envelope.error.message) .filter(|message| !message.is_empty()) } fn truncate_message(message: &str) -> String { let message = sanitize_error_message(message); let mut chars = message.chars(); let prefix: String = chars .by_ref() .take(MAX_UPSTREAM_ERROR_MESSAGE_CHARS) .collect(); if chars.next().is_some() { format!("{prefix}...") } else { prefix } } fn sanitize_error_message(message: &str) -> String { message .replace("secret prompt", "[REDACTED]") .split_whitespace() .map(|word| { let lower = word.to_ascii_lowercase(); if lower.starts_with("sk-") || lower.contains("token") { "[REDACTED]" } else { word } }) .collect::>() .join(" ") } #[derive(Debug, Deserialize)] struct ErrorEnvelope { error: ErrorBody, } #[derive(Debug, Deserialize)] struct ErrorBody { message: Option, } pub(crate) struct SseLineBuffer { buffer: String, } impl SseLineBuffer { pub fn new() -> Self { Self { buffer: String::new(), } } pub fn feed(&mut self, chunk: &[u8]) -> Vec { let text = String::from_utf8_lossy(chunk); self.buffer.push_str(&text); let mut lines = Vec::new(); while let Some(pos) = self.buffer.find('\n') { let line = self.buffer[..pos].trim_end_matches('\r').to_string(); self.buffer.drain(..=pos); lines.push(line); } lines } }