The TypeSafe System One wire protocol (POST /v1/systemone), without
I/O and without a runtime: build a request's exact bytes, check a
response against the questions that were asked, and decide retries.
Any transport can drive it; postjevsql's drives it from inside a
Postgres backend.
Design points taken from prior art (both MIT OR Apache-2.0): answer keys typed by question kind, limits checked before sending, responses verified against the questions, and structured API errors come from JedimEmO/typesafe-client; fuzzed parsers from kunobi-ninja/kunobi-jev (Apache-2.0). Unlike both, the state is sent as caller-supplied bytes (RFC 8785 canonical for rows), so the bytes on the wire are exactly the bytes a cache key hashes.
14#![forbid(unsafe_code)]
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}
The endpoint path, relative to the API origin.
43pub const SYSTEM_ONE_PATH: &str = system_one_path!();
Where the evaluation endpoint is (digest §1). Every transport (jev-http's own connection, jev-worker's fetch) posts here unless pointed at a relay or a test server.
47pub const ENDPOINT: &str = concat!("https://api.typesafe.ai", system_one_path!());
The response header carrying the id to quote in a billing dispute.
49pub const REQUEST_ID_HEADER: &str = "x-typesafe-request-id";
The request header both vendor SDKs send on a retry.
51pub const RETRY_COUNT_HEADER: &str = "x-typesafe-retry-count";
The request id a response carries, if any.
Parsers meet bytes from the network: they must reject, never panic (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}