Skip to main content

fxrs_core/
gateway.rs

1use std::sync::Arc;
2
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6use crate::{BoxFuture, ChatMessage, ToolCall, Usage};
7
8#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
9#[serde(rename_all = "snake_case")]
10pub enum ToolChoice {
11    #[default]
12    Auto,
13    None,
14    Required,
15}
16
17#[derive(Clone, Debug, PartialEq, Serialize)]
18pub struct GatewayRequest {
19    pub model: String,
20    pub messages: Vec<ChatMessage>,
21    pub tools: Vec<ToolAdvertisement>,
22    pub tool_choice: ToolChoice,
23    pub max_output_tokens: Option<u32>,
24}
25
26#[derive(Clone, Debug, PartialEq, Serialize)]
27pub struct ToolAdvertisement {
28    pub name: String,
29    pub description: String,
30    pub input_schema: serde_json::Value,
31    pub kind: ToolAdvertisementKind,
32}
33
34impl ToolAdvertisement {
35    pub fn function(
36        name: impl Into<String>,
37        description: impl Into<String>,
38        input_schema: serde_json::Value,
39    ) -> Self {
40        Self {
41            name: name.into(),
42            description: description.into(),
43            input_schema,
44            kind: ToolAdvertisementKind::Function,
45        }
46    }
47
48    pub fn provider(
49        id: impl Into<String>,
50        name: impl Into<String>,
51        arguments: serde_json::Value,
52    ) -> Self {
53        Self {
54            name: name.into(),
55            description: String::new(),
56            input_schema: serde_json::Value::Null,
57            kind: ToolAdvertisementKind::Provider {
58                id: id.into(),
59                arguments,
60            },
61        }
62    }
63}
64
65/// How a provider should project a tool advertisement on its wire protocol.
66///
67/// Local fx tools are ordinary functions. Provider tools are executed by the
68/// model provider and carry provider-owned configuration instead of a JSON
69/// input schema.
70#[derive(Clone, Debug, PartialEq, Serialize)]
71pub enum ToolAdvertisementKind {
72    Function,
73    Provider {
74        id: String,
75        arguments: serde_json::Value,
76    },
77}
78
79#[derive(Clone, Debug, Default, PartialEq)]
80pub struct GatewayResponse {
81    pub content: Option<String>,
82    pub tool_calls: Vec<ToolCall>,
83    pub generation_id: Option<String>,
84    pub finish_reason: Option<crate::FinishReason>,
85    pub usage: Usage,
86    /// True when request delivery may have incurred cost but no reliable
87    /// generation identity was recovered.
88    pub delivery_ambiguous: bool,
89}
90
91#[derive(Clone, Debug, PartialEq)]
92pub enum GatewayEvent {
93    ContentDelta(String),
94    ReasoningDelta(String),
95    ToolStarted { id: String, name: String },
96}
97
98pub trait GatewayEventSink: Send {
99    fn emit(&mut self, event: GatewayEvent);
100}
101
102#[derive(Debug, Error)]
103pub enum GatewayError {
104    #[error("request was cancelled")]
105    Cancelled,
106    #[error("authentication failed")]
107    Authentication,
108    #[error("provider rejected the request: {0}")]
109    Rejected(String),
110    #[error("gateway transport failed before delivery")]
111    DefinitelyUnsent,
112    #[error("gateway transport failed after possible delivery")]
113    PossiblySent,
114    #[error("gateway response was invalid: {0}")]
115    InvalidResponse(String),
116    #[error("gateway is unavailable: {0}")]
117    Unavailable(String),
118}
119
120/// Provider boundary for model streaming.
121///
122/// The trait owns semantic retries; HTTP adapters may only retry failures that
123/// are known to be unsent. This avoids duplicate billed generations.
124pub trait Gateway: Send + Sync {
125    fn complete<'a>(
126        &'a self,
127        request: GatewayRequest,
128        events: &'a mut dyn GatewayEventSink,
129    ) -> BoxFuture<'a, Result<GatewayResponse, GatewayError>>;
130}
131
132/// Provider-neutral retry boundary for requests proven not to have been sent.
133///
134/// `PossiblySent`, protocol errors, rejections, and invalid streams are never
135/// replayed because they may represent a billed generation or partial output.
136#[derive(Clone)]
137pub struct SafeRetryGateway {
138    inner: Arc<dyn Gateway>,
139    max_unsent_retries: usize,
140}
141
142impl SafeRetryGateway {
143    pub fn new(inner: Arc<dyn Gateway>) -> Self {
144        Self {
145            inner,
146            max_unsent_retries: 1,
147        }
148    }
149
150    pub fn with_max_unsent_retries(mut self, max_unsent_retries: usize) -> Self {
151        self.max_unsent_retries = max_unsent_retries;
152        self
153    }
154}
155
156impl Gateway for SafeRetryGateway {
157    fn complete<'a>(
158        &'a self,
159        request: GatewayRequest,
160        events: &'a mut dyn GatewayEventSink,
161    ) -> BoxFuture<'a, Result<GatewayResponse, GatewayError>> {
162        Box::pin(async move {
163            let mut retries = 0usize;
164            loop {
165                match self.inner.complete(request.clone(), events).await {
166                    Err(GatewayError::DefinitelyUnsent) if retries < self.max_unsent_retries => {
167                        retries += 1;
168                    }
169                    result => return result,
170                }
171            }
172        })
173    }
174}
175
176impl std::fmt::Debug for SafeRetryGateway {
177    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178        formatter
179            .debug_struct("SafeRetryGateway")
180            .field("max_unsent_retries", &self.max_unsent_retries)
181            .finish_non_exhaustive()
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use std::collections::VecDeque;
188    use std::sync::Mutex;
189
190    use super::*;
191
192    struct ScriptedGateway(Mutex<VecDeque<Result<GatewayResponse, GatewayError>>>);
193
194    impl Gateway for ScriptedGateway {
195        fn complete<'a>(
196            &'a self,
197            _request: GatewayRequest,
198            _events: &'a mut dyn GatewayEventSink,
199        ) -> BoxFuture<'a, Result<GatewayResponse, GatewayError>> {
200            Box::pin(async move { self.0.lock().unwrap().pop_front().unwrap() })
201        }
202    }
203
204    struct Events;
205
206    impl GatewayEventSink for Events {
207        fn emit(&mut self, _event: GatewayEvent) {}
208    }
209
210    fn request() -> GatewayRequest {
211        GatewayRequest {
212            model: "model".into(),
213            messages: Vec::new(),
214            tools: Vec::new(),
215            tool_choice: ToolChoice::Auto,
216            max_output_tokens: None,
217        }
218    }
219
220    #[test]
221    fn retries_only_definitely_unsent_requests() {
222        let successful = Arc::new(ScriptedGateway(Mutex::new(VecDeque::from([
223            Err(GatewayError::DefinitelyUnsent),
224            Ok(GatewayResponse::default()),
225        ]))));
226        let retrying = SafeRetryGateway::new(successful.clone());
227        assert!(pollster::block_on(retrying.complete(request(), &mut Events)).is_ok());
228        assert!(successful.0.lock().unwrap().is_empty());
229
230        let ambiguous = Arc::new(ScriptedGateway(Mutex::new(VecDeque::from([
231            Err(GatewayError::PossiblySent),
232            Ok(GatewayResponse::default()),
233        ]))));
234        let retrying = SafeRetryGateway::new(ambiguous.clone());
235        assert!(matches!(
236            pollster::block_on(retrying.complete(request(), &mut Events)),
237            Err(GatewayError::PossiblySent)
238        ));
239        assert_eq!(ambiguous.0.lock().unwrap().len(), 1);
240    }
241}