cortexkit_role_classifier/
errors.rs1pub use cortexkit_model_batch::errors::{
22 admission_class, classes, is_batch_level, item_class, ErrorClass, ItemError, ItemErrorDetail,
23 Refusal, RefusalDetail, ACCOUNT_LIMIT_EXHAUSTED, ACCOUNT_NOT_ENTITLED, ACCOUNT_WALLED,
24 ANSWER_TOO_LARGE, AUTH_FAILED, BATCH_ID_REUSE, BATCH_IN_PROGRESS, BATCH_LEVEL_CODES,
25 COST_EXCEEDED, INVALID_ITEM, INVALID_PARAMS, MODEL_NOT_CLASSIFIER, MODEL_UNAVAILABLE,
26 MODEL_UNKNOWN, PROVIDER_ERROR, PROVIDER_REFUSED, RATE_LIMITED,
27};
28
29pub const ADMISSION_CODES: &[&str] = &[
33 INVALID_PARAMS,
34 BATCH_ID_REUSE,
35 MODEL_UNKNOWN,
36 MODEL_NOT_CLASSIFIER,
37 COST_EXCEEDED,
38 ACCOUNT_WALLED,
39 BATCH_IN_PROGRESS,
40 AUTH_FAILED,
41 MODEL_UNAVAILABLE,
42];
43
44pub const ITEM_CODES: &[&str] = &[
51 RATE_LIMITED,
52 PROVIDER_ERROR,
53 PROVIDER_REFUSED,
54 INVALID_ITEM,
55 COST_EXCEEDED,
56 AUTH_FAILED,
57 MODEL_UNAVAILABLE,
58];
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[test]
65 fn every_listed_code_has_its_tables_class() {
66 for code in ADMISSION_CODES {
67 assert!(admission_class(code).is_some(), "{code}");
68 }
69 for code in ITEM_CODES {
70 assert!(item_class(code).is_some(), "{code}");
71 }
72 assert_eq!(
73 admission_class(BATCH_IN_PROGRESS),
74 Some(ErrorClass::Transient)
75 );
76 assert_eq!(admission_class(ACCOUNT_WALLED), Some(ErrorClass::Transient));
77 assert_eq!(item_class(RATE_LIMITED), Some(ErrorClass::Transient));
78 assert_eq!(item_class(PROVIDER_ERROR), Some(ErrorClass::Transient));
79 assert_eq!(item_class(PROVIDER_REFUSED), Some(ErrorClass::Permanent));
80 assert_eq!(item_class(INVALID_ITEM), Some(ErrorClass::Permanent));
81 assert_eq!(item_class(COST_EXCEEDED), Some(ErrorClass::Permanent));
82 for code in [AUTH_FAILED, MODEL_UNAVAILABLE] {
83 assert_eq!(item_class(code), Some(ErrorClass::Permanent), "{code}");
84 assert_eq!(admission_class(code), Some(ErrorClass::Permanent), "{code}");
85 }
86 assert_eq!(admission_class("nope"), None);
87 }
88
89 #[test]
90 fn an_unknown_class_is_kept_and_acted_on_as_permanent() {
91 let error: ItemError = serde_json::from_str(
92 r#"{"code":"quota_gone","class":"degraded","message":"m","retry_after_ms":5}"#,
93 )
94 .unwrap();
95 assert_eq!(error.code, "quota_gone");
96 assert_eq!(error.class, ErrorClass::Other("degraded".into()));
97 assert_eq!(error.class.effective(), ErrorClass::Permanent);
98 assert!(!error.is_retried_on_resend());
99 assert_eq!(
100 serde_json::to_string(&error).unwrap(),
101 r#"{"code":"quota_gone","class":"degraded","message":"m","retry_after_ms":5}"#
102 );
103 }
104
105 #[test]
106 fn stored_transient_errors_are_retried_and_permanent_ones_are_not() {
107 for code in [RATE_LIMITED, PROVIDER_ERROR] {
108 assert!(
109 ItemError::for_code(code, "m").is_retried_on_resend(),
110 "{code}"
111 );
112 }
113 for code in [PROVIDER_REFUSED, INVALID_ITEM, COST_EXCEEDED] {
114 assert!(
115 !ItemError::for_code(code, "m").is_retried_on_resend(),
116 "{code}"
117 );
118 }
119 }
120
121 #[test]
122 fn auth_and_model_failures_are_never_stored_and_stop_the_call() {
123 for code in [AUTH_FAILED, MODEL_UNAVAILABLE] {
124 let error = ItemError::for_code(code, "m");
125 assert!(!error.is_stored(), "{code}");
126 assert!(error.stops_the_call(), "{code}");
127 }
128 assert!(ItemError::for_code(RATE_LIMITED, "m").stops_the_call());
129 for code in [
130 PROVIDER_ERROR,
131 PROVIDER_REFUSED,
132 INVALID_ITEM,
133 COST_EXCEEDED,
134 ] {
135 let error = ItemError::for_code(code, "m");
136 assert!(error.is_stored(), "{code}");
137 assert!(!error.stops_the_call(), "{code}");
138 }
139 assert!(ItemError::for_code(RATE_LIMITED, "m").is_stored());
140 assert_eq!(
141 Refusal::model_unavailable("m").detail.model.as_deref(),
142 Some("m")
143 );
144 assert_eq!(Refusal::auth_failed().detail.class, ErrorClass::Permanent);
145 }
146
147 #[test]
148 fn the_batch_level_account_codes_are_never_stored_and_stop_the_call() {
149 assert_eq!(
150 BATCH_LEVEL_CODES,
151 [ACCOUNT_LIMIT_EXHAUSTED, ACCOUNT_NOT_ENTITLED]
152 );
153 for code in BATCH_LEVEL_CODES {
154 let error = ItemError::for_code(code, "m");
155 assert!(!error.is_stored(), "{code}");
156 assert!(error.stops_the_call(), "{code}");
157 assert!(!ADMISSION_CODES.contains(code) && !ITEM_CODES.contains(code));
158 }
159 assert_eq!(
160 item_class(ACCOUNT_LIMIT_EXHAUSTED),
161 Some(ErrorClass::Transient)
162 );
163 assert_eq!(
164 item_class(ACCOUNT_NOT_ENTITLED),
165 Some(ErrorClass::Permanent)
166 );
167 }
168
169 #[test]
170 fn answer_too_large_is_in_the_vocabulary_but_not_the_tables() {
171 assert_eq!(item_class(ANSWER_TOO_LARGE), Some(ErrorClass::Permanent));
172 assert!(ItemError::for_code(ANSWER_TOO_LARGE, "m").is_stored());
173 assert!(!ITEM_CODES.contains(&ANSWER_TOO_LARGE));
174 }
175
176 #[test]
177 fn refusals_carry_their_class_and_field() {
178 let refusal = Refusal::over_limit("items", 100, "too many items");
179 assert_eq!(refusal.code, INVALID_PARAMS);
180 assert_eq!(refusal.field(), Some("items"));
181 assert_eq!(refusal.detail.limit, Some(100));
182 assert_eq!(refusal.detail.class, ErrorClass::Permanent);
183 let busy = Refusal::batch_in_progress(250);
184 assert_eq!(busy.detail.class, ErrorClass::Transient);
185 assert_eq!(busy.detail.retry_after_ms, Some(250));
186 }
187}