rate_limits/
reset_time.rs1use crate::convert;
2use crate::error::{Error, Result};
3use time::{
4 OffsetDateTime,
5 format_description::well_known::{Rfc2822, Rfc3339},
6};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub(crate) enum ResetTimeKind {
10 Seconds,
11 Timestamp,
12 TimestampMillis,
13 ImfFixdate,
14 Iso8601,
15 OpenAiDuration,
16}
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum ResetTime {
24 Seconds(usize),
26 DateTime(OffsetDateTime),
28}
29
30impl ResetTime {
31 pub(crate) fn new(value: &str, kind: ResetTimeKind) -> Result<Self> {
38 match kind {
39 ResetTimeKind::Seconds => {
40 let s = convert::to_usize(value)?;
41 Ok(ResetTime::Seconds(s))
42 }
43 ResetTimeKind::Timestamp => {
44 let s = value.parse::<i64>().map_err(|_| Error::NoMatchingVariant)?;
45 let dt =
46 OffsetDateTime::from_unix_timestamp(s).map_err(|_| Error::NoMatchingVariant)?;
47 Ok(ResetTime::DateTime(dt))
48 }
49 ResetTimeKind::TimestampMillis => {
50 let ms = value
51 .parse::<i128>()
52 .map_err(|_| Error::NoMatchingVariant)?;
53 let dt = OffsetDateTime::from_unix_timestamp_nanos(ms * 1_000_000)
54 .map_err(|_| Error::NoMatchingVariant)?;
55 Ok(ResetTime::DateTime(dt))
56 }
57 ResetTimeKind::ImfFixdate => {
58 let dt =
59 OffsetDateTime::parse(value, &Rfc2822).map_err(|_| Error::NoMatchingVariant)?;
60 Ok(ResetTime::DateTime(dt))
61 }
62 ResetTimeKind::Iso8601 => {
63 let dt =
64 OffsetDateTime::parse(value, &Rfc3339).map_err(|_| Error::NoMatchingVariant)?;
65 Ok(ResetTime::DateTime(dt))
66 }
67 ResetTimeKind::OpenAiDuration => {
68 let seconds = parse_openai_duration(value).ok_or(Error::NoMatchingVariant)?;
69 Ok(ResetTime::Seconds(seconds))
70 }
71 }
72 }
73
74 #[must_use]
76 pub fn seconds(&self) -> usize {
77 match self {
78 ResetTime::Seconds(s) => *s,
79 #[allow(clippy::cast_possible_truncation)]
83 ResetTime::DateTime(d) => {
84 let diff = *d - OffsetDateTime::now_utc();
85 let seconds = diff.whole_seconds();
86 if seconds < 0 { 0 } else { seconds as usize }
87 }
88 }
89 }
90
91 #[must_use]
93 pub fn duration(&self) -> std::time::Duration {
94 match self {
95 ResetTime::Seconds(s) => std::time::Duration::from_secs(*s as u64),
96 ResetTime::DateTime(d) => {
97 let diff = *d - OffsetDateTime::now_utc();
98 std::time::Duration::try_from(diff).unwrap_or(std::time::Duration::ZERO)
99 }
100 }
101 }
102}
103
104impl TryFrom<&str> for ResetTime {
105 type Error = Error;
106
107 fn try_from(value: &str) -> Result<Self> {
120 if let Ok(n) = convert::to_usize(value) {
121 let kind = if n > 1_000_000_000 {
124 ResetTimeKind::Timestamp
125 } else {
126 ResetTimeKind::Seconds
127 };
128 return Self::new(value, kind);
129 }
130 if let Ok(r) = Self::new(value, ResetTimeKind::ImfFixdate) {
131 return Ok(r);
132 }
133 if let Ok(r) = Self::new(value, ResetTimeKind::Iso8601) {
134 return Ok(r);
135 }
136 Err(Error::NoMatchingVariant)
137 }
138}
139
140fn parse_openai_duration(s: &str) -> Option<usize> {
161 if s.is_empty() {
162 return None;
163 }
164
165 let mut total_ms = 0.0_f64;
166 let mut num_start: Option<usize> = None;
167 let bytes = s.as_bytes();
168 let mut i = 0;
169
170 while i < bytes.len() {
171 let c = bytes[i];
172 if c.is_ascii_digit() || c == b'.' {
173 if num_start.is_none() {
174 num_start = Some(i);
175 }
176 i += 1;
177 continue;
178 }
179
180 let start = num_start.take()?;
182 let val: f64 = s[start..i].parse().ok()?;
183
184 let (multiplier_ms, consumed) = match c {
186 b's' => (1_000.0, 1),
187 b'm' if bytes.get(i + 1) == Some(&b's') => (1.0, 2),
188 b'm' => (60_000.0, 1),
189 b'h' => (3_600_000.0, 1),
190 b'd' => (86_400_000.0, 1),
191 _ => return None,
192 };
193
194 total_ms += val * multiplier_ms;
195 i += consumed;
196 }
197
198 if num_start.is_some() {
200 return None;
201 }
202
203 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
204 Some((total_ms / 1000.0).ceil() as usize)
205}
206
207#[cfg(test)]
208mod openai_duration_tests {
209 use super::parse_openai_duration;
210
211 #[test]
212 fn seconds() {
213 assert_eq!(parse_openai_duration("1s"), Some(1));
214 assert_eq!(parse_openai_duration("42s"), Some(42));
215 }
216
217 #[test]
218 fn milliseconds_round_up() {
219 assert_eq!(parse_openai_duration("500ms"), Some(1));
220 assert_eq!(parse_openai_duration("1000ms"), Some(1));
221 assert_eq!(parse_openai_duration("1001ms"), Some(2));
222 }
223
224 #[test]
225 fn minutes_vs_milliseconds() {
226 assert_eq!(parse_openai_duration("1m"), Some(60));
227 assert_eq!(parse_openai_duration("1ms"), Some(1));
228 }
229
230 #[test]
231 fn compound() {
232 assert_eq!(parse_openai_duration("1m30s"), Some(90));
233 assert_eq!(parse_openai_duration("1h2m3s"), Some(3723));
234 }
235
236 #[test]
237 fn fractional() {
238 assert_eq!(parse_openai_duration("1.5s"), Some(2));
239 assert_eq!(parse_openai_duration("0.5m"), Some(30));
240 }
241
242 #[test]
243 fn hours_and_days() {
244 assert_eq!(parse_openai_duration("1h"), Some(3600));
245 assert_eq!(parse_openai_duration("1d"), Some(86_400));
246 }
247
248 #[test]
249 fn invalid() {
250 assert_eq!(parse_openai_duration(""), None);
251 assert_eq!(parse_openai_duration("10"), None); assert_eq!(parse_openai_duration("s"), None); assert_eq!(parse_openai_duration("10x"), None); assert_eq!(parse_openai_duration("abc"), None);
255 }
256}