systemprompt_api/services/gateway/protocol/outbound/
mod.rs1pub 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#[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#[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);