1//! The TypeSafe System One wire protocol (`POST /v1/systemone`), without 2//! I/O and without a runtime: build a request's exact bytes, check a 3//! response against the questions that were asked, and decide retries. 4//! Any transport can drive it; postjevsql's drives it from inside a 5//! Postgres backend. 6//! 7//! Design points taken from prior art (both MIT OR Apache-2.0): 8//! answer keys typed by question kind, limits checked before sending, 9//! responses verified against the questions, and structured API errors 10//! come from JedimEmO/typesafe-client; fuzzed parsers from 11//! kunobi-ninja/kunobi-jev (Apache-2.0). Unlike both, the state is sent 12//! as caller-supplied bytes (RFC 8785 canonical for rows), so the bytes 13//! on the wire are exactly the bytes a cache key hashes. 14#![forbid(unsafe_code)] 15 16mod answer; 17mod error; 18mod jcs; 19mod json; 20mod model; 21mod question; 22mod request; 23mod response; 24pub mod retry; 25 26pub use answer::{ChoiceAnswer, DOLLARS_PER_MTOK, NoulAnswer, ScoreAnswer, Usage}; 27pub use error::{ApiError, ApiErrorKind, FieldError, ProtocolError}; 28pub use json::Json; 29pub use model::ModelId; 30pub use question::{Choice, MAX_CHOICE_OPTIONS, Noul, Question, Score}; 31pub use request::{ 32 Key, MAX_REQUEST_TOKENS, Questions, question_bytes, request_bytes, worst_case_dollars, worst_case_tokens, 33}; 34pub use response::Response; 35 36macro_rules! system_one_path { 37 () => { 38 "/v1/systemone" 39 }; 40} 41 42/// The endpoint path, relative to the API origin. 43pub const SYSTEM_ONE_PATH: &str = system_one_path!(); 44/// Where the evaluation endpoint is (digest §1). Every transport (jev-http's 45/// own connection, jev-worker's fetch) posts here unless pointed at a relay 46/// or a test server. 47pub const ENDPOINT: &str = concat!("https://api.typesafe.ai", system_one_path!()); 48/// The response header carrying the id to quote in a billing dispute. 49pub const REQUEST_ID_HEADER: &str = "x-typesafe-request-id"; 50/// The request header both vendor SDKs send on a retry. 51pub const RETRY_COUNT_HEADER: &str = "x-typesafe-retry-count"; 52 53/// The request id a response carries, if any. 54pub fn request_id(headers: &http::HeaderMap) -> Option<String> { 55 headers.get(REQUEST_ID_HEADER).and_then(|v| v.to_str().ok()).map(str::to_owned) 56} 57 58/// Parsers meet bytes from the network: they must reject, never panic 59/// (kunobi-jev fuzzes the same three). 60#[cfg(test)] 61mod never_panics { 62 use proptest::prelude::*; 63 64 use super::*; 65 66 proptest! { 67 #[test] 68 fn response(body in proptest::collection::vec(any::<u8>(), 0..512)) { 69 let mut questions = Questions::new(); 70 questions.noul("q", Noul::new(Json::text("?"))).unwrap(); 71 let _ = Response::parse(&ModelId::pinned("jev-1.13.0").unwrap(), &questions, &body); 72 } 73 74 #[test] 75 fn api_error(status in 100u16..600, body in proptest::collection::vec(any::<u8>(), 0..512)) { 76 let _ = ApiError::from_response(status, &http::HeaderMap::new(), &body).to_string(); 77 } 78 79 #[test] 80 fn retry_after(ms in "[ -~]{0,24}", header in "[ -~]{0,40}", tries in 0u32..4, jitter in 0.0f64..1.0) { 81 let mut headers = http::HeaderMap::new(); 82 headers.insert("retry-after-ms", http::HeaderValue::from_str(&ms).unwrap()); 83 headers.insert("retry-after", http::HeaderValue::from_str(&header).unwrap()); 84 let outcome = retry::Outcome::Status { status: 429, headers: &headers }; 85 if let Some(d) = retry::next_delay(&outcome, tries, jitter, std::time::SystemTime::now()) { 86 prop_assert!(d <= std::time::Duration::from_secs(60)); 87 } 88 } 89 90 #[test] 91 fn canonical_json(text in ".{0,64}") { 92 let _ = Json::canonical(&text); 93 } 94 } 95}