1use std::collections::BTreeMap;
17
18use serde::{Deserialize, Serialize};
19
20use crate::descriptor::BackendDescriptor;
21use crate::ids::{ArtifactId, DescriptorId};
22use crate::limits::{EXCHANGE_V1, REMOTE_V1};
23use crate::output::RawOutput;
24use crate::run::{Charge, CostBound};
25use crate::schema;
26
27pub const PROTOCOL_MAJOR: u32 = 1;
29pub const PROTOCOL: &str = "rustev.remote/1";
31pub const PROTOCOL_MAJOR_V2: u32 = 2;
35pub const PROTOCOL_V2: &str = "rustev.remote/2";
36pub const MAX_PRINCIPAL_BYTES: usize = 128;
38pub const MAX_PRINCIPAL_LABEL_BYTES: usize = 64;
40pub const PRINCIPAL_DIGEST_TAG: &str = "rustev.principal-reference/1";
42
43pub const fn protocol_name(major: u32) -> Option<&'static str> {
45 match major {
46 PROTOCOL_MAJOR => Some(PROTOCOL),
47 PROTOCOL_MAJOR_V2 => Some(PROTOCOL_V2),
48 _ => None,
49 }
50}
51pub const MAX_TEXT_BYTES: usize = 256;
54pub const MAX_ITEMS: usize = 32;
57pub const MAX_ROUTE: usize = 8;
59pub const MAX_ENTRIES: usize = 16;
62
63pub fn cut_text(s: &str) -> String {
65 cut_to(s, MAX_TEXT_BYTES)
66}
67
68pub fn cut_to(s: &str, max: usize) -> String {
70 if s.len() <= max {
71 return s.to_string();
72 }
73 let mut end = max;
74 while !s.is_char_boundary(end) {
75 end -= 1;
76 }
77 s[..end].to_string()
78}
79
80#[derive(
87 Debug,
88 Clone,
89 Copy,
90 PartialEq,
91 Eq,
92 PartialOrd,
93 Ord,
94 Hash,
95 Serialize,
96 Deserialize,
97 schemars::JsonSchema,
98)]
99#[serde(rename_all = "snake_case")]
100pub enum RemoteCode {
101 TransportUnsent,
102 TransportInterrupted,
103 RateLimited,
104 Overloaded,
105 Unauthorized,
106 Capability,
107 MalformedRequest,
108 MalformedResponse,
109 IdentityMismatch,
110 RemoteDeadline,
111 RemoteError,
112 Cancelled,
113}
114
115impl RemoteCode {
116 pub const ALL: [RemoteCode; 12] = [
118 RemoteCode::TransportUnsent,
119 RemoteCode::TransportInterrupted,
120 RemoteCode::RateLimited,
121 RemoteCode::Overloaded,
122 RemoteCode::Unauthorized,
123 RemoteCode::Capability,
124 RemoteCode::MalformedRequest,
125 RemoteCode::MalformedResponse,
126 RemoteCode::IdentityMismatch,
127 RemoteCode::RemoteDeadline,
128 RemoteCode::RemoteError,
129 RemoteCode::Cancelled,
130 ];
131
132 pub const fn as_str(self) -> &'static str {
134 match self {
135 RemoteCode::TransportUnsent => "transport_unsent",
136 RemoteCode::TransportInterrupted => "transport_interrupted",
137 RemoteCode::RateLimited => "rate_limited",
138 RemoteCode::Overloaded => "overloaded",
139 RemoteCode::Unauthorized => "unauthorized",
140 RemoteCode::Capability => "capability",
141 RemoteCode::MalformedRequest => "malformed_request",
142 RemoteCode::MalformedResponse => "malformed_response",
143 RemoteCode::IdentityMismatch => "identity_mismatch",
144 RemoteCode::RemoteDeadline => "remote_deadline",
145 RemoteCode::RemoteError => "remote_error",
146 RemoteCode::Cancelled => "cancelled",
147 }
148 }
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
153#[serde(rename_all = "snake_case")]
154pub enum CancelSupport {
155 None,
156 BestEffort,
158 Confirmed,
160}
161
162#[derive(
164 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
165)]
166#[serde(rename_all = "snake_case")]
167pub enum Disclosure {
168 Pinned,
170 Reported,
172 Unknown,
174}
175
176#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
179#[serde(deny_unknown_fields)]
180pub struct ServedTerms {
181 pub disclosure: Disclosure,
182 pub identity: Option<String>,
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
187#[serde(rename_all = "snake_case", deny_unknown_fields)]
188pub enum Batching {
189 None,
190 SharedState { max_items: u32 },
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
196#[serde(deny_unknown_fields)]
197pub struct RemoteTerms {
198 pub cost: CostBound,
201 pub refusals_charged: bool,
204 pub cancellation: CancelSupport,
205 pub served: ServedTerms,
206 pub batching: Batching,
207 pub max_request_bytes: u64,
208 pub max_response_bytes: u64,
209 pub idempotency_window_ms: u64,
211 pub privacy: Vec<String>,
213}
214
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
217#[serde(deny_unknown_fields)]
218pub struct ServedIdentity {
219 pub identity: Option<String>,
220 pub disclosure: Disclosure,
221}
222
223impl ServedIdentity {
224 pub fn unknown() -> Self {
226 ServedIdentity {
227 identity: None,
228 disclosure: Disclosure::Unknown,
229 }
230 }
231}
232
233#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
238#[serde(deny_unknown_fields)]
239pub struct DescribeRequest {
240 pub schema: String,
241 pub versions: Vec<u32>,
242}
243
244crate::document::document!(DescribeRequest, schema::REMOTE_DESCRIBE, REMOTE_V1);
245
246#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
249#[serde(deny_unknown_fields)]
250pub struct DescribeResponse {
251 pub schema: String,
252 pub version: u32,
253 pub backends: Vec<HostedBackend>,
254}
255
256crate::document::document!(DescribeResponse, schema::REMOTE_DESCRIBE, REMOTE_V1);
257
258#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
259#[serde(deny_unknown_fields)]
260pub struct HostedBackend {
261 pub descriptor: BackendDescriptor,
262 pub descriptor_id: DescriptorId,
263 pub terms: RemoteTerms,
264}
265
266#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
270#[serde(deny_unknown_fields)]
271pub struct InferRequest {
272 pub schema: String,
273 pub version: u32,
274 pub backend_id: String,
275 pub descriptor: DescriptorId,
277 pub artifact: ArtifactId,
279 pub budget_ms: u64,
281 pub trace_id: String,
282 pub privacy: BTreeMap<String, String>,
284 pub items: Vec<InferItem>,
285 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub principal: Option<WirePrincipal>,
291}
292
293crate::document::document!(InferRequest, schema::REMOTE_INFER, REMOTE_V1);
294
295impl InferRequest {
296 pub fn members_fit_version(&self) -> bool {
300 match &self.principal {
301 None => true,
302 Some(p) => self.version == PROTOCOL_MAJOR_V2 && p.is_canonical(),
303 }
304 }
305}
306
307#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
312#[serde(deny_unknown_fields)]
313pub struct WirePrincipal {
314 pub reference: String,
315 pub mapping_version: String,
316 pub scope: String,
317}
318
319impl std::fmt::Debug for WirePrincipal {
320 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
322 f.debug_struct("WirePrincipal")
323 .field("reference", &"<redacted>")
324 .field("mapping_version", &self.mapping_version)
325 .field("scope", &self.scope)
326 .finish()
327 }
328}
329
330fn is_label(s: &str) -> bool {
331 !s.is_empty() && s.len() <= MAX_PRINCIPAL_LABEL_BYTES && s.bytes().all(|b| b.is_ascii_graphic())
332}
333
334fn hex(bytes: &[u8]) -> String {
335 let mut s = String::with_capacity(bytes.len() * 2);
336 for b in bytes {
337 s.push_str(&format!("{b:02x}"));
338 }
339 s
340}
341
342impl WirePrincipal {
343 pub fn encode(reference: &[u8], mapping_version: &str, scope: &str) -> Option<Self> {
347 let p = WirePrincipal {
348 reference: hex(reference),
349 mapping_version: mapping_version.to_string(),
350 scope: scope.to_string(),
351 };
352 p.is_canonical().then_some(p)
353 }
354
355 pub fn reference_bytes(&self) -> Option<Vec<u8>> {
357 let r = self.reference.as_bytes();
358 if r.is_empty() || !r.len().is_multiple_of(2) || r.len() > MAX_PRINCIPAL_BYTES * 2 {
359 return None;
360 }
361 let nibble = |c: u8| match c {
362 b'0'..=b'9' => Some(c - b'0'),
363 b'a'..=b'f' => Some(c - b'a' + 10),
364 _ => None,
365 };
366 r.chunks(2)
367 .map(|p| Some(nibble(p[0])? << 4 | nibble(p[1])?))
368 .collect()
369 }
370
371 pub fn is_canonical(&self) -> bool {
372 self.reference_bytes().is_some() && is_label(&self.mapping_version) && is_label(&self.scope)
373 }
374
375 pub fn evidence(&self) -> PrincipalEvidence {
378 let mut b = Vec::new();
379 for part in [&self.mapping_version, &self.scope, &self.reference] {
380 b.extend_from_slice(&(part.len() as u64).to_be_bytes());
381 b.extend_from_slice(part.as_bytes());
382 }
383 PrincipalEvidence {
384 digest: crate::canonical::tagged_digest(PRINCIPAL_DIGEST_TAG, &b),
385 mapping_version: self.mapping_version.clone(),
386 }
387 }
388}
389
390#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
393#[serde(deny_unknown_fields)]
394pub struct PrincipalEvidence {
395 pub digest: String,
396 pub mapping_version: String,
397}
398
399#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
400#[serde(deny_unknown_fields)]
401pub struct InferItem {
402 pub attempt_id: String,
404 pub projection: String,
406}
407
408#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
409#[serde(deny_unknown_fields)]
410pub struct InferResponse {
411 pub schema: String,
412 pub version: u32,
413 pub artifact: ArtifactId,
415 pub served: ServedIdentity,
416 pub route: Vec<String>,
418 pub generation_id: Option<String>,
419 pub items: Vec<ItemAnswer>,
421 pub charge: Charge,
423 pub extras: BTreeMap<String, String>,
425}
426
427crate::document::document!(InferResponse, schema::REMOTE_INFER, REMOTE_V1);
428
429#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
430#[serde(deny_unknown_fields)]
431pub struct ItemAnswer {
432 pub attempt_id: String,
433 pub result: ItemResult,
434}
435
436#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
437#[serde(rename_all = "snake_case", deny_unknown_fields)]
438pub enum ItemResult {
439 Output(RawOutput),
441 Failure {
442 code: RemoteCode,
443 detail: String,
444 },
445 InProgress,
447}
448
449#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
453#[serde(deny_unknown_fields)]
454pub struct CancelRequest {
455 pub schema: String,
456 pub version: u32,
457 pub backend_id: String,
458 pub attempt_ids: Vec<String>,
459}
460
461crate::document::document!(CancelRequest, schema::REMOTE_CANCEL, REMOTE_V1);
462
463#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
464#[serde(deny_unknown_fields)]
465pub struct CancelResponse {
466 pub schema: String,
467 pub version: u32,
468 pub answers: Vec<CancelItem>,
469}
470
471crate::document::document!(CancelResponse, schema::REMOTE_CANCEL, REMOTE_V1);
472
473#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
474#[serde(deny_unknown_fields)]
475pub struct CancelItem {
476 pub attempt_id: String,
477 pub answer: RemoteCancelAnswer,
478}
479
480#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
481#[serde(rename_all = "snake_case", deny_unknown_fields)]
482pub enum RemoteCancelAnswer {
483 Stopped {
485 charge: Charge,
486 },
487 Unconfirmed,
488 UnknownAttempt,
489}
490
491#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
500#[serde(deny_unknown_fields)]
501pub struct ExchangeRecord {
502 pub schema: String,
503 pub protocol: String,
505 pub binding: ArtifactId,
507 pub members: Vec<ExchangeMember>,
509 pub request_digest: Option<String>,
511 pub response_digest: Option<String>,
513 pub bytes_sent: bool,
515 pub status: Option<u16>,
516 pub retry_after: Option<String>,
518 pub requested_model: Option<String>,
519 pub served: ServedIdentity,
520 pub route: Vec<String>,
521 pub generation_id: Option<String>,
522 pub usage: BTreeMap<String, u64>,
524 pub reported_cost: Option<ReportedCost>,
526 pub exchange_charge: Charge,
528 pub attribution: Attribution,
529 pub privacy_requested: BTreeMap<String, String>,
530 pub provider_extras: BTreeMap<String, String>,
532 pub late: bool,
535 pub truncated: bool,
537 #[serde(default, skip_serializing_if = "Option::is_none")]
540 pub principal: Option<PrincipalEvidence>,
541 #[serde(default, skip_serializing_if = "Option::is_none")]
545 pub phase_expiry: Option<PhaseExpiry>,
546 #[serde(default, skip_serializing_if = "Option::is_none")]
549 pub connection: Option<ConnectionObservation>,
550}
551
552crate::document::document!(ExchangeRecord, schema::REMOTE_EXCHANGE, EXCHANGE_V1);
553
554#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
556#[serde(rename_all = "snake_case")]
557pub enum ConnectionReuse {
558 Fresh,
559 Reused,
560}
561
562#[derive(
565 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
566)]
567#[serde(rename_all = "snake_case")]
568pub enum AgeBucket {
569 Under1s,
570 Under10s,
571 Under60s,
572 Under600s,
573 Over600s,
574}
575
576impl AgeBucket {
577 pub fn of_ms(ms: u64) -> Self {
578 match ms {
579 0..1_000 => AgeBucket::Under1s,
580 1_000..10_000 => AgeBucket::Under10s,
581 10_000..60_000 => AgeBucket::Under60s,
582 60_000..600_000 => AgeBucket::Under600s,
583 _ => AgeBucket::Over600s,
584 }
585 }
586}
587
588#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
591#[serde(rename_all = "snake_case")]
592pub enum Multiplex {
593 None,
594}
595
596#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema)]
598#[serde(rename_all = "snake_case")]
599pub enum EvictionReason {
600 IdleExpired,
602 LifetimeExpired,
604 MaxUses,
606 IdleCapacity,
608 Capacity,
610 Closed,
612 Failed,
614}
615
616#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
621#[serde(deny_unknown_fields)]
622pub struct ConnectionObservation {
623 pub reuse: ConnectionReuse,
624 pub instance_id: String,
625 pub age_bucket: AgeBucket,
626 pub use_count: u64,
627 pub multiplex: Multiplex,
628 pub key_digest: String,
629 pub eviction: Option<EvictionReason>,
630}
631
632#[derive(
635 Debug,
636 Clone,
637 Copy,
638 PartialEq,
639 Eq,
640 PartialOrd,
641 Ord,
642 Hash,
643 Serialize,
644 Deserialize,
645 schemars::JsonSchema,
646)]
647#[serde(rename_all = "snake_case")]
648pub enum TransportPhase {
649 Resolution,
650 Connect,
651 SecureHandshake,
652 RequestWrite,
653 FirstResponseByte,
654 ResponseBody,
655 CancellationAck,
656}
657
658impl TransportPhase {
659 pub const ALL: [TransportPhase; 7] = [
661 TransportPhase::Resolution,
662 TransportPhase::Connect,
663 TransportPhase::SecureHandshake,
664 TransportPhase::RequestWrite,
665 TransportPhase::FirstResponseByte,
666 TransportPhase::ResponseBody,
667 TransportPhase::CancellationAck,
668 ];
669}
670
671#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
674#[serde(rename_all = "snake_case")]
675pub enum PhaseLimit {
676 Ceiling,
677 Budget,
678}
679
680#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
685#[serde(deny_unknown_fields)]
686pub struct PhaseExpiry {
687 pub phase: TransportPhase,
688 pub ceiling_ms: u64,
689 pub allowance_ms: u64,
690 pub elapsed_ms: u64,
691 pub limit: PhaseLimit,
692 pub bytes_sent: bool,
693 pub code: RemoteCode,
694}
695
696#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
697#[serde(deny_unknown_fields)]
698pub struct ExchangeMember {
699 pub attempt_id: String,
700 pub outcome: MemberOutcome,
701 pub charge: Charge,
704}
705
706#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
707#[serde(rename_all = "snake_case", deny_unknown_fields)]
708pub enum MemberOutcome {
709 Output {
711 digest: String,
712 },
713 Failure {
714 code: RemoteCode,
715 detail: String,
716 },
717}
718
719#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
720#[serde(deny_unknown_fields)]
721pub struct ReportedCost {
722 pub amount: String,
724 pub currency: String,
725}
726
727#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
729#[serde(rename_all = "snake_case", deny_unknown_fields)]
730pub enum Attribution {
731 Single,
733 SharedState { coalescing_window_ms: u64 },
736 RuntimePlanned,
740}
741
742fn cut_flag(s: &mut String, truncated: &mut bool) {
743 if s.len() > MAX_TEXT_BYTES {
744 *s = cut_text(s);
745 *truncated = true;
746 }
747}
748
749fn cut_opt(s: &mut Option<String>, truncated: &mut bool) {
750 if let Some(v) = s {
751 cut_flag(v, truncated);
752 }
753}
754
755fn cut_map<V>(m: BTreeMap<String, V>, truncated: &mut bool) -> BTreeMap<String, V>
756where
757 V: CutValue,
758{
759 if m.len() > MAX_ENTRIES {
760 *truncated = true;
761 }
762 let mut out = BTreeMap::new();
763 for (k, mut v) in m.into_iter().take(MAX_ENTRIES) {
764 let mut k = k;
765 cut_flag(&mut k, truncated);
766 v.cut(truncated);
767 out.entry(k).or_insert(v);
768 }
769 out
770}
771
772trait CutValue {
773 fn cut(&mut self, truncated: &mut bool);
774}
775
776impl CutValue for String {
777 fn cut(&mut self, truncated: &mut bool) {
778 cut_flag(self, truncated);
779 }
780}
781
782impl CutValue for u64 {
783 fn cut(&mut self, _: &mut bool) {}
784}
785
786impl ExchangeRecord {
787 pub fn bounded(mut self) -> Self {
792 let mut t = self.truncated;
793 self.schema = schema::REMOTE_EXCHANGE.to_string();
794 cut_flag(&mut self.protocol, &mut t);
795 if self.members.len() > MAX_ITEMS {
796 self.members.truncate(MAX_ITEMS);
797 t = true;
798 }
799 for m in &mut self.members {
800 cut_flag(&mut m.attempt_id, &mut t);
801 if let MemberOutcome::Failure { detail, .. } = &mut m.outcome {
802 cut_flag(detail, &mut t);
803 }
804 }
805 cut_opt(&mut self.request_digest, &mut t);
806 cut_opt(&mut self.response_digest, &mut t);
807 cut_opt(&mut self.retry_after, &mut t);
808 cut_opt(&mut self.requested_model, &mut t);
809 cut_opt(&mut self.served.identity, &mut t);
810 if self.route.len() > MAX_ROUTE {
811 self.route.truncate(MAX_ROUTE);
812 t = true;
813 }
814 for r in &mut self.route {
815 cut_flag(r, &mut t);
816 }
817 cut_opt(&mut self.generation_id, &mut t);
818 self.usage = cut_map(std::mem::take(&mut self.usage), &mut t);
819 if let Some(c) = &mut self.reported_cost {
820 cut_flag(&mut c.amount, &mut t);
821 cut_flag(&mut c.currency, &mut t);
822 }
823 self.privacy_requested = cut_map(std::mem::take(&mut self.privacy_requested), &mut t);
824 self.provider_extras = cut_map(std::mem::take(&mut self.provider_extras), &mut t);
825 if let Some(c) = &mut self.connection {
826 cut_flag(&mut c.instance_id, &mut t);
827 cut_flag(&mut c.key_digest, &mut t);
828 }
829 if let Some(p) = &mut self.principal {
830 cut_flag(&mut p.digest, &mut t);
831 if p.mapping_version.len() > MAX_PRINCIPAL_LABEL_BYTES {
832 p.mapping_version = cut_to(&p.mapping_version, MAX_PRINCIPAL_LABEL_BYTES);
833 t = true;
834 }
835 }
836 self.truncated = t;
837 self
838 }
839
840 pub fn is_bounded(&self) -> bool {
844 let t = |s: &str| s.len() <= MAX_TEXT_BYTES;
845 let o = |s: &Option<String>| s.as_deref().is_none_or(t);
846 t(&self.protocol)
847 && self.members.len() <= MAX_ITEMS
848 && self.members.iter().all(|m| {
849 t(&m.attempt_id)
850 && match &m.outcome {
851 MemberOutcome::Failure { detail, .. } => t(detail),
852 MemberOutcome::Output { digest } => t(digest),
853 }
854 })
855 && o(&self.request_digest)
856 && o(&self.response_digest)
857 && o(&self.retry_after)
858 && o(&self.requested_model)
859 && o(&self.served.identity)
860 && self.route.len() <= MAX_ROUTE
861 && self.route.iter().all(|r| t(r))
862 && o(&self.generation_id)
863 && self.usage.len() <= MAX_ENTRIES
864 && self.usage.keys().all(|k| t(k))
865 && self
866 .reported_cost
867 .as_ref()
868 .is_none_or(|c| t(&c.amount) && t(&c.currency))
869 && [&self.privacy_requested, &self.provider_extras]
870 .iter()
871 .all(|m| m.len() <= MAX_ENTRIES && m.iter().all(|(k, v)| t(k) && t(v)))
872 && self.principal.as_ref().is_none_or(|p| {
873 t(&p.digest) && p.mapping_version.len() <= MAX_PRINCIPAL_LABEL_BYTES
874 })
875 && self
876 .connection
877 .as_ref()
878 .is_none_or(|c| t(&c.instance_id) && t(&c.key_digest))
879 }
880
881 pub fn parse_checked(bytes: &[u8]) -> Result<Self, crate::DocumentError> {
883 use crate::Document;
884 let r = Self::parse(bytes)?;
885 if !r.is_bounded() {
886 return Err(crate::DocumentError::Parse(
887 crate::bounded::ParseError::Schema {
888 message: "exchange record beyond its fixed bounds".into(),
889 },
890 ));
891 }
892 Ok(r)
893 }
894}
895
896#[cfg(test)]
897mod tests {
898 use super::*;
899 use crate::Document;
900 use crate::DocumentError;
901 use crate::bounded::{BoundError, ParseError};
902 use crate::definition::{Determinism, Operation};
903 use crate::descriptor::{InputExcess, InputLimit, OperationSupport, OutputKind};
904 use crate::{Identified, limits};
905
906 const A: &str = "sha256:00000000000000000000000000000000000000000000000000000000000000aa";
907
908 fn descriptor() -> BackendDescriptor {
909 BackendDescriptor {
910 ranking: None,
911 schema: schema::BACKEND.into(),
912 backend_id: "synthetic-remote".into(),
913 artifact: ArtifactId::parse(A).unwrap(),
914 operations: vec![OperationSupport {
915 isolation: None,
916 screen: None,
917 operation: Operation::Classify,
918 output: OutputKind::Distribution,
919 max_options: 8,
920 }],
921 input_limit: InputLimit {
922 max_bytes: 4096,
923 on_excess: InputExcess::Refuse,
924 },
925 determinism: Determinism::Unspecified,
926 }
927 }
928
929 fn terms() -> RemoteTerms {
930 RemoteTerms {
931 cost: CostBound::Bounded { max_units: 3 },
932 refusals_charged: false,
933 cancellation: CancelSupport::Confirmed,
934 served: ServedTerms {
935 disclosure: Disclosure::Pinned,
936 identity: Some("synthetic-model@1".into()),
937 },
938 batching: Batching::SharedState { max_items: 4 },
939 max_request_bytes: 65_536,
940 max_response_bytes: 65_536,
941 idempotency_window_ms: 60_000,
942 privacy: vec!["no_retention".into()],
943 }
944 }
945
946 fn describe_response() -> DescribeResponse {
947 let d = descriptor();
948 DescribeResponse {
949 schema: schema::REMOTE_DESCRIBE.into(),
950 version: 1,
951 backends: vec![HostedBackend {
952 descriptor_id: d.id().unwrap(),
953 descriptor: d,
954 terms: terms(),
955 }],
956 }
957 }
958
959 fn infer_request() -> InferRequest {
960 InferRequest {
961 schema: schema::REMOTE_INFER.into(),
962 version: 1,
963 backend_id: "synthetic-remote".into(),
964 descriptor: descriptor().id().unwrap(),
965 artifact: ArtifactId::parse(A).unwrap(),
966 budget_ms: 900,
967 trace_id: "d-1".into(),
968 privacy: BTreeMap::from([("no_retention".into(), "true".into())]),
969 items: vec![InferItem {
970 attempt_id: "d-1/topic//1".into(),
971 projection: r#"{"operation":"classify"}"#.into(),
972 }],
973 principal: None,
974 }
975 }
976
977 fn infer_response() -> InferResponse {
978 InferResponse {
979 schema: schema::REMOTE_INFER.into(),
980 version: 1,
981 artifact: ArtifactId::parse(A).unwrap(),
982 served: ServedIdentity {
983 identity: Some("synthetic-model@1".into()),
984 disclosure: Disclosure::Pinned,
985 },
986 route: vec!["synthetic-route".into()],
987 generation_id: Some("gen-1".into()),
988 items: vec![
989 ItemAnswer {
990 attempt_id: "d-1/topic//1".into(),
991 result: ItemResult::Output(RawOutput::Distribution(BTreeMap::from([
992 ("a".into(), 0.1),
993 ("b".into(), 0.9),
994 ]))),
995 },
996 ItemAnswer {
997 attempt_id: "d-1/topic//2".into(),
998 result: ItemResult::Failure {
999 code: RemoteCode::RemoteDeadline,
1000 detail: "SYNTHETIC".into(),
1001 },
1002 },
1003 ItemAnswer {
1004 attempt_id: "d-1/topic//3".into(),
1005 result: ItemResult::InProgress,
1006 },
1007 ],
1008 charge: Charge::Observed { units: 3 },
1009 extras: BTreeMap::from([("argmax".into(), "b".into())]),
1010 }
1011 }
1012
1013 fn cancel_docs() -> (CancelRequest, CancelResponse) {
1014 (
1015 CancelRequest {
1016 schema: schema::REMOTE_CANCEL.into(),
1017 version: 1,
1018 backend_id: "synthetic-remote".into(),
1019 attempt_ids: vec!["x/1".into(), "x/2".into(), "x/3".into()],
1020 },
1021 CancelResponse {
1022 schema: schema::REMOTE_CANCEL.into(),
1023 version: 1,
1024 answers: vec![
1025 CancelItem {
1026 attempt_id: "x/1".into(),
1027 answer: RemoteCancelAnswer::Stopped {
1028 charge: Charge::Observed { units: 1 },
1029 },
1030 },
1031 CancelItem {
1032 attempt_id: "x/2".into(),
1033 answer: RemoteCancelAnswer::Unconfirmed,
1034 },
1035 CancelItem {
1036 attempt_id: "x/3".into(),
1037 answer: RemoteCancelAnswer::UnknownAttempt,
1038 },
1039 ],
1040 },
1041 )
1042 }
1043
1044 fn exchange() -> ExchangeRecord {
1045 ExchangeRecord {
1046 schema: schema::REMOTE_EXCHANGE.into(),
1047 protocol: PROTOCOL.into(),
1048 binding: ArtifactId::parse(A).unwrap(),
1049 members: vec![ExchangeMember {
1050 attempt_id: "d-1/topic//1".into(),
1051 outcome: MemberOutcome::Failure {
1052 code: RemoteCode::RateLimited,
1053 detail: "remote:rate_limited SYNTHETIC".into(),
1054 },
1055 charge: Charge::Observed { units: 0 },
1056 }],
1057 request_digest: Some(A.into()),
1058 response_digest: None,
1059 bytes_sent: true,
1060 status: Some(429),
1061 retry_after: Some("30".into()),
1062 requested_model: None,
1063 served: ServedIdentity::unknown(),
1064 route: vec![],
1065 generation_id: None,
1066 usage: BTreeMap::new(),
1067 reported_cost: Some(ReportedCost {
1068 amount: "0.000125".into(),
1069 currency: "SYNTHETIC".into(),
1070 }),
1071 exchange_charge: Charge::Observed { units: 0 },
1072 attribution: Attribution::Single,
1073 privacy_requested: BTreeMap::new(),
1074 provider_extras: BTreeMap::new(),
1075 late: false,
1076 truncated: false,
1077 phase_expiry: None,
1078 principal: None,
1079 connection: None,
1080 }
1081 }
1082
1083 fn round_trip<T: Document + PartialEq + std::fmt::Debug>(doc: &T) {
1084 let bytes = doc.record_canonical().unwrap();
1085 let back = T::parse(&bytes).unwrap();
1086 assert_eq!(&back, doc);
1087 assert_eq!(back.record_canonical().unwrap(), bytes);
1088 }
1089
1090 #[test]
1091 fn a_principal_reference_is_a_version_2_member_and_evidence_holds_only_its_digest() {
1092 let v1 = infer_request();
1093 assert!(v1.members_fit_version());
1094 let bytes = String::from_utf8(v1.canonical().unwrap()).unwrap();
1095 assert!(!bytes.contains("principal"), "version 1 bytes unchanged");
1096 let p = WirePrincipal::encode(b"SYNTHETIC", "map/1", "routing").unwrap();
1097 assert_eq!(p.reference, "53594e544845544943");
1098 assert_eq!(p.reference_bytes().unwrap(), b"SYNTHETIC");
1099 let mut with = infer_request();
1100 with.principal = Some(p.clone());
1101 assert!(
1102 !with.members_fit_version(),
1103 "a version 1 request carries none"
1104 );
1105 with.version = PROTOCOL_MAJOR_V2;
1106 assert!(with.members_fit_version());
1107 round_trip(&with);
1108 assert!(WirePrincipal::encode(b"", "m", "s").is_none());
1110 assert!(WirePrincipal::encode(&[1; MAX_PRINCIPAL_BYTES + 1], "m", "s").is_none());
1111 assert!(WirePrincipal::encode(&[1; MAX_PRINCIPAL_BYTES], "m", "s").is_some());
1112 for bad in ["ABCD", "abc", "zz", ""] {
1113 let q = WirePrincipal {
1114 reference: bad.into(),
1115 ..p.clone()
1116 };
1117 assert!(!q.is_canonical(), "{bad}");
1118 }
1119 let e = p.evidence();
1122 assert!(!e.digest.contains(&p.reference));
1123 for other in [
1124 WirePrincipal::encode(b"SYNTHETIC2", "map/1", "routing").unwrap(),
1125 WirePrincipal::encode(b"SYNTHETIC", "map/2", "routing").unwrap(),
1126 WirePrincipal::encode(b"SYNTHETIC", "map/1", "other").unwrap(),
1127 ] {
1128 assert_ne!(other.evidence().digest, e.digest);
1129 }
1130 assert!(!format!("{p:?}").contains(&p.reference));
1131 let mut rec = exchange();
1132 rec.principal = Some(e);
1133 round_trip(&rec);
1134 assert!(rec.is_bounded());
1135 }
1136
1137 #[test]
1138 fn a_connection_observation_is_omitted_without_a_pool_and_round_trips_with_one() {
1139 let plain = String::from_utf8(exchange().canonical().unwrap()).unwrap();
1140 assert!(!plain.contains("connection"));
1141 let mut rec = exchange();
1142 rec.connection = Some(ConnectionObservation {
1143 reuse: ConnectionReuse::Reused,
1144 instance_id: "conn-7".into(),
1145 age_bucket: AgeBucket::of_ms(1_500),
1146 use_count: 2,
1147 multiplex: Multiplex::None,
1148 key_digest: A.into(),
1149 eviction: Some(EvictionReason::MaxUses),
1150 });
1151 round_trip(&rec);
1152 assert_eq!(
1153 rec.connection.as_ref().unwrap().age_bucket,
1154 AgeBucket::Under10s
1155 );
1156 for (ms, b) in [
1157 (0, AgeBucket::Under1s),
1158 (999, AgeBucket::Under1s),
1159 (1_000, AgeBucket::Under10s),
1160 (59_999, AgeBucket::Under60s),
1161 (60_000, AgeBucket::Under600s),
1162 (600_000, AgeBucket::Over600s),
1163 ] {
1164 assert_eq!(AgeBucket::of_ms(ms), b);
1165 }
1166 }
1167
1168 #[test]
1169 fn a_phase_expiry_is_omitted_when_absent_and_round_trips_when_present() {
1170 let plain = exchange();
1172 let bytes = String::from_utf8(plain.canonical().unwrap()).unwrap();
1173 assert!(!bytes.contains("phase_expiry"), "{bytes}");
1174 let mut timed = exchange();
1175 timed.phase_expiry = Some(PhaseExpiry {
1176 phase: TransportPhase::FirstResponseByte,
1177 ceiling_ms: 50,
1178 allowance_ms: 40,
1179 elapsed_ms: 40,
1180 limit: PhaseLimit::Budget,
1181 bytes_sent: true,
1182 code: RemoteCode::TransportInterrupted,
1183 });
1184 round_trip(&timed);
1185 let text = String::from_utf8(timed.canonical().unwrap()).unwrap();
1186 assert!(text.contains(r#""phase_expiry":{"allowance_ms":40,"bytes_sent":true,"ceiling_ms":50,"code":"transport_interrupted","elapsed_ms":40,"limit":"budget","phase":"first_response_byte"}"#), "{text}");
1187 assert!(
1188 ExchangeRecord::parse(
1189 text.replace(r#""limit":"budget""#, r#""limit":"budget","extra":1"#)
1190 .as_bytes()
1191 )
1192 .is_err()
1193 );
1194 }
1195
1196 #[test]
1197 fn remote_documents_round_trip_through_canonical_bytes() {
1198 round_trip(&DescribeRequest {
1199 schema: schema::REMOTE_DESCRIBE.into(),
1200 versions: vec![1],
1201 });
1202 round_trip(&describe_response());
1203 round_trip(&infer_request());
1204 round_trip(&infer_response());
1205 let (req, resp) = cancel_docs();
1206 round_trip(&req);
1207 round_trip(&resp);
1208 round_trip(&exchange());
1209 let r = infer_request();
1211 assert_eq!(r.canonical().unwrap(), r.record_canonical().unwrap());
1212 }
1213
1214 #[test]
1215 fn remote_codes_are_closed_and_named_as_in_the_table() {
1216 for c in RemoteCode::ALL {
1217 let s = serde_json::to_string(&c).unwrap();
1218 assert_eq!(s, format!("\"{}\"", c.as_str()));
1219 }
1220 assert!(serde_json::from_str::<RemoteCode>("\"timeout\"").is_err());
1221 }
1222
1223 fn with_extra_field(doc: &impl Document, at: Option<&str>) -> Vec<u8> {
1224 let mut v: serde_json::Value = serde_json::to_value(doc).unwrap();
1225 let target = match at {
1226 None => &mut v,
1227 Some(p) => v.pointer_mut(p).unwrap(),
1228 };
1229 target
1230 .as_object_mut()
1231 .unwrap()
1232 .insert("zz_unknown".into(), serde_json::json!(1));
1233 serde_json::to_vec(&v).unwrap()
1234 }
1235
1236 fn schema_error<T: Document + std::fmt::Debug>(bytes: &[u8]) {
1237 match T::parse(bytes) {
1238 Err(DocumentError::Parse(ParseError::Schema { .. })) => {}
1239 other => panic!("{other:?}"),
1240 }
1241 }
1242
1243 #[test]
1244 fn remote_documents_refuse_unknown_fields() {
1245 schema_error::<DescribeResponse>(&with_extra_field(&describe_response(), None));
1246 schema_error::<DescribeResponse>(&with_extra_field(
1247 &describe_response(),
1248 Some("/backends/0/terms"),
1249 ));
1250 schema_error::<InferRequest>(&with_extra_field(&infer_request(), Some("/items/0")));
1251 schema_error::<InferResponse>(&with_extra_field(&infer_response(), Some("/served")));
1252 schema_error::<InferResponse>(&with_extra_field(
1253 &infer_response(),
1254 Some("/items/1/result/failure"),
1255 ));
1256 let (req, resp) = cancel_docs();
1257 schema_error::<CancelRequest>(&with_extra_field(&req, None));
1258 schema_error::<CancelResponse>(&with_extra_field(&resp, Some("/answers/0")));
1259 schema_error::<ExchangeRecord>(&with_extra_field(&exchange(), Some("/members/0")));
1260 }
1261
1262 #[test]
1263 fn remote_documents_refuse_a_wrong_schema_string() {
1264 let mut r = infer_request();
1265 r.schema = "rustev.remote-infer/2".into();
1266 let bytes = r.canonical().unwrap();
1267 assert!(matches!(
1268 InferRequest::parse(&bytes),
1269 Err(DocumentError::Schema { .. })
1270 ));
1271 let (c, _) = cancel_docs();
1273 assert!(InferRequest::parse(&c.canonical().unwrap()).is_err());
1274 let mut d = describe_response();
1275 d.schema = schema::REMOTE_INFER.into();
1276 assert!(matches!(
1277 DescribeResponse::parse(&d.canonical().unwrap()),
1278 Err(DocumentError::Schema { .. })
1279 ));
1280 }
1281
1282 #[test]
1283 fn remote_documents_enforce_their_parse_limits() {
1284 let big = vec![b' '; limits::REMOTE_V1.max_bytes + 1];
1286 assert!(matches!(
1287 InferRequest::parse(&big),
1288 Err(DocumentError::Parse(ParseError::Bound(
1289 BoundError::TooLarge { .. }
1290 )))
1291 ));
1292 let deep = format!(
1294 "{}{}",
1295 "[".repeat(limits::REMOTE_V1.max_depth + 1),
1296 "]".repeat(limits::REMOTE_V1.max_depth + 1)
1297 );
1298 assert!(matches!(
1299 InferResponse::parse(deep.as_bytes()),
1300 Err(DocumentError::Parse(ParseError::Bound(
1301 BoundError::TooDeep { .. }
1302 )))
1303 ));
1304 let mut r = infer_request();
1306 r.items = (0..limits::REMOTE_V1.max_collection_len + 1)
1307 .map(|i| InferItem {
1308 attempt_id: format!("a/{i}"),
1309 projection: "{}".into(),
1310 })
1311 .collect();
1312 assert!(matches!(
1313 InferRequest::parse(&r.canonical().unwrap()),
1314 Err(DocumentError::Parse(ParseError::Bound(
1315 BoundError::CollectionTooLong { .. }
1316 )))
1317 ));
1318 let mut e = exchange();
1320 e.protocol = "p".repeat(limits::EXCHANGE_V1.max_string_bytes + 1);
1321 assert!(matches!(
1322 ExchangeRecord::parse(&e.canonical().unwrap()),
1323 Err(DocumentError::Parse(ParseError::Bound(
1324 BoundError::StringTooLong { .. }
1325 )))
1326 ));
1327 }
1328
1329 #[test]
1330 fn remote_exchange_records_are_cut_to_a_fixed_maximum_size() {
1331 let nasty = "\u{1}".repeat(4 * MAX_TEXT_BYTES);
1334 let s = || nasty.clone();
1335 let map = |n: usize| -> BTreeMap<String, String> {
1336 (0..n).map(|i| (format!("{i:03}{nasty}"), s())).collect()
1337 };
1338 let mut e = exchange();
1339 e.protocol = s();
1340 e.members = (0..MAX_ITEMS + 5)
1341 .map(|i| ExchangeMember {
1342 attempt_id: format!("{i:03}{nasty}"),
1343 outcome: MemberOutcome::Failure {
1344 code: RemoteCode::TransportInterrupted,
1345 detail: s(),
1346 },
1347 charge: Charge::Observed { units: u64::MAX },
1348 })
1349 .collect();
1350 e.request_digest = Some(s());
1351 e.response_digest = Some(s());
1352 e.retry_after = Some(s());
1353 e.requested_model = Some(s());
1354 e.served.identity = Some(s());
1355 e.route = vec![s(); MAX_ROUTE + 3];
1356 e.generation_id = Some(s());
1357 e.usage = (0..MAX_ENTRIES + 3)
1358 .map(|i| (format!("{i:03}{nasty}"), u64::MAX))
1359 .collect();
1360 e.reported_cost = Some(ReportedCost {
1361 amount: s(),
1362 currency: s(),
1363 });
1364 e.privacy_requested = map(MAX_ENTRIES + 3);
1365 e.provider_extras = map(MAX_ENTRIES + 3);
1366 assert!(!e.is_bounded());
1367 let b = e.bounded();
1368 assert!(b.truncated && b.is_bounded());
1369 assert_eq!(b.members.len(), MAX_ITEMS);
1370 assert!(
1371 b.members
1372 .iter()
1373 .all(|m| m.attempt_id.len() <= MAX_TEXT_BYTES)
1374 );
1375 let bytes = b.canonical().unwrap();
1376 assert!(
1377 bytes.len() <= limits::EXCHANGE_V1.max_bytes,
1378 "{} bytes",
1379 bytes.len()
1380 );
1381 assert_eq!(ExchangeRecord::parse_checked(&bytes).unwrap(), b);
1382 let mut over = exchange();
1384 over.generation_id = Some("g".repeat(MAX_TEXT_BYTES + 1));
1385 assert!(ExchangeRecord::parse_checked(&over.canonical().unwrap()).is_err());
1386 }
1387
1388 #[test]
1389 fn remote_text_is_cut_on_a_character_boundary() {
1390 let s = "é".repeat(200);
1391 let c = cut_text(&s);
1392 assert!(c.len() <= MAX_TEXT_BYTES && c.len() >= MAX_TEXT_BYTES - 1);
1393 assert!(s.starts_with(&c));
1394 assert_eq!(cut_text("short"), "short");
1395 }
1396}