1use serde::de::DeserializeOwned;
4use thiserror::Error;
5
6use rig_core::{
7 memory::MemoryError,
8 wasm_compat::{WasmCompatSend, WasmCompatSync},
9};
10
11pub use rig_core::completion::*;
12
13#[derive(Debug, Error)]
15pub enum PromptError {
16 #[error("CompletionError: {0}")]
18 CompletionError(#[from] CompletionError),
19
20 #[error("MemoryError: {0}")]
22 MemoryError(#[from] MemoryError),
23
24 #[error("MaxTurnsError: reached max turns limit: {max_turns}")]
26 MaxTurnsError {
27 max_turns: usize,
29 chat_history: Box<Vec<Message>>,
31 prompt: Box<Message>,
33 },
34
35 #[error("PromptCancelled: {reason}")]
37 PromptCancelled {
38 chat_history: Vec<Message>,
40 reason: String,
42 },
43
44 #[error(
46 "UnknownToolCall: model attempted to call unknown or disallowed tool `{tool_name}`. Available tools: {available_tools:?}. Allowed tools for this turn: {allowed_tools:?}"
47 )]
48 UnknownToolCall {
49 tool_name: String,
51 available_tools: Vec<String>,
53 allowed_tools: Vec<String>,
55 chat_history: Box<Vec<Message>>,
57 },
58}
59
60macro_rules! forward_provider_response_helpers {
63 ($err:ident, $variant:ident, $inner:literal) => {
64 impl $err {
65 #[doc = concat!("Returns the provider response body exposed by a wrapped ", $inner, ".")]
66 pub fn provider_response_body(&self) -> Option<&str> {
67 match self {
68 Self::$variant(error) => error.provider_response_body(),
69 _ => None,
70 }
71 }
72
73 #[doc = concat!("Parses the provider response body of a wrapped ", $inner, " as JSON when present.")]
74 pub fn provider_response_json(
75 &self,
76 ) -> Result<Option<serde_json::Value>, serde_json::Error> {
77 match self {
78 Self::$variant(error) => error.provider_response_json(),
79 _ => Ok(None),
80 }
81 }
82
83 #[doc = concat!("Returns the provider transport request id exposed by a wrapped ", $inner, " (rig#2314).")]
84 pub fn provider_request_id(&self) -> Option<&str> {
85 match self {
86 Self::$variant(error) => error.provider_request_id(),
87 _ => None,
88 }
89 }
90
91 #[doc = concat!("Returns the HTTP status exposed by a wrapped ", $inner, ".")]
92 pub fn provider_response_status(&self) -> Option<http::StatusCode> {
93 match self {
94 Self::$variant(error) => error.provider_response_status(),
95 _ => None,
96 }
97 }
98
99 #[doc = concat!("Returns the response headers exposed by a wrapped ", $inner, " — e.g. `Retry-After` on a 429 (rig#2210).")]
100 pub fn provider_response_headers(&self) -> Option<&http::HeaderMap> {
101 match self {
102 Self::$variant(error) => error.provider_response_headers(),
103 _ => None,
104 }
105 }
106 }
107 };
108}
109
110forward_provider_response_helpers!(PromptError, CompletionError, "completion error");
111forward_provider_response_helpers!(StructuredOutputError, PromptError, "prompt error");
112
113impl PromptError {
114 pub(crate) fn prompt_cancelled(
115 chat_history: impl IntoIterator<Item = Message>,
116 reason: impl Into<String>,
117 ) -> Self {
118 Self::PromptCancelled {
119 chat_history: chat_history.into_iter().collect(),
120 reason: reason.into(),
121 }
122 }
123}
124
125#[derive(Debug, Error)]
127pub enum StructuredOutputError {
128 #[error("PromptError: {0}")]
130 PromptError(#[from] Box<PromptError>),
131 #[error("DeserializationError: {0}")]
133 DeserializationError(#[from] serde_json::Error),
134 #[error("EmptyResponse: model returned no content")]
136 EmptyResponse,
137}
138
139pub trait Prompt: WasmCompatSend + WasmCompatSync {
141 fn prompt(
143 &self,
144 prompt: impl Into<Message> + WasmCompatSend,
145 ) -> impl std::future::IntoFuture<Output = Result<String, PromptError>, IntoFuture: WasmCompatSend>;
146}
147
148pub trait Chat: WasmCompatSend + WasmCompatSync {
150 fn chat(
152 &self,
153 prompt: impl Into<Message> + WasmCompatSend,
154 chat_history: &mut Vec<Message>,
155 ) -> impl std::future::Future<Output = Result<String, PromptError>> + WasmCompatSend;
156}
157
158pub trait TypedPrompt: WasmCompatSend + WasmCompatSync {
160 type TypedRequest<T>: std::future::IntoFuture<Output = Result<T, StructuredOutputError>>
162 where
163 T: schemars::JsonSchema + DeserializeOwned + WasmCompatSend + 'static;
164
165 fn prompt_typed<T>(&self, prompt: impl Into<Message> + WasmCompatSend) -> Self::TypedRequest<T>
167 where
168 T: schemars::JsonSchema + DeserializeOwned + WasmCompatSend;
169}
170
171#[cfg(test)]
172mod provider_response_tests {
173 use rig_core::{ProviderResponseError, http_client};
174
175 use super::*;
176
177 #[test]
178 fn prompt_error_forwards_provider_response_to_completion_error() {
179 let body = r#"{"error":{"message":"boom"}}"#;
180 let inner =
181 CompletionError::from_http_response(http::StatusCode::SERVICE_UNAVAILABLE, body);
182 let error = PromptError::CompletionError(inner);
183
184 assert_eq!(
185 error.provider_response_status(),
186 Some(http::StatusCode::SERVICE_UNAVAILABLE),
187 );
188 assert_eq!(error.provider_response_body(), Some(body));
189 assert_eq!(
190 error
191 .provider_response_json()
192 .expect("valid json")
193 .expect("present json")["error"]["message"],
194 "boom",
195 );
196 }
197
198 #[test]
199 fn prompt_error_provider_response_helpers_forward_http_status_and_body() {
200 let body = r#"{"error":{"message":"unauthorized"}}"#;
201 let error = PromptError::CompletionError(CompletionError::HttpError(
202 http_client::Error::InvalidStatusCodeWithMessage(
203 http::StatusCode::UNAUTHORIZED,
204 body.to_string(),
205 ),
206 ));
207
208 assert_eq!(error.provider_response_body(), Some(body));
209 assert_eq!(
210 error.provider_response_status(),
211 Some(http::StatusCode::UNAUTHORIZED)
212 );
213 assert_eq!(
214 error.provider_response_json().expect("valid JSON body"),
215 Some(serde_json::json!({
216 "error": { "message": "unauthorized" }
217 }))
218 );
219 }
220
221 #[test]
222 fn prompt_error_provider_response_helpers_forward_wrapped_completion_error() {
223 let body = r#"{"error":{"code":"invalid_request","message":"bad input"}}"#;
224 let error = PromptError::CompletionError(CompletionError::ProviderResponse(
225 ProviderResponseError::without_status(body),
226 ));
227
228 assert_eq!(error.provider_response_body(), Some(body));
229 assert_eq!(error.provider_response_status(), None);
230 assert_eq!(error.provider_request_id(), None);
232 assert_eq!(
233 error.provider_response_json().expect("valid JSON body"),
234 Some(serde_json::json!({
235 "error": {
236 "code": "invalid_request",
237 "message": "bad input"
238 }
239 }))
240 );
241 }
242
243 #[test]
248 fn prompt_error_forwards_captured_response_headers() {
249 let mut headers = http::HeaderMap::new();
250 headers.insert(
251 http::header::RETRY_AFTER,
252 http::HeaderValue::from_static("20"),
253 );
254 let body = r#"{"error":{"message":"rate limited"}}"#;
255
256 for completion_error in [
257 CompletionError::from_http_response_with_request_id(
259 http::StatusCode::TOO_MANY_REQUESTS,
260 body,
261 Some("req_abc".to_string()),
262 )
263 .with_response_headers(Some(Box::new(headers.clone()))),
264 CompletionError::from_http_response(http::StatusCode::TOO_MANY_REQUESTS, body)
266 .with_response_headers(Some(Box::new(headers.clone()))),
267 ] {
268 let prompt_error = PromptError::CompletionError(completion_error);
269 assert_eq!(
270 prompt_error
271 .provider_response_headers()
272 .and_then(|headers| headers.get(http::header::RETRY_AFTER))
273 .and_then(|value| value.to_str().ok()),
274 Some("20"),
275 "PromptError dropped the captured headers",
276 );
277
278 let structured = StructuredOutputError::PromptError(Box::new(prompt_error));
279 assert_eq!(
280 structured
281 .provider_response_headers()
282 .and_then(|headers| headers.get(http::header::RETRY_AFTER))
283 .and_then(|value| value.to_str().ok()),
284 Some("20"),
285 "StructuredOutputError dropped the captured headers",
286 );
287 }
288 }
289
290 #[test]
292 fn prompt_error_reports_no_headers_for_unrelated_variants() {
293 let error = PromptError::PromptCancelled {
294 chat_history: vec![Message::user("hi")],
295 reason: "cancelled".to_string(),
296 };
297 assert!(error.provider_response_headers().is_none());
298 assert!(
299 StructuredOutputError::EmptyResponse
300 .provider_response_headers()
301 .is_none()
302 );
303 }
304
305 #[test]
308 fn prompt_error_forwards_the_provider_request_id() {
309 let error = PromptError::CompletionError(CompletionError::ProviderResponse(
310 ProviderResponseError::new(http::StatusCode::NOT_FOUND, "{}")
311 .with_provider_request_id(Some("req_failed_call".to_string())),
312 ));
313 assert_eq!(error.provider_request_id(), Some("req_failed_call"));
314 }
315
316 #[test]
317 fn prompt_error_provider_response_helpers_return_none_for_unrelated_variant() {
318 let error = PromptError::PromptCancelled {
319 chat_history: vec![Message::user("hi")],
320 reason: "cancelled".to_string(),
321 };
322
323 assert_eq!(error.provider_response_body(), None);
324 assert_eq!(error.provider_response_status(), None);
325 assert_eq!(
326 error
327 .provider_response_json()
328 .expect("no body is not an error"),
329 None
330 );
331 }
332
333 #[test]
334 fn structured_output_error_provider_response_helpers_forward_prompt_error() {
335 let body = r#"{"error":{"message":"bad input"}}"#;
336 let error = StructuredOutputError::PromptError(Box::new(PromptError::CompletionError(
337 CompletionError::ProviderResponse(ProviderResponseError::new(
338 http::StatusCode::BAD_REQUEST,
339 body,
340 )),
341 )));
342
343 assert_eq!(error.provider_response_body(), Some(body));
344 assert_eq!(
345 error.provider_response_status(),
346 Some(http::StatusCode::BAD_REQUEST)
347 );
348 }
349}