cortexkit_model_batch/
admission.rs1use serde_json::Number;
6
7use crate::errors::Refusal;
8
9pub const BATCH_ID_MAX_LEN: usize = 128;
11
12pub fn is_batch_id(batch_id: &str) -> bool {
15 !batch_id.is_empty()
16 && batch_id.len() <= BATCH_ID_MAX_LEN
17 && batch_id.bytes().all(|b| (0x20..=0x7e).contains(&b))
18}
19
20pub fn check_batch_id(batch_id: &str) -> Result<(), Refusal> {
22 if is_batch_id(batch_id) {
23 Ok(())
24 } else {
25 Err(Refusal::invalid_params(
26 "batch_id",
27 "batch_id is 1-128 printable ASCII characters",
28 ))
29 }
30}
31
32pub fn bounded(
35 count: usize,
36 min: usize,
37 max: usize,
38 field: &str,
39 what: &str,
40) -> Result<(), Refusal> {
41 if count < min {
42 Err(Refusal::over_limit(
43 field,
44 min as u64,
45 format!("{count} {what}; at least {min}"),
46 ))
47 } else if count > max {
48 Err(Refusal::over_limit(
49 field,
50 max as u64,
51 format!("{count} {what}; at most {max}"),
52 ))
53 } else {
54 Ok(())
55 }
56}
57
58pub fn check_request_size(len: usize, max_request_bytes: u64) -> Result<(), Refusal> {
62 if len as u64 > max_request_bytes {
63 Err(Refusal::over_limit(
64 "params",
65 max_request_bytes,
66 format!("the request is {len} bytes; at most {max_request_bytes}"),
67 ))
68 } else {
69 Ok(())
70 }
71}
72
73pub fn check_max_cost_usd(max_cost_usd: Option<&Number>) -> Result<(), Refusal> {
76 match max_cost_usd {
77 Some(max) if max.as_f64().is_none_or(|usd| usd < 0.0) => Err(Refusal::invalid_params(
78 "max_cost_usd",
79 "max_cost_usd is a number of USD, zero or more",
80 )),
81 _ => Ok(()),
82 }
83}
84
85pub fn effective_ceiling(recorded: Option<f64>, sent: Option<f64>) -> Option<f64> {
96 match (recorded, sent) {
97 (Some(recorded), Some(sent)) => Some(recorded.min(sent)),
98 (one, None) | (None, one) => one,
99 }
100}
101
102#[derive(Clone, Copy, Debug, PartialEq, Eq)]
105pub enum CatalogRow<'a> {
106 Absent,
108 Kindless,
110 Kind(&'a str),
112}
113
114pub fn check_catalog_row(
119 model: &str,
120 row: CatalogRow<'_>,
121 expected_kind: &str,
122 wrong_kind: impl FnOnce(&str, Option<&str>) -> Refusal,
123) -> Result<(), Refusal> {
124 match row {
125 CatalogRow::Absent => Err(Refusal::model_unknown(model)),
126 CatalogRow::Kind(kind) if kind == expected_kind => Ok(()),
127 CatalogRow::Kind(kind) => Err(wrong_kind(model, Some(kind))),
128 CatalogRow::Kindless => Err(wrong_kind(model, None)),
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use crate::errors::{MODEL_NOT_EXTRACTOR, MODEL_UNKNOWN};
136
137 #[test]
138 fn batch_ids_are_printable_ascii_up_to_128() {
139 assert!(is_batch_id("b-1"));
140 assert!(!is_batch_id(""));
141 assert!(!is_batch_id("tab\there"));
142 assert!(!is_batch_id(&"x".repeat(129)));
143 assert!(is_batch_id(&"x".repeat(128)));
144 }
145
146 #[test]
147 fn the_ceiling_tightens_and_never_loosens() {
148 let mut recorded = None;
149 let mut worked_under = Vec::new();
150 for sent in [5.0, 1.0, 5.0] {
151 recorded = effective_ceiling(recorded, Some(sent));
152 worked_under.push(recorded);
153 }
154 assert_eq!(worked_under, [Some(5.0), Some(1.0), Some(1.0)]);
155 assert_eq!(effective_ceiling(None, None), None);
156 }
157
158 #[test]
159 fn a_kindless_row_is_not_a_missing_row() {
160 let wrong = |model: &str, kind: Option<&str>| Refusal::model_not_extractor(model, kind);
161 let absent = check_catalog_row("m", CatalogRow::Absent, "extraction", wrong).unwrap_err();
162 assert_eq!(absent.code, MODEL_UNKNOWN);
163 let kindless =
164 check_catalog_row("m", CatalogRow::Kindless, "extraction", wrong).unwrap_err();
165 assert_eq!(kindless.code, MODEL_NOT_EXTRACTOR);
166 assert_eq!(kindless.detail.kind, None);
167 assert!(
168 check_catalog_row("m", CatalogRow::Kind("extraction"), "extraction", wrong).is_ok()
169 );
170 }
171}