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