Skip to main content

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

1//! Inbound protocol adapters: caller wire format to canonical model.
2//!
3//! The [`InboundAdapter`] trait parses a request body into a
4//! [`CanonicalRequest`] and renders canonical responses, streaming events, and
5//! errors back in the caller's protocol. Implementations cover the Anthropic
6//! Messages and `OpenAI` Responses surfaces; [`InboundParseError`] reports
7//! malformed or unsupported inputs.
8//!
9//! Copyright (c) systemprompt.io — Business Source License 1.1.
10//! See <https://systemprompt.io> for licensing details.
11
12pub mod anthropic_messages;
13pub mod openai_responses;
14
15use bytes::Bytes;
16use http::StatusCode;
17use systemprompt_models::profile::WireProtocol;
18
19use super::canonical::CanonicalRequest;
20use super::canonical_response::{CanonicalEvent, CanonicalResponse};
21
22#[derive(Debug, thiserror::Error)]
23pub enum InboundParseError {
24    #[error("invalid request body: {0}")]
25    InvalidJson(String),
26    #[error("missing required field: {0}")]
27    MissingField(&'static str),
28    #[error("unsupported value for {field}: {detail}")]
29    Unsupported { field: &'static str, detail: String },
30}
31
32pub trait InboundAdapter: Send + Sync + std::fmt::Debug {
33    fn wire_name(&self) -> &'static str;
34
35    /// A rebuild from [`CanonicalRequest`] silently drops beta-gated fields
36    /// (`context_management`, `output_config`, …) that clients pair with an
37    /// `anthropic-beta` header, so a same-protocol route forwards the original
38    /// bytes instead.
39    fn passthrough_wire(&self) -> Option<WireProtocol> {
40        None
41    }
42
43    fn parse_request(&self, raw: &Bytes) -> Result<CanonicalRequest, InboundParseError>;
44    fn render_response(&self, response: &CanonicalResponse) -> Bytes;
45    fn render_event(&self, event: &CanonicalEvent, model: &str) -> Option<Bytes>;
46
47    /// For wires whose terminal frame must embed fully-accumulated content the
48    /// per-event [`CanonicalEvent`] does not carry; `None` falls back to
49    /// [`InboundAdapter::render_event`].
50    fn render_terminal_event(
51        &self,
52        event: &CanonicalEvent,
53        snapshot: &CanonicalResponse,
54        model: &str,
55    ) -> Option<Bytes> {
56        // Why: unused-arg suppression in a default trait method body.
57        let _ = (event, snapshot, model);
58        None
59    }
60
61    fn render_error(&self, status: StatusCode, message: &str) -> Bytes;
62    fn streaming_content_type(&self) -> &'static str {
63        "text/event-stream"
64    }
65}