Skip to content

Commit 00d302a

Browse files
fix rate limit reset duration parsing
1 parent 7954d02 commit 00d302a

1 file changed

Lines changed: 106 additions & 5 deletions

File tree

src/cortex-ratelimits/src/limits.rs

Lines changed: 106 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Rate limit types.
22
3-
use chrono::{DateTime, Utc};
3+
use chrono::{DateTime, Duration, Utc};
44
use serde::{Deserialize, Serialize};
55

66
/// Rate limit information.
@@ -174,11 +174,112 @@ pub fn parse_rate_limit_headers(
174174
info.tokens_remaining = Some(remaining);
175175
}
176176

177-
if let Some(reset) = headers.get("x-ratelimit-reset-requests") {
178-
if let Ok(ts) = reset.parse::<i64>() {
179-
info.reset_at = DateTime::from_timestamp(ts, 0);
180-
}
177+
if let Some(reset) = headers
178+
.get("x-ratelimit-reset-requests")
179+
.or_else(|| headers.get("x-ratelimit-reset-tokens"))
180+
{
181+
info.reset_at = parse_reset_header(reset, Utc::now());
181182
}
182183

183184
info
184185
}
186+
187+
fn parse_reset_header(value: &str, now: DateTime<Utc>) -> Option<DateTime<Utc>> {
188+
let value = value.trim();
189+
if value.is_empty() {
190+
return None;
191+
}
192+
193+
if let Ok(timestamp) = value.parse::<i64>() {
194+
return if timestamp > 1_000_000_000 {
195+
DateTime::from_timestamp(timestamp, 0)
196+
} else {
197+
Some(now + Duration::seconds(timestamp))
198+
};
199+
}
200+
201+
if let Ok(timestamp) = value.parse::<DateTime<Utc>>() {
202+
return Some(timestamp);
203+
}
204+
205+
parse_duration_seconds(value).map(|seconds| now + Duration::seconds(seconds))
206+
}
207+
208+
fn parse_duration_seconds(value: &str) -> Option<i64> {
209+
let mut seconds = 0_i64;
210+
let mut digits = String::new();
211+
let mut saw_unit = false;
212+
213+
for ch in value.chars() {
214+
if ch.is_ascii_digit() {
215+
digits.push(ch);
216+
continue;
217+
}
218+
219+
let amount = digits.parse::<i64>().ok()?;
220+
digits.clear();
221+
saw_unit = true;
222+
match ch {
223+
'h' => seconds = seconds.checked_add(amount.checked_mul(60 * 60)?)?,
224+
'm' => seconds = seconds.checked_add(amount.checked_mul(60)?)?,
225+
's' => seconds = seconds.checked_add(amount)?,
226+
_ => return None,
227+
}
228+
}
229+
230+
if saw_unit && digits.is_empty() {
231+
Some(seconds)
232+
} else {
233+
None
234+
}
235+
}
236+
237+
#[cfg(test)]
238+
mod tests {
239+
use super::*;
240+
use std::collections::HashMap;
241+
242+
fn fixed_now() -> DateTime<Utc> {
243+
DateTime::from_timestamp(1_700_000_000, 0).unwrap()
244+
}
245+
246+
#[test]
247+
fn parses_openai_duration_reset_headers() {
248+
let reset = parse_reset_header("6m0s", fixed_now()).unwrap();
249+
250+
assert_eq!(reset, fixed_now() + Duration::seconds(360));
251+
}
252+
253+
#[test]
254+
fn parses_plain_integer_reset_as_seconds_from_now() {
255+
let reset = parse_reset_header("60", fixed_now()).unwrap();
256+
257+
assert_eq!(reset, fixed_now() + Duration::seconds(60));
258+
}
259+
260+
#[test]
261+
fn preserves_unix_timestamp_reset_headers() {
262+
let reset = parse_reset_header("1800000000", fixed_now()).unwrap();
263+
264+
assert_eq!(reset, DateTime::from_timestamp(1_800_000_000, 0).unwrap());
265+
}
266+
267+
#[test]
268+
fn parses_reset_headers_in_rate_limit_info() {
269+
let mut headers = HashMap::new();
270+
headers.insert(
271+
"x-ratelimit-reset-requests".to_string(),
272+
"1m30s".to_string(),
273+
);
274+
275+
let info = parse_rate_limit_headers(&headers);
276+
277+
assert!(info.reset_at.is_some());
278+
assert!(info.reset_at.unwrap() > Utc::now());
279+
}
280+
281+
#[test]
282+
fn ignores_invalid_reset_headers() {
283+
assert!(parse_reset_header("soon", fixed_now()).is_none());
284+
}
285+
}

0 commit comments

Comments
 (0)