clawft_kernel/cog_ingest/
types.rs1use serde_json::Value;
8
9pub const DIMS: usize = 8;
11pub const MAX_VECTORS_PER_BATCH: usize = 256;
13pub const MAX_BODY_BYTES: usize = 64 * 1024;
15pub const INGEST_PATH: &str = "/api/v1/store/ingest";
17
18#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
20pub struct IngestVector {
21 pub id: u64,
23 pub values: [f32; DIMS],
25}
26
27#[derive(Debug, Clone, PartialEq)]
29pub struct IngestBatch {
30 pub vectors: Vec<IngestVector>,
32 pub dedup: bool,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
38pub enum IngestError {
39 #[error("malformed request: {0}")]
41 Malformed(String),
42 #[error("too large: {0}")]
44 TooLarge(String),
45 #[error("unauthorized")]
47 Unauthorized,
48 #[error("forbidden")]
50 Forbidden,
51 #[error("rate limited")]
53 RateLimited,
54 #[error("store unavailable: {0}")]
56 Unavailable(String),
57 #[error("not routed: {0}")]
59 NotRouted(String),
60}
61
62impl IngestError {
63 pub fn status(&self) -> u16 {
65 match self {
66 Self::Malformed(_) => 400,
67 Self::Unauthorized => 401,
68 Self::Forbidden => 403,
69 Self::TooLarge(_) => 413,
70 Self::RateLimited => 429,
71 Self::Unavailable(_) | Self::NotRouted(_) => 502,
72 }
73 }
74}
75
76fn bad(m: impl Into<String>) -> IngestError {
77 IngestError::Malformed(m.into())
78}
79
80pub fn parse_batch(body: &[u8]) -> Result<IngestBatch, IngestError> {
82 if body.len() > MAX_BODY_BYTES {
83 return Err(IngestError::TooLarge(format!(
84 "body over {MAX_BODY_BYTES} bytes"
85 )));
86 }
87 let v: Value = serde_json::from_slice(body).map_err(|e| bad(format!("not JSON: {e}")))?;
88 let obj = v.as_object().ok_or_else(|| bad("body must be an object"))?;
89 if let Some(k) = obj.keys().find(|k| *k != "vectors" && *k != "dedup") {
90 return Err(bad(format!("unknown key `{k}`")));
91 }
92 let dedup = match obj.get("dedup") {
93 None => false,
94 Some(Value::Bool(b)) => *b,
95 Some(_) => return Err(bad("`dedup` must be a boolean")),
96 };
97 let arr = obj
98 .get("vectors")
99 .and_then(Value::as_array)
100 .ok_or_else(|| bad("`vectors` must be an array"))?;
101 if arr.is_empty() {
102 return Err(bad("`vectors` is empty"));
103 }
104 if arr.len() > MAX_VECTORS_PER_BATCH {
105 return Err(IngestError::TooLarge(format!(
106 "{} vectors, at most {MAX_VECTORS_PER_BATCH}",
107 arr.len()
108 )));
109 }
110 let vectors = arr
111 .iter()
112 .enumerate()
113 .map(|(i, e)| parse_vector(e).map_err(|m| bad(format!("vectors[{i}]: {m}"))))
114 .collect::<Result<Vec<_>, _>>()?;
115 Ok(IngestBatch { vectors, dedup })
116}
117
118fn parse_vector(e: &Value) -> Result<IngestVector, String> {
119 let pair = e.as_array().filter(|p| p.len() == 2);
120 let [id, vals] = pair.map(|p| [&p[0], &p[1]]).ok_or("expected [id, [values]]")?;
121 let id = id.as_u64().ok_or("id must be a non-negative integer")?;
122 let vals = vals.as_array().ok_or("values must be an array")?;
123 if vals.len() != DIMS {
124 return Err(format!("{} values, expected {DIMS}", vals.len()));
125 }
126 let mut values = [0f32; DIMS];
127 for (slot, x) in values.iter_mut().zip(vals) {
128 let f = x.as_f64().ok_or("values must be numbers")? as f32;
129 if !f.is_finite() {
130 return Err("values must be finite f32".into());
131 }
132 *slot = f;
133 }
134 Ok(IngestVector { id, values })
135}
136
137pub fn valid_project_id(s: &str) -> bool {
140 clawft_types::project::validate_id(s).is_ok()
141}