Skip to main content

cortexkit_model_batch/
status.rs

1//! Mapping a provider's error to a per-item code, once the runner's own
2//! retries are spent.
3//!
4//! [`item_code_for_status`] is the mapping by HTTP status alone, for a
5//! provider that documents no error codes. [`workers_ai_item_code`] reads
6//! Cloudflare Workers AI's error code first, and falls back to the status
7//! mapping for a code it does not list. `RULES.md` ยง15 states both tables.
8
9use crate::errors::{
10    ACCOUNT_LIMIT_EXHAUSTED, ACCOUNT_NOT_ENTITLED, AUTH_FAILED, INVALID_ITEM, MODEL_UNAVAILABLE,
11    PROVIDER_ERROR, RATE_LIMITED,
12};
13
14/// The per-item error code for a provider's HTTP status, for a provider
15/// whose error body the runner does not read:
16///
17/// - 401 and 403: `auth_failed`;
18/// - 404: `model_unavailable`;
19/// - 429 and 529 (an "overloaded" status some providers use):
20///   `rate_limited`;
21/// - 408 and every 5xx: `provider_error`;
22/// - every other 4xx (400, 413, 422 and the rest): `invalid_item`.
23///
24/// 401, 403 and 404 say nothing about the item, so they are never
25/// `invalid_item`, which a caller reads as "drop this item". `None` for a
26/// status that is not an error. Nothing maps to `provider_refused`.
27pub fn item_code_for_status(status: u16) -> Option<&'static str> {
28    match status {
29        401 | 403 => Some(AUTH_FAILED),
30        404 => Some(MODEL_UNAVAILABLE),
31        429 | 529 => Some(RATE_LIMITED),
32        408 | 500..=599 => Some(PROVIDER_ERROR),
33        400..=499 => Some(INVALID_ITEM),
34        _ => None,
35    }
36}
37
38/// The Workers AI error codes this crate maps, with what each means in
39/// Cloudflare's error reference.
40pub mod workers_ai_codes {
41    /// 400: the input data is invalid.
42    pub const INVALID_DATA: u64 = 5004;
43    /// 400: the request is incomplete.
44    pub const INCOMPLETE_REQUEST: u64 = 3003;
45    /// 400: no such model.
46    pub const NO_SUCH_MODEL: u64 = 5007;
47    /// 404: no such model.
48    pub const MODEL_NOT_FOUND: u64 = 3042;
49    /// 403: the model needs a paid plan.
50    pub const NEEDS_PAID_PLAN: u64 = 5035;
51    /// 403: the account has not accepted the model's agreement.
52    pub const MODEL_AGREEMENT: u64 = 5016;
53    /// 403: the account is blocked.
54    pub const ACCOUNT_BLOCKED: u64 = 3023;
55    /// 413: the request is too large.
56    pub const REQUEST_TOO_LARGE: u64 = 3006;
57    /// 408: the request timed out.
58    pub const TIMEOUT: u64 = 3007;
59    /// 408: the request was aborted.
60    pub const ABORTED: u64 = 3008;
61    /// 429: the account's daily free allocation is used up.
62    pub const DAILY_ALLOCATION_USED: u64 = 3036;
63    /// 429: the model is out of capacity.
64    pub const OUT_OF_CAPACITY: u64 = 3040;
65
66    /// Every code [`super::workers_ai_item_code`] maps by code.
67    pub const ALL: &[u64] = &[
68        INVALID_DATA,
69        INCOMPLETE_REQUEST,
70        NO_SUCH_MODEL,
71        MODEL_NOT_FOUND,
72        NEEDS_PAID_PLAN,
73        MODEL_AGREEMENT,
74        ACCOUNT_BLOCKED,
75        REQUEST_TOO_LARGE,
76        TIMEOUT,
77        ABORTED,
78        DAILY_ALLOCATION_USED,
79        OUT_OF_CAPACITY,
80    ];
81}
82
83/// The per-item error code for a Cloudflare Workers AI error: the HTTP
84/// status and the first `errors[].code` of its reply, when it gave one.
85///
86/// A listed code decides, whatever the status:
87///
88/// | code | role code |
89/// |---|---|
90/// | 5004 invalid data, 3003 incomplete request | `invalid_item` |
91/// | 5007, 3042 no such model | `model_unavailable` |
92/// | 5035 paid plan, 5016 model agreement, 3023 account blocked | `account_not_entitled` |
93/// | 3006 request too large | `invalid_item` |
94/// | 3007 timeout, 3008 aborted | `provider_error` |
95/// | 3036 daily free allocation used | `account_limit_exhausted` |
96/// | 3040 out of capacity | `rate_limited` |
97///
98/// Any other code, or none, keeps [`item_code_for_status`]: a 403 without
99/// an account code is `auth_failed`, any other 429 is `rate_limited`, any
100/// 5xx is `provider_error`. A runner puts an unlisted code into the item
101/// error's `message`, so a caller can still read it.
102pub fn workers_ai_item_code(status: u16, code: Option<u64>) -> Option<&'static str> {
103    use workers_ai_codes::*;
104    if !(400..=599).contains(&status) {
105        return None;
106    }
107    match code {
108        Some(INVALID_DATA | INCOMPLETE_REQUEST | REQUEST_TOO_LARGE) => Some(INVALID_ITEM),
109        Some(NO_SUCH_MODEL | MODEL_NOT_FOUND) => Some(MODEL_UNAVAILABLE),
110        Some(NEEDS_PAID_PLAN | MODEL_AGREEMENT | ACCOUNT_BLOCKED) => Some(ACCOUNT_NOT_ENTITLED),
111        Some(TIMEOUT | ABORTED) => Some(PROVIDER_ERROR),
112        Some(DAILY_ALLOCATION_USED) => Some(ACCOUNT_LIMIT_EXHAUSTED),
113        Some(OUT_OF_CAPACITY) => Some(RATE_LIMITED),
114        _ => item_code_for_status(status),
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn statuses_map_to_item_codes() {
124        for status in [429, 529] {
125            assert_eq!(item_code_for_status(status), Some(RATE_LIMITED));
126        }
127        for status in [408, 500, 502, 503] {
128            assert_eq!(item_code_for_status(status), Some(PROVIDER_ERROR));
129        }
130        for status in [400, 413, 422, 409] {
131            assert_eq!(item_code_for_status(status), Some(INVALID_ITEM));
132        }
133        for status in [401, 403] {
134            assert_eq!(item_code_for_status(status), Some(AUTH_FAILED));
135        }
136        assert_eq!(item_code_for_status(404), Some(MODEL_UNAVAILABLE));
137        assert_eq!(item_code_for_status(200), None);
138    }
139
140    #[test]
141    fn workers_ai_codes_decide_before_the_status() {
142        assert_eq!(
143            workers_ai_item_code(429, Some(3036)),
144            Some(ACCOUNT_LIMIT_EXHAUSTED)
145        );
146        assert_eq!(workers_ai_item_code(429, Some(3040)), Some(RATE_LIMITED));
147        assert_eq!(workers_ai_item_code(429, None), Some(RATE_LIMITED));
148        for code in [5035, 5016, 3023] {
149            assert_eq!(
150                workers_ai_item_code(403, Some(code)),
151                Some(ACCOUNT_NOT_ENTITLED)
152            );
153        }
154        assert_eq!(workers_ai_item_code(403, Some(9999)), Some(AUTH_FAILED));
155        assert_eq!(
156            workers_ai_item_code(400, Some(5007)),
157            Some(MODEL_UNAVAILABLE)
158        );
159        assert_eq!(
160            workers_ai_item_code(404, Some(3042)),
161            Some(MODEL_UNAVAILABLE)
162        );
163        assert_eq!(workers_ai_item_code(413, Some(3006)), Some(INVALID_ITEM));
164        assert_eq!(workers_ai_item_code(408, Some(3008)), Some(PROVIDER_ERROR));
165        assert_eq!(workers_ai_item_code(200, Some(3036)), None);
166    }
167}