lib.rsannotatedlib.rssource95 lines · 3.7 KB · raw
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}