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, `OpenAI` Responses, and `OpenAI` Chat Completions surfaces;
7//! [`InboundParseError`] reports 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_chat;
14pub mod openai_responses;
15
16use bytes::Bytes;
17use http::StatusCode;
18use systemprompt_models::services::WireProtocol;
19
20use super::canonical::CanonicalRequest;
21use super::canonical_response::{CanonicalEvent, CanonicalResponse};
22
23#[derive(Debug, thiserror::Error)]
24pub enum InboundParseError {
25    #[error("invalid request body: {0}")]
26    InvalidJson(String),
27    #[error("missing required field: {0}")]
28    MissingField(&'static str),
29    #[error("unsupported value for {field}: {detail}")]
30    Unsupported { field: &'static str, detail: String },
31}
32
33// Why: the wire contract names a client mistake `invalid_request_error`; the
34// generic `api_error` is reserved for the server's own faults, so a rendered
35// error must take its type from the status it is being sent with.
36pub(crate) fn error_type_for_status(status: StatusCode) -> &'static str {
37    match status {
38        StatusCode::UNAUTHORIZED => "authentication_error",
39        StatusCode::FORBIDDEN => "permission_error",
40        StatusCode::NOT_FOUND => "not_found_error",
41        StatusCode::TOO_MANY_REQUESTS => "rate_limit_error",
42        s if s.is_client_error() => "invalid_request_error",
43        _ => "api_error",
44    }
45}
46
47pub trait InboundAdapter: Send + Sync + std::fmt::Debug {
48    fn wire_name(&self) -> &'static str;
49
50    fn passthrough_wire(&self) -> Option<WireProtocol> {
51        None
52    }
53
54    fn parse_request(&self, raw: &Bytes) -> Result<CanonicalRequest, InboundParseError>;
55    fn render_response(&self, response: &CanonicalResponse) -> Bytes;
56    fn render_event(&self, event: &CanonicalEvent, model: &str) -> Option<Bytes>;
57
58    fn render_terminal_event(
59        &self,
60        _event: &CanonicalEvent,
61        _snapshot: &CanonicalResponse,
62        _model: &str,
63    ) -> Option<Bytes> {
64        None
65    }
66
67    fn wants_stream_usage(&self, _raw: &Bytes) -> bool {
68        false
69    }
70
71    fn render_stream_tail(
72        &self,
73        _snapshot: &CanonicalResponse,
74        _include_usage: bool,
75    ) -> Option<Bytes> {
76        None
77    }
78
79    fn render_error(&self, status: StatusCode, message: &str) -> Bytes;
80    fn streaming_content_type(&self) -> &'static str {
81        "text/event-stream"
82    }
83}