|
- 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<Url> {
- 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<String> {
- serde_json::from_str::<ErrorEnvelope>(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::<Vec<_>>()
- .join(" ")
- }
-
- #[derive(Debug, Deserialize)]
- struct ErrorEnvelope {
- error: ErrorBody,
- }
-
- #[derive(Debug, Deserialize)]
- struct ErrorBody {
- message: Option<String>,
- }
-
- pub(crate) struct SseLineBuffer {
- buffer: String,
- }
-
- impl SseLineBuffer {
- pub fn new() -> Self {
- Self {
- buffer: String::new(),
- }
- }
-
- pub fn feed(&mut self, chunk: &[u8]) -> Vec<String> {
- 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
- }
- }
|