Skip to main content

ferrox_api/
request_id.rs

1//! Server-assigned request ids.
2//!
3//! The id is stated by the server in the response (and in the *first*
4//! streamed chunk, before any content), so a client never has to invent
5//! one or correlate by heuristic. The alternative -- matching a live
6//! request against a metrics snapshot by "the newest one that looks like
7//! mine" -- is what a UI is forced into when the server stays silent,
8//! and it mis-attributes as soon as two chats run at once.
9//!
10//! Format: `chatcmpl-` + a per-process random-ish stamp + a monotonic
11//! counter. The stamp makes ids from two runs of the same binary
12//! distinguishable (so a log spanning a restart does not collide); the
13//! counter makes them unique within a run without a lock or an RNG
14//! dependency. The prefix matches OpenAI's, which some clients display.
15
16use std::sync::atomic::{AtomicU64, Ordering};
17
18/// OpenAI uses this prefix for chat completion ids; clients occasionally
19/// display or match on it.
20pub const PREFIX: &str = "chatcmpl-";
21
22static COUNTER: AtomicU64 = AtomicU64::new(0);
23
24/// Per-process stamp: the low bits of the wall clock at first use. Not
25/// a security token -- it only needs to differ between restarts.
26fn process_stamp() -> u64 {
27    use std::sync::OnceLock;
28    static STAMP: OnceLock<u64> = OnceLock::new();
29    *STAMP.get_or_init(|| {
30        std::time::SystemTime::now()
31            .duration_since(std::time::UNIX_EPOCH)
32            .map(|d| d.as_nanos() as u64)
33            .unwrap_or(0)
34    })
35}
36
37/// A fresh request id. Unique within this process, and in practice
38/// across restarts.
39pub fn next_request_id() -> String {
40    let n = COUNTER.fetch_add(1, Ordering::Relaxed);
41    format!(
42        "{PREFIX}{:012x}{:06x}",
43        process_stamp() & 0xffff_ffff_ffff,
44        n
45    )
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    #[test]
53    fn ids_are_unique_and_prefixed() {
54        let a = next_request_id();
55        let b = next_request_id();
56        assert_ne!(a, b);
57        assert!(a.starts_with(PREFIX), "{a}");
58        assert!(b.starts_with(PREFIX), "{b}");
59    }
60
61    #[test]
62    fn ids_are_stable_width_for_the_first_million_requests() {
63        // A UI that column-aligns a request log should not have the
64        // column jump on request 16.
65        let first = next_request_id();
66        for _ in 0..64 {
67            assert_eq!(next_request_id().len(), first.len());
68        }
69    }
70
71    #[test]
72    fn ids_are_unique_across_threads() {
73        let handles: Vec<_> = (0..8)
74            .map(|_| std::thread::spawn(|| (0..64).map(|_| next_request_id()).collect::<Vec<_>>()))
75            .collect();
76        let mut seen = std::collections::BTreeSet::new();
77        for h in handles {
78            for id in h.join().unwrap() {
79                assert!(seen.insert(id.clone()), "duplicate id {id}");
80            }
81        }
82    }
83}