1use serde_json::{Map, Value, json};
2use sim_cookbook::fnv1a64;
3use sim_kernel::{Error, Expr, Symbol};
4
5use crate::{
6 clock::{SystemWallClock, WallClock},
7 content_id::content_id_for_expr,
8 objects::{GatewayRequest, GatewayResponse},
9 plan::{check_plan, parse_plan, resolve_atom_address, shape::plan_parts},
10 server::GatewayRouteState,
11 storage::GatewayStore,
12};
13
14use super::{
15 errors::OpenAiRouteError,
16 execution_record::{EventInput, EventLog, RunPrologue, append_event, begin_run},
17};
18
19pub use super::execution_record::{
23 GatewayRunExecution as EmbeddingExecution, GatewayRunIdGenerators as EmbeddingIdGenerators,
24};
25
26pub const EMBEDDINGS_PATH: &str = "/v1/embeddings";
28pub const TENSOR_F64_SMALL_EMBEDDING_MODEL: &str = "sim/embed/tensor-f64-small";
30
31const TENSOR_F64_SMALL_DIMENSION: usize = 8;
32const EMBEDDING_SCALE: u64 = 1_000_000;
33
34type RouteResult<T> = std::result::Result<T, OpenAiRouteError>;
35
36#[derive(Clone, Debug)]
37struct EmbeddingModel {
38 id: String,
39 dimension: usize,
40 runner: Symbol,
41}
42
43#[derive(Clone, Debug)]
44struct EmbeddingUsage {
45 prompt_tokens: u64,
46 total_tokens: u64,
47}
48
49pub fn handle_embeddings(request: &GatewayRequest, state: &GatewayRouteState) -> GatewayResponse {
52 let mut clock = SystemWallClock;
53 let seed = clock.now_ms().unwrap_or(1);
54 let mut ids = EmbeddingIdGenerators::deterministic(seed);
55 match state.store().lock() {
56 Ok(mut store) => execute_embedding_request(&mut *store, &mut ids, &mut clock, request)
57 .response()
58 .clone(),
59 Err(err) => OpenAiRouteError::internal_message(format!("gateway store lock failed: {err}"))
60 .into_response(),
61 }
62}
63
64pub fn execute_embedding_request<S, C>(
70 store: &mut S,
71 ids: &mut EmbeddingIdGenerators,
72 clock: &mut C,
73 request: &GatewayRequest,
74) -> EmbeddingExecution
75where
76 S: GatewayStore,
77 C: WallClock,
78{
79 match try_execute_embedding_request(store, ids, clock, request) {
80 Ok(execution) => execution,
81 Err(error) => EmbeddingExecution::error(error),
82 }
83}
84
85fn try_execute_embedding_request<S, C>(
86 store: &mut S,
87 ids: &mut EmbeddingIdGenerators,
88 clock: &mut C,
89 request: &GatewayRequest,
90) -> RouteResult<EmbeddingExecution>
91where
92 S: GatewayStore,
93 C: WallClock,
94{
95 let object = request_object(request.body())?;
96 let model = required_string(&object, "model")?.to_owned();
97 let inputs = embedding_inputs(&object)?;
98 let record_execution = object.get("store").and_then(Value::as_bool).unwrap_or(true);
99
100 let plan = parse_plan(&model).map_err(OpenAiRouteError::bad_model_from_error)?;
101 check_plan(&plan).map_err(OpenAiRouteError::bad_model_from_error)?;
102 let embedding_model = embedding_model_from_plan(&plan, &model)?;
103
104 let RunPrologue {
105 recorded_request,
106 request_content_id,
107 run_id,
108 run_content_id,
109 } = begin_run(store, ids, clock, request, None, record_execution)?;
110
111 let embeddings = inputs
112 .iter()
113 .map(|input| embedding_for_text(&embedding_model.id, input, embedding_model.dimension))
114 .collect::<Vec<_>>();
115 let usage = embedding_usage(&inputs);
116
117 let mut event_log = EventLog::default();
118 append_event(
119 store,
120 ids,
121 clock,
122 &run_id,
123 EventInput::new(0, "request-start", recorded_request.to_expr()),
124 record_execution,
125 &mut event_log,
126 )?;
127 append_event(
128 store,
129 ids,
130 clock,
131 &run_id,
132 EventInput::new(1, "plan-start", plan.clone()),
133 record_execution,
134 &mut event_log,
135 )?;
136 append_event(
137 store,
138 ids,
139 clock,
140 &run_id,
141 EventInput::new(2, "model-start", Expr::String(embedding_model.id.clone())),
142 record_execution,
143 &mut event_log,
144 )?;
145 append_event(
146 store,
147 ids,
148 clock,
149 &run_id,
150 EventInput::new(
151 3,
152 "embedding",
153 embedding_event_expr(&embedding_model, inputs.len()),
154 ),
155 record_execution,
156 &mut event_log,
157 )?;
158 append_event(
159 store,
160 ids,
161 clock,
162 &run_id,
163 EventInput::new(4, "usage", usage_expr(&usage)),
164 record_execution,
165 &mut event_log,
166 )?;
167
168 let response_body = embedding_response_body(&embedding_model.id, &embeddings, &usage)?;
169 let response = GatewayResponse::json(200, response_body);
170 append_event(
171 store,
172 ids,
173 clock,
174 &run_id,
175 EventInput::new(
176 5,
177 "final",
178 final_event_expr(&embedding_model, inputs.len(), &usage),
179 ),
180 record_execution,
181 &mut event_log,
182 )?;
183 let response_content_id = if record_execution {
184 let id = content_id_for_expr(&response.to_expr()).map_err(OpenAiRouteError::internal)?;
185 store
186 .put_response(id.clone(), response.clone())
187 .map_err(OpenAiRouteError::internal)?;
188 Some(id)
189 } else {
190 None
191 };
192
193 Ok(EmbeddingExecution {
194 response,
195 request_content_id: Some(request_content_id),
196 run_content_id: Some(run_content_id),
197 event_content_ids: event_log.content_ids,
198 events: event_log.events,
199 response_id: None,
200 response_created_at_ms: None,
201 response_content_id,
202 })
203}
204
205use crate::routes::request_json::{request_object, required_string};
206
207fn embedding_inputs(object: &Map<String, Value>) -> RouteResult<Vec<String>> {
208 match object.get("input") {
209 Some(Value::String(input)) => Ok(vec![input.clone()]),
210 Some(Value::Array(inputs)) => inputs
211 .iter()
212 .map(|input| match input {
213 Value::String(text) => Ok(text.clone()),
214 _ => Err(OpenAiRouteError::bad_request(
215 "embeddings input list must contain only strings",
216 Some("input"),
217 "invalid_input",
218 )),
219 })
220 .collect(),
221 Some(_) => Err(OpenAiRouteError::bad_request(
222 "embeddings input must be a string or list of strings",
223 Some("input"),
224 "invalid_input",
225 )),
226 None => Err(OpenAiRouteError::missing_required("input")),
227 }
228}
229
230fn embedding_model_from_plan(plan: &Expr, model: &str) -> RouteResult<EmbeddingModel> {
231 let (name, args) = plan_parts(plan).map_err(OpenAiRouteError::bad_model_from_error)?;
232 if name != "atom" {
233 return Err(OpenAiRouteError::bad_request(
234 "embeddings model must be a plan atom",
235 Some("model"),
236 "invalid_model",
237 ));
238 }
239 let [Expr::String(address)] = args else {
240 return Err(OpenAiRouteError::bad_model_from_error(Error::Eval(
241 "plan/atom expects one address".to_owned(),
242 )));
243 };
244 let descriptor =
245 resolve_atom_address(address).map_err(|err| OpenAiRouteError::model(err, model))?;
246 if !descriptor.address.starts_with("sim/embed/") {
247 return Err(model_not_found(model));
248 }
249 let dimension = match descriptor.address.as_str() {
250 TENSOR_F64_SMALL_EMBEDDING_MODEL => TENSOR_F64_SMALL_DIMENSION,
251 _ => return Err(model_not_found(model)),
252 };
253 Ok(EmbeddingModel {
254 id: descriptor.address,
255 dimension,
256 runner: descriptor.runner,
257 })
258}
259
260fn model_not_found(model: &str) -> OpenAiRouteError {
261 OpenAiRouteError::model(Error::Eval(format!("model_not_found: {model}")), model)
262}
263
264fn embedding_for_text(model: &str, input: &str, dimension: usize) -> Vec<f64> {
265 (0..dimension)
266 .map(|index| hash_to_unit(stable_embedding_hash(model, input, index)))
267 .collect()
268}
269
270fn stable_embedding_hash(model: &str, input: &str, dimension_index: usize) -> u64 {
271 let mut bytes =
272 Vec::with_capacity(model.len() + input.len() + 2 + std::mem::size_of::<usize>());
273 bytes.extend_from_slice(model.as_bytes());
274 bytes.push(0xff);
275 bytes.extend_from_slice(input.as_bytes());
276 bytes.push(0xfe);
277 bytes.extend_from_slice(&dimension_index.to_le_bytes());
278 fnv1a64(&bytes)
279}
280fn hash_to_unit(hash: u64) -> f64 {
281 let bucket = hash % (EMBEDDING_SCALE * 2 + 1);
282 (bucket as f64 / EMBEDDING_SCALE as f64) - 1.0
283}
284fn embedding_usage(inputs: &[String]) -> EmbeddingUsage {
285 let prompt_tokens = inputs
286 .iter()
287 .map(|input| input.split_whitespace().count() as u64)
288 .sum();
289 EmbeddingUsage {
290 prompt_tokens,
291 total_tokens: prompt_tokens,
292 }
293}
294
295fn embedding_response_body(
296 model: &str,
297 embeddings: &[Vec<f64>],
298 usage: &EmbeddingUsage,
299) -> RouteResult<Vec<u8>> {
300 Ok(crate::objects::canonical_json_bytes(json!({
301 "object": "list",
302 "data": embeddings
303 .iter()
304 .enumerate()
305 .map(|(index, embedding)| {
306 json!({
307 "object": "embedding",
308 "embedding": embedding,
309 "index": index,
310 })
311 })
312 .collect::<Vec<_>>(),
313 "model": model,
314 "usage": {
315 "prompt_tokens": usage.prompt_tokens,
316 "total_tokens": usage.total_tokens,
317 },
318 })))
319}
320
321fn embedding_event_expr(model: &EmbeddingModel, input_count: usize) -> Expr {
322 Expr::Map(vec![
323 field("model", Expr::String(model.id.clone())),
324 field("runner", Expr::Symbol(model.runner.clone())),
325 field("input-count", Expr::String(input_count.to_string())),
326 field("dimension", Expr::String(model.dimension.to_string())),
327 ])
328}
329
330fn usage_expr(usage: &EmbeddingUsage) -> Expr {
334 Expr::Map(vec![
335 field(
336 "prompt-tokens",
337 Expr::String(usage.prompt_tokens.to_string()),
338 ),
339 field("total-tokens", Expr::String(usage.total_tokens.to_string())),
340 ])
341}
342
343fn final_event_expr(model: &EmbeddingModel, input_count: usize, usage: &EmbeddingUsage) -> Expr {
344 Expr::Map(vec![
345 field("model", Expr::String(model.id.clone())),
346 field("object", Expr::String("list".to_owned())),
347 field("input-count", Expr::String(input_count.to_string())),
348 field("dimension", Expr::String(model.dimension.to_string())),
349 field("usage", usage_expr(usage)),
350 ])
351}
352
353use sim_value::build::entry as field;