Skip to main content

systemprompt_api/services/gateway/protocol/outbound/
mod.rs

1//! Outbound protocol adapters: canonical model to upstream provider.
2//!
3//! The [`OutboundAdapter`] trait sends a [`CanonicalRequest`] to an upstream
4//! provider and yields an [`OutboundOutcome`] — a buffered response or a stream
5//! of canonical events. Adapters register themselves via
6//! [`OutboundAdapterRegistration`] (collected by `inventory`) so the upstream
7//! registry can resolve one by provider tag. Implementations cover Anthropic,
8//! `OpenAI` Chat Completions, and `OpenAI` Responses.
9
10pub mod anthropic;
11pub mod gemini;
12pub mod openai_chat;
13pub mod openai_responses;
14
15use std::sync::Arc;
16
17use anyhow::Result;
18use async_trait::async_trait;
19use futures_util::stream::BoxStream;
20use systemprompt_models::profile::GatewayRoute;
21use systemprompt_models::services::ai::ModelLimits;
22use thiserror::Error;
23
24use super::canonical::CanonicalRequest;
25use super::canonical_response::{CanonicalEvent, CanonicalResponse};
26
27/// Upstream provider failure, carried inside the `anyhow::Error` an adapter
28/// returns so the route layer can recover the real HTTP status by downcast
29/// instead of flattening every failure to 502.
30#[derive(Debug, Error)]
31pub enum UpstreamError {
32    #[error("{provider} returned {status}: {message}")]
33    Status {
34        provider: String,
35        status: u16,
36        message: String,
37    },
38    #[error("{provider} request failed: {source}")]
39    Transport {
40        provider: String,
41        #[source]
42        source: reqwest::Error,
43    },
44}
45
46pub fn extract_upstream_message(body: &str) -> String {
47    serde_json::from_str::<serde_json::Value>(body)
48        .ok()
49        .and_then(|v| v["error"]["message"].as_str().map(ToOwned::to_owned))
50        .unwrap_or_else(|| body.chars().take(500).collect())
51}
52
53#[derive(Debug)]
54pub struct OutboundCtx<'a> {
55    pub route: &'a GatewayRoute,
56    pub endpoint: &'a str,
57    pub api_key: &'a str,
58    pub request: &'a CanonicalRequest,
59    pub upstream_model: &'a str,
60    pub model_limits: Option<ModelLimits>,
61}
62
63#[expect(
64    missing_debug_implementations,
65    reason = "variants hold streaming bodies that intentionally do not implement Debug"
66)]
67pub enum OutboundOutcome {
68    Buffered(Box<CanonicalResponse>),
69    Streaming(BoxStream<'static, Result<CanonicalEvent, String>>),
70}
71
72// Why: #[async_trait] is required — the upstream registry stores adapters as
73// `Arc<dyn OutboundAdapter>`, so the trait must stay dyn-compatible.
74#[async_trait]
75pub trait OutboundAdapter: Send + Sync {
76    async fn send(&self, ctx: OutboundCtx<'_>) -> Result<OutboundOutcome>;
77}
78
79#[derive(Debug, Clone, Copy)]
80pub struct OutboundAdapterRegistration {
81    pub tag: &'static str,
82    pub factory: fn() -> Arc<dyn OutboundAdapter>,
83}
84
85inventory::collect!(OutboundAdapterRegistration);