lib.rsannotatedlib.rssource95 lines · 3.7 KB · raw

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.

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}

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}