pub mod anthropic;
pub mod gemini;
pub mod openai_chat;
pub mod openai_responses;
use std::sync::Arc;
use anyhow::Result;
use async_trait::async_trait;
use futures_util::stream::BoxStream;
use systemprompt_models::profile::GatewayRoute;
use systemprompt_models::services::ai::ModelLimits;
use thiserror::Error;
use super::canonical::CanonicalRequest;
use super::canonical_response::{CanonicalEvent, CanonicalResponse};
#[derive(Debug, Error)]
pub enum UpstreamError {
#[error("{provider} returned {status}: {message}")]
Status {
provider: String,
status: u16,
message: String,
body: bytes::Bytes,
retry_after: Option<String>,
request_id: Option<String>,
},
#[error("{provider} request failed: {source}")]
Transport {
provider: String,
#[source]
source: reqwest::Error,
},
}
impl UpstreamError {
pub async fn from_response(provider: &str, response: reqwest::Response) -> Self {
let status = response.status().as_u16();
let header = |name: &str| {
response
.headers()
.get(name)
.and_then(|v| v.to_str().ok())
.map(ToOwned::to_owned)
};
let retry_after = header("retry-after");
let request_id = header("request-id").or_else(|| header("x-request-id"));
let body = response.bytes().await.unwrap_or_default();
Self::Status {
provider: provider.to_owned(),
status,
message: extract_upstream_message(&String::from_utf8_lossy(&body)),
body,
retry_after,
request_id,
}
}
}
pub fn extract_upstream_message(body: &str) -> String {
serde_json::from_str::<serde_json::Value>(body)
.ok()
.and_then(|v| v["error"]["message"].as_str().map(ToOwned::to_owned))
.unwrap_or_else(|| body.chars().take(500).collect())
}
#[derive(Debug)]
pub struct OutboundCtx<'a> {
pub route: &'a GatewayRoute,
pub endpoint: &'a str,
pub api_key: &'a str,
pub request: &'a CanonicalRequest,
pub upstream_model: &'a str,
pub model_limits: Option<ModelLimits>,
pub forward_headers: &'a [(String, String)],
pub raw_body: Option<&'a bytes::Bytes>,
}
#[expect(
missing_debug_implementations,
reason = "variants hold streaming bodies that intentionally do not implement Debug"
)]
pub enum OutboundOutcome {
Buffered(Box<CanonicalResponse>),
Streaming(BoxStream<'static, Result<CanonicalEvent, String>>),
RawBuffered {
body: bytes::Bytes,
content_type: Option<String>,
canonical: Box<CanonicalResponse>,
},
RawStreaming {
content_type: Option<String>,
stream: BoxStream<'static, Result<bytes::Bytes, String>>,
},
}
#[derive(Debug, Clone)]
pub struct PreparedBody {
pub bytes: bytes::Bytes,
pub raw_lane: bool,
}
#[async_trait]
pub trait OutboundAdapter: Send + Sync {
fn build_body(&self, ctx: &OutboundCtx<'_>) -> Result<PreparedBody>;
async fn send(&self, ctx: OutboundCtx<'_>, body: &PreparedBody) -> Result<OutboundOutcome>;
}
#[derive(Debug, Clone, Copy)]
pub struct OutboundAdapterRegistration {
pub tag: &'static str,
pub factory: fn() -> Arc<dyn OutboundAdapter>,
}
inventory::collect!(OutboundAdapterRegistration);