sim_lib_openai_server/runtime/
fabric.rs1use std::sync::{Arc, Mutex};
2
3use serde_json::{Map, Number, Value as JsonValue};
4use sim_citizen_derive::non_citizen;
5use sim_kernel::{
6 Consistency, Cx, Error, EvalFabric, EvalMode, EvalReply, EvalRequest, Expr, Object,
7 ObjectCompat, Result, Symbol, Value, eval_remote_capability,
8};
9
10use crate::{
11 clock::{DeterministicWallClock, SystemWallClock, WallClock, WallTimestamp},
12 objects::{GatewayRequest, GatewayResponseValue},
13 routes::responses::{RESPONSES_PATH, ResponseExecution, ResponseIdGenerators},
14 server::GatewayRouteState,
15};
16
17#[non_citizen(
24 reason = "live OpenAI gateway fabric handle; reconstruct route data via openai/GatewayRequest and openai/Plan descriptors",
25 kind = "handle",
26 descriptor = "openai/GatewayRequest"
27)]
28pub struct OpenAiGatewayFabric {
29 state: GatewayRouteState,
30 runtime: Mutex<GatewayFabricRuntime>,
31 codecs: Vec<Symbol>,
32}
33
34struct GatewayFabricRuntime {
35 ids: ResponseIdGenerators,
36 clock: GatewayFabricClock,
37 last_execution: Option<ResponseExecution>,
38}
39
40enum GatewayFabricClock {
41 System(SystemWallClock),
42 Deterministic(DeterministicWallClock),
43}
44
45impl OpenAiGatewayFabric {
46 pub fn memory() -> Self {
48 Self::with_state_system(GatewayRouteState::memory(), 1)
49 }
50
51 pub fn deterministic(id_start: u64, clock_start_ms: u64, clock_step_ms: u64) -> Self {
56 Self::with_state_clock(
57 GatewayRouteState::memory(),
58 ResponseIdGenerators::deterministic(id_start),
59 GatewayFabricClock::Deterministic(DeterministicWallClock::new(
60 clock_start_ms,
61 clock_step_ms,
62 )),
63 )
64 }
65
66 pub fn with_state_system(state: GatewayRouteState, id_seed: u64) -> Self {
70 Self::with_state_clock(
71 state,
72 ResponseIdGenerators::deterministic(id_seed),
73 GatewayFabricClock::System(SystemWallClock),
74 )
75 }
76
77 fn with_state_clock(
78 state: GatewayRouteState,
79 ids: ResponseIdGenerators,
80 clock: GatewayFabricClock,
81 ) -> Self {
82 Self {
83 state,
84 runtime: Mutex::new(GatewayFabricRuntime {
85 ids,
86 clock,
87 last_execution: None,
88 }),
89 codecs: vec![
90 Symbol::qualified("codec", "binary"),
91 Symbol::qualified("codec", "openai"),
92 ],
93 }
94 }
95
96 pub fn eval_request_for_gateway_request(request: &GatewayRequest) -> EvalRequest {
98 EvalRequest {
99 expr: Expr::Bytes(request.body().to_vec()),
100 result_shape: None,
101 required_capabilities: Vec::new(),
102 deadline: None,
103 consistency: Consistency::LocalFirst,
104 mode: EvalMode::Eval,
105 answer_limit: None,
106 stream_buffer: None,
107 stream: false,
108 trace: false,
109 }
110 }
111
112 pub fn last_execution(&self) -> Result<Option<ResponseExecution>> {
114 self.runtime
115 .lock()
116 .map_err(|_| Error::PoisonedLock("openai gateway fabric runtime"))
117 .map(|runtime| runtime.last_execution.clone())
118 }
119}
120
121impl Object for OpenAiGatewayFabric {
122 fn display(&self, _cx: &mut Cx) -> Result<String> {
123 Ok("#<openai-gateway-fabric>".to_owned())
124 }
125
126 fn as_any(&self) -> &dyn std::any::Any {
127 self
128 }
129}
130
131impl ObjectCompat for OpenAiGatewayFabric {
132 fn as_table(&self, cx: &mut Cx) -> Result<Value> {
133 cx.factory().table(vec![
134 (
135 Symbol::new("kind"),
136 cx.factory().symbol(Symbol::new("openai-gateway"))?,
137 ),
138 (
139 Symbol::new("address"),
140 cx.factory().symbol(Symbol::new("local"))?,
141 ),
142 (
143 Symbol::new("codecs"),
144 cx.factory().list(
145 self.codecs
146 .iter()
147 .cloned()
148 .map(|codec| cx.factory().symbol(codec))
149 .collect::<Result<Vec<_>>>()?,
150 )?,
151 ),
152 ])
153 }
154
155 fn as_eval_fabric(&self) -> Option<&dyn EvalFabric> {
156 Some(self)
157 }
158}
159
160impl EvalFabric for OpenAiGatewayFabric {
161 fn realize(&self, cx: &mut Cx, request: EvalRequest) -> Result<EvalReply> {
162 if matches!(request.consistency, Consistency::RemoteOnly) {
163 return Err(Error::CapabilityDenied {
164 capability: eval_remote_capability(),
165 });
166 }
167 if !matches!(request.mode, EvalMode::Eval) {
168 return Err(Error::Eval(
169 "openai gateway fabric only supports eval mode".to_owned(),
170 ));
171 }
172 for capability in &request.required_capabilities {
173 cx.require(capability)?;
174 }
175
176 let gateway_request = gateway_request_from_expr(&request.expr, request.stream)?;
177 let mut runtime = self
178 .runtime
179 .lock()
180 .map_err(|_| Error::PoisonedLock("openai gateway fabric runtime"))?;
181 let mut store = self
182 .state
183 .store()
184 .lock()
185 .map_err(|_| Error::PoisonedLock("openai gateway fabric store"))?;
186 let mut cache = self
187 .state
188 .cache()
189 .lock()
190 .map_err(|_| Error::PoisonedLock("openai gateway fabric cache"))?;
191 let runtime = &mut *runtime;
192 let execution =
193 crate::routes::responses::execute_response_request_with_cache_runners_and_federation(
194 cx,
195 &mut *store,
196 &mut cache,
197 &mut runtime.ids,
198 &mut runtime.clock,
199 &gateway_request,
200 crate::routes::responses::ResponseRuntimeTargets::with_federation(
201 self.state.runners(),
202 self.state.federation(),
203 ),
204 );
205 let response = execution.response().clone();
206 runtime.last_execution = Some(execution);
207 Ok(EvalReply {
208 value: cx
209 .factory()
210 .opaque(Arc::new(GatewayResponseValue::new(response)))?,
211 diagnostics: cx.take_diagnostics(),
212 trace: request
213 .trace
214 .then(|| cx.factory().symbol(Symbol::new("openai-gateway")))
215 .transpose()?,
216 })
217 }
218}
219
220#[cfg(feature = "http")]
221impl sim_lib_server::EvalSite for OpenAiGatewayFabric {
222 fn site_kind(&self) -> &'static str {
223 "openai-gateway"
224 }
225
226 fn address(&self) -> &sim_lib_server::ServerAddress {
227 static ADDRESS: sim_lib_server::ServerAddress = sim_lib_server::ServerAddress::Local;
228 &ADDRESS
229 }
230
231 fn codecs(&self) -> &[Symbol] {
232 &self.codecs
233 }
234
235 fn answer(
236 &self,
237 cx: &mut Cx,
238 frame: sim_lib_server::ServerFrame,
239 ) -> Result<sim_lib_server::ServerFrame> {
240 let consistency = frame.envelope.consistency;
241 let reply_codec = frame
242 .envelope
243 .reply_codec_hint
244 .clone()
245 .filter(|hint| self.codecs.iter().any(|codec| codec == hint))
246 .unwrap_or_else(|| self.codecs[0].clone());
247 let request = sim_lib_server::eval_request_from_frame(cx, &frame)?;
248 let reply = self.realize(cx, request)?;
249 sim_lib_server::server_frame_from_reply(cx, &reply_codec, reply, consistency)
250 }
251
252 fn as_eval_fabric(&self) -> Option<&dyn EvalFabric> {
253 Some(self)
254 }
255
256 fn as_any(&self) -> &dyn std::any::Any {
257 self
258 }
259}
260
261impl WallClock for GatewayFabricClock {
262 fn now(&self) -> Result<WallTimestamp> {
263 match self {
264 Self::System(clock) => clock.now(),
265 Self::Deterministic(clock) => clock.now(),
266 }
267 }
268}
269
270fn gateway_request_from_expr(expr: &Expr, stream: bool) -> Result<GatewayRequest> {
271 let mut body = match expr {
272 Expr::Bytes(bytes) => bytes.clone(),
273 Expr::String(text) => text.as_bytes().to_vec(),
274 Expr::Map(_) => {
275 let mut json = json_from_expr(expr)?;
276 if stream && let Some(object) = json.as_object_mut() {
277 object.insert("stream".to_owned(), JsonValue::Bool(true));
278 }
279 crate::objects::canonical_json_bytes(json)
280 }
281 _ => {
282 return Err(Error::TypeMismatch {
283 expected: "OpenAI gateway request bytes, JSON string, or map",
284 found: "non-request",
285 });
286 }
287 };
288 if stream && !matches!(expr, Expr::Map(_)) {
289 body = ensure_streaming_body(body)?;
290 }
291 Ok(GatewayRequest::new(
292 "POST",
293 RESPONSES_PATH,
294 vec![("Content-Type".to_owned(), "application/json".to_owned())],
295 body,
296 ))
297}
298
299fn ensure_streaming_body(body: Vec<u8>) -> Result<Vec<u8>> {
300 let mut json = serde_json::from_slice::<JsonValue>(&body)
301 .map_err(|err| Error::Eval(format!("invalid OpenAI gateway request JSON: {err}")))?;
302 let Some(object) = json.as_object_mut() else {
303 return Err(Error::Eval(
304 "OpenAI gateway request JSON must be an object".to_owned(),
305 ));
306 };
307 object.insert("stream".to_owned(), JsonValue::Bool(true));
308 Ok(crate::objects::canonical_json_bytes(json))
309}
310
311fn json_from_expr(expr: &Expr) -> Result<JsonValue> {
312 Ok(match expr {
313 Expr::Nil => JsonValue::Null,
314 Expr::Bool(flag) => JsonValue::Bool(*flag),
315 Expr::Number(number) => number
316 .canonical
317 .parse::<i64>()
318 .ok()
319 .map(Number::from)
320 .map(JsonValue::Number)
321 .unwrap_or_else(|| JsonValue::String(number.canonical.clone())),
322 Expr::String(text) => JsonValue::String(text.clone()),
323 Expr::Symbol(symbol) | Expr::Local(symbol) => JsonValue::String(symbol.to_string()),
324 Expr::List(items) | Expr::Vector(items) => JsonValue::Array(
325 items
326 .iter()
327 .map(json_from_expr)
328 .collect::<Result<Vec<_>>>()?,
329 ),
330 Expr::Map(entries) => {
331 let mut object = Map::new();
332 for (key, value) in entries {
333 object.insert(json_key(key)?, json_from_expr(value)?);
334 }
335 JsonValue::Object(object)
336 }
337 _ => {
338 return Err(Error::TypeMismatch {
339 expected: "JSON-compatible request expression",
340 found: "non-json",
341 });
342 }
343 })
344}
345
346fn json_key(expr: &Expr) -> Result<String> {
347 match expr {
348 Expr::String(text) => Ok(text.clone()),
349 Expr::Symbol(symbol) | Expr::Local(symbol) if symbol.namespace.is_none() => {
350 Ok(symbol.name.as_ref().to_owned())
351 }
352 Expr::Symbol(symbol) | Expr::Local(symbol) => Ok(symbol.to_string()),
353 _ => Err(Error::TypeMismatch {
354 expected: "JSON object key",
355 found: "non-key",
356 }),
357 }
358}