Skip to main content

rig_core/completion/
request.rs

1//! Completion request, response, and provider trait definitions.
2//!
3//! Most applications use [`Prompt`] or [`Chat`] through
4//! [`Agent`](crate::agent::Agent). Provider integrations implement
5//! [`CompletionModel`] and translate [`CompletionRequest`] into their native HTTP
6//! request format.
7//!
8//! # Low-level request example
9//!
10//! ```no_run
11//! use rig_core::{
12//!     client::{CompletionClient, ProviderClient},
13//!     completion::{AssistantContent, CompletionModel},
14//!     providers::openai,
15//! };
16//!
17//! # async fn run() -> Result<(), Box<dyn std::error::Error>> {
18//! let client = openai::Client::from_env()?;
19//! let model = client.completion_model(openai::GPT_5_2);
20//!
21//! let request = model
22//!     .completion_request("Who are you?")
23//!     .preamble("You are a concise assistant.".to_string())
24//!     .temperature(0.5)
25//!     .build();
26//!
27//! let response = model.completion(request).await?;
28//! for item in response.choice {
29//!     if let AssistantContent::Text(text) = item {
30//!         println!("{}", text.text);
31//!     }
32//! }
33//! # Ok(())
34//! # }
35//! ```
36
37use super::message::{AssistantContent, DocumentMediaType};
38use crate::message::ToolChoice;
39use crate::provider_response;
40use crate::streaming::StreamingCompletionResponse;
41use crate::tool::server::ToolServerError;
42use crate::wasm_compat::{WasmCompatSend, WasmCompatSync};
43use crate::{OneOrMany, http_client};
44use crate::{
45    json_utils,
46    message::{Message, UserContent},
47    tool::ToolSetError,
48};
49
50use serde::de::DeserializeOwned;
51use serde::{Deserialize, Serialize};
52use std::collections::HashMap;
53use std::ops::{Add, AddAssign};
54use thiserror::Error;
55
56// Errors
57/// Errors returned by completion models.
58///
59/// Inspect provider failures with [`Self::provider_response_body`],
60/// [`Self::provider_response_json`], and [`Self::provider_response_status`].
61/// These recover the provider's raw HTTP status and response body so you can
62/// branch on a provider error code or surface a precise diagnostic. The same
63/// helpers are available on `EmbeddingError`, `ImageGenerationError`,
64/// `AudioGenerationError`, `TranscriptionError`, `RerankError`, and (forwarded)
65/// [`PromptError`].
66///
67/// ```
68/// use rig_core::completion::CompletionError;
69///
70/// /// Log the provider's raw error response when a completion fails.
71/// fn report(error: &CompletionError) {
72///     if let Some(status) = error.provider_response_status() {
73///         // Note: this can be a 2xx status for providers that return an error
74///         // envelope alongside a success status — the error itself means failure.
75///         eprintln!("provider returned HTTP {status}");
76///     }
77///     match error.provider_response_json() {
78///         Ok(Some(json)) => eprintln!("provider error payload: {json}"),
79///         Ok(None) => eprintln!("no provider response body (e.g. a transport error)"),
80///         Err(_) => eprintln!(
81///             "provider response body was not valid JSON: {:?}",
82///             error.provider_response_body(),
83///         ),
84///     }
85/// }
86/// ```
87#[derive(Debug, Error)]
88#[non_exhaustive]
89pub enum CompletionError {
90    /// Http error (e.g.: connection error, timeout, etc.)
91    #[error("HttpError: {0}")]
92    HttpError(#[from] http_client::Error),
93
94    /// Json error (e.g.: serialization, deserialization)
95    #[error("JsonError: {0}")]
96    JsonError(#[from] serde_json::Error),
97
98    /// Url error (e.g.: invalid URL)
99    #[error("UrlError: {0}")]
100    UrlError(#[from] url::ParseError),
101
102    #[cfg(not(target_family = "wasm"))]
103    /// Error building the completion request
104    #[error("RequestError: {0}")]
105    RequestError(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
106
107    #[cfg(target_family = "wasm")]
108    /// Error building the completion request
109    #[error("RequestError: {0}")]
110    RequestError(#[from] Box<dyn std::error::Error + 'static>),
111
112    /// Error parsing the completion response
113    #[error("ResponseError: {0}")]
114    ResponseError(String),
115
116    /// Error returned by the completion model provider
117    #[error("ProviderError: {0}")]
118    ProviderError(String),
119
120    /// Raw error response preserved from the completion model provider
121    #[error("ProviderResponseError: {0}")]
122    ProviderResponse(provider_response::ProviderResponseError),
123}
124
125crate::provider_response::impl_provider_response_helpers!(CompletionError);
126
127impl CompletionError {
128    /// Maps an SSE transport error into a completion error without flattening HTTP failures.
129    ///
130    /// Non-success HTTP responses remain [`CompletionError::HttpError`] so provider response
131    /// helpers can read status and body. Other transport failures keep the existing
132    /// [`CompletionError::ProviderError`] display string behavior.
133    pub(crate) fn from_stream_transport(error: http_client::Error) -> Self {
134        if error.non_success_status().is_some() {
135            Self::HttpError(error)
136        } else {
137            Self::ProviderError(error.to_string())
138        }
139    }
140}
141
142/// Errors from agent prompting.
143///
144/// When the failure wraps [`CompletionError`], [`Self::provider_response_body`],
145/// [`Self::provider_response_json`], and [`Self::provider_response_status`] forward
146/// to the inner completion error's helpers.
147#[derive(Debug, Error)]
148pub enum PromptError {
149    /// Something went wrong with the completion
150    #[error("CompletionError: {0}")]
151    CompletionError(#[from] CompletionError),
152
153    /// There was an error while using a tool
154    #[error("ToolCallError: {0}")]
155    ToolError(#[from] ToolSetError),
156
157    /// There was an issue while executing a tool on a tool server
158    #[error("ToolServerError: {0}")]
159    ToolServerError(#[from] Box<ToolServerError>),
160
161    /// The run exhausted its total model-call budget. The budget includes the
162    /// initial call and every retry or continuation; increase `.max_turns()` if
163    /// the intended interaction requires more calls.
164    #[error("MaxTurnsError: reached max turns limit: {max_turns}")]
165    MaxTurnsError {
166        max_turns: usize,
167        chat_history: Box<Vec<Message>>,
168        prompt: Box<Message>,
169    },
170
171    /// A prompting loop was cancelled.
172    #[error("PromptCancelled: {reason}")]
173    PromptCancelled {
174        chat_history: Vec<Message>,
175        reason: String,
176    },
177
178    /// The model emitted a structured tool call for a tool Rig did not allow
179    /// for the current turn.
180    #[error(
181        "UnknownToolCall: model attempted to call unknown or disallowed tool `{tool_name}`. Available tools: {available_tools:?}. Allowed tools for this turn: {allowed_tools:?}"
182    )]
183    UnknownToolCall {
184        tool_name: String,
185        available_tools: Vec<String>,
186        allowed_tools: Vec<String>,
187        chat_history: Box<Vec<Message>>,
188    },
189}
190
191/// Surface [`crate::memory::ConversationMemory`] failures through the existing
192/// [`CompletionError::RequestError`] variant so adding memory support does not
193/// require a new top-level [`PromptError`] arm in downstream exhaustive matchers.
194impl From<crate::memory::MemoryError> for PromptError {
195    fn from(err: crate::memory::MemoryError) -> Self {
196        Self::CompletionError(CompletionError::RequestError(Box::new(err)))
197    }
198}
199
200impl PromptError {
201    /// Returns the provider response body when this wraps a completion error that exposes one.
202    pub fn provider_response_body(&self) -> Option<&str> {
203        match self {
204            Self::CompletionError(error) => error.provider_response_body(),
205            _ => None,
206        }
207    }
208
209    /// Parses the provider response body as JSON when available through a wrapped completion error.
210    ///
211    /// Returns:
212    /// - `Ok(Some(value))` when a body is present and valid JSON.
213    /// - `Ok(None)` when no provider response body is available.
214    /// - `Err(error)` when a body is present but isn't valid JSON.
215    pub fn provider_response_json(&self) -> Result<Option<serde_json::Value>, serde_json::Error> {
216        match self {
217            Self::CompletionError(error) => error.provider_response_json(),
218            _ => Ok(None),
219        }
220    }
221
222    /// Returns the HTTP status when this wraps a completion error that preserves
223    /// one, including from non-success HTTP responses and 2xx error envelopes.
224    pub fn provider_response_status(&self) -> Option<http::StatusCode> {
225        match self {
226            Self::CompletionError(error) => error.provider_response_status(),
227            _ => None,
228        }
229    }
230
231    pub(crate) fn prompt_cancelled(
232        chat_history: impl IntoIterator<Item = Message>,
233        reason: impl Into<String>,
234    ) -> Self {
235        Self::PromptCancelled {
236            chat_history: chat_history.into_iter().collect(),
237            reason: reason.into(),
238        }
239    }
240}
241
242/// Errors that can occur when using typed structured output via [`TypedPrompt::prompt_typed`].
243///
244/// When the failure wraps [`PromptError`] that in turn wraps a [`CompletionError`]
245/// exposing a provider response, [`Self::provider_response_body`],
246/// [`Self::provider_response_json`], and [`Self::provider_response_status`] forward
247/// through the chain.
248#[derive(Debug, Error)]
249pub enum StructuredOutputError {
250    /// An error occurred during the prompt execution.
251    #[error("PromptError: {0}")]
252    PromptError(#[from] Box<PromptError>),
253
254    /// Failed to deserialize the model's response into the target type.
255    #[error("DeserializationError: {0}")]
256    DeserializationError(#[from] serde_json::Error),
257
258    /// The model returned an empty response.
259    #[error("EmptyResponse: model returned no content")]
260    EmptyResponse,
261}
262
263impl StructuredOutputError {
264    /// Returns the provider response body when this wraps a prompt error that exposes one.
265    pub fn provider_response_body(&self) -> Option<&str> {
266        match self {
267            Self::PromptError(error) => error.provider_response_body(),
268            _ => None,
269        }
270    }
271
272    /// Parses the provider response body as JSON when available through a wrapped prompt error.
273    ///
274    /// Returns:
275    /// - `Ok(Some(value))` when a body is present and valid JSON.
276    /// - `Ok(None)` when no provider response body is available.
277    /// - `Err(error)` when a body is present but isn't valid JSON.
278    pub fn provider_response_json(&self) -> Result<Option<serde_json::Value>, serde_json::Error> {
279        match self {
280            Self::PromptError(error) => error.provider_response_json(),
281            _ => Ok(None),
282        }
283    }
284
285    /// Returns the HTTP status when this wraps a prompt error that preserves one.
286    pub fn provider_response_status(&self) -> Option<http::StatusCode> {
287        match self {
288            Self::PromptError(error) => error.provider_response_status(),
289            _ => None,
290        }
291    }
292}
293
294#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
295pub struct Document {
296    /// Stable document identifier included in the serialized context block.
297    pub id: String,
298    /// Text content passed to the model as retrieval or static context.
299    pub text: String,
300    /// Additional string metadata rendered before the document text.
301    #[serde(flatten)]
302    pub additional_props: HashMap<String, String>,
303}
304
305impl std::fmt::Display for Document {
306    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307        write!(
308            f,
309            concat!("<file id: {}>\n", "{}\n", "</file>\n"),
310            self.id,
311            if self.additional_props.is_empty() {
312                self.text.clone()
313            } else {
314                let mut sorted_props = self.additional_props.iter().collect::<Vec<_>>();
315                sorted_props.sort_by(|a, b| a.0.cmp(b.0));
316                let metadata = sorted_props
317                    .iter()
318                    .map(|(k, v)| format!("{k}: {v:?}"))
319                    .collect::<Vec<_>>()
320                    .join(" ");
321                format!("<metadata {} />\n{}", metadata, self.text)
322            }
323        )
324    }
325}
326
327#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
328pub struct ToolDefinition {
329    /// Tool name exposed to the model. It must match the registered tool name.
330    pub name: String,
331    /// Human-readable description sent to the model.
332    pub description: String,
333    /// JSON Schema describing tool arguments.
334    pub parameters: serde_json::Value,
335}
336
337/// Provider-native tool definition.
338///
339/// Stored under `additional_params.tools` and forwarded by providers that support
340/// provider-managed tools.
341#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
342pub struct ProviderToolDefinition {
343    /// Tool type/kind name as expected by the target provider (for example `web_search`).
344    #[serde(rename = "type")]
345    pub kind: String,
346    /// Additional provider-specific configuration for this hosted tool.
347    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
348    pub config: serde_json::Map<String, serde_json::Value>,
349}
350
351impl ProviderToolDefinition {
352    /// Creates a provider-hosted tool definition by type.
353    pub fn new(kind: impl Into<String>) -> Self {
354        Self {
355            kind: kind.into(),
356            config: serde_json::Map::new(),
357        }
358    }
359
360    /// Adds a provider-specific configuration key/value.
361    pub fn with_config(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
362        self.config.insert(key.into(), value);
363        self
364    }
365}
366
367// ================================================================
368// Implementations
369// ================================================================
370/// Trait defining a high-level LLM simple prompt interface (i.e.: prompt in, response out).
371pub trait Prompt: WasmCompatSend + WasmCompatSync {
372    /// Send a simple prompt to the underlying completion model.
373    ///
374    /// If the completion model's response is a message, then it is returned as a string.
375    ///
376    /// If the completion model's response is a tool call, then the tool is called and
377    /// the result is returned as a string.
378    ///
379    /// If the tool does not exist, or the tool call fails, then an error is returned.
380    fn prompt(
381        &self,
382        prompt: impl Into<Message> + WasmCompatSend,
383    ) -> impl std::future::IntoFuture<Output = Result<String, PromptError>, IntoFuture: WasmCompatSend>;
384}
385
386/// Trait defining a high-level LLM chat interface (i.e.: prompt and chat history in, response out).
387pub trait Chat: WasmCompatSend + WasmCompatSync {
388    /// Send a prompt with optional chat history to the underlying completion model.
389    ///
390    /// If the completion model's response is a message, then it is returned as a string.
391    ///
392    /// If the completion model's response is a tool call, then the tool is called and the result
393    /// is returned as a string.
394    ///
395    /// If the tool does not exist, or the tool call fails, then an error is returned.
396    ///
397    /// The prompt and any assistant or tool messages produced during the turn
398    /// are appended to `chat_history`. Callers should pass the current
399    /// conversation history and should not push the user prompt themselves
400    /// before calling this method.
401    fn chat(
402        &self,
403        prompt: impl Into<Message> + WasmCompatSend,
404        chat_history: &mut Vec<Message>,
405    ) -> impl std::future::Future<Output = Result<String, PromptError>> + WasmCompatSend;
406}
407
408/// Trait defining a high-level typed prompt interface for structured output.
409///
410/// This trait provides an ergonomic way to get typed responses from an LLM by automatically
411/// generating a JSON schema from the target type and deserializing the response.
412///
413/// # Example
414/// ```rust,ignore
415/// use rig_core::prelude::*;
416/// use schemars::JsonSchema;
417/// use serde::Deserialize;
418///
419/// #[derive(Debug, Deserialize, JsonSchema)]
420/// struct WeatherForecast {
421///     city: String,
422///     temperature_f: f64,
423///     conditions: String,
424/// }
425///
426/// let agent = client.agent("gpt-4o").build();
427/// let forecast: WeatherForecast = agent
428///     .prompt_typed("What's the weather in NYC?")
429///     .await?;
430/// ```
431pub trait TypedPrompt: WasmCompatSend + WasmCompatSync {
432    /// The type of the typed prompt request returned by `prompt_typed`.
433    type TypedRequest<T>: std::future::IntoFuture<Output = Result<T, StructuredOutputError>>
434    where
435        T: schemars::JsonSchema + DeserializeOwned + WasmCompatSend + 'static;
436
437    /// Send a prompt and receive a typed structured response.
438    ///
439    /// The JSON schema for `T` is automatically generated and sent to the provider.
440    /// Providers that support native structured outputs will constrain the model's
441    /// response to match this schema.
442    ///
443    /// # Type Parameters
444    /// * `T` - The target type to deserialize the response into. Must implement
445    ///   `JsonSchema` (for schema generation), `DeserializeOwned` (for deserialization),
446    ///   and `WasmCompatSend` (for async compatibility).
447    ///
448    /// # Example
449    /// ```rust,ignore
450    /// // Type can be inferred
451    /// let forecast: WeatherForecast = agent.prompt_typed("What's the weather?").await?;
452    ///
453    /// // Or specified explicitly with turbofish
454    /// let forecast = agent.prompt_typed::<WeatherForecast>("What's the weather?").await?;
455    /// ```
456    fn prompt_typed<T>(&self, prompt: impl Into<Message> + WasmCompatSend) -> Self::TypedRequest<T>
457    where
458        T: schemars::JsonSchema + DeserializeOwned + WasmCompatSend;
459}
460
461/// Trait defining a low-level LLM completion interface
462pub trait Completion<M: CompletionModel> {
463    /// Generates a completion request builder for the given `prompt` and `chat_history`.
464    /// This function is meant to be called by the user to further customize the
465    /// request at prompt time before sending it.
466    ///
467    /// ❗IMPORTANT: The type that implements this trait might have already
468    /// populated fields in the builder (the exact fields depend on the type).
469    /// For fields that have already been set by the model, calling the corresponding
470    /// method on the builder will overwrite the value set by the model.
471    ///
472    /// For example, the request builder returned by [`Agent::completion`](crate::agent::Agent::completion) will already
473    /// contain the `preamble` provided when creating the agent.
474    fn completion<I, T>(
475        &self,
476        prompt: impl Into<Message> + WasmCompatSend,
477        chat_history: I,
478    ) -> impl std::future::Future<Output = Result<CompletionRequestBuilder<M>, CompletionError>>
479    + WasmCompatSend
480    where
481        I: IntoIterator<Item = T> + WasmCompatSend,
482        T: Into<Message>;
483}
484
485/// General completion response struct that contains the high-level completion choice
486/// and the raw response. The completion choice contains one or more assistant content.
487#[derive(Debug)]
488pub struct CompletionResponse<T> {
489    /// The completion choice (represented by one or more assistant message content)
490    /// returned by the completion model provider
491    pub choice: OneOrMany<AssistantContent>,
492    /// Tokens used during prompting and responding
493    pub usage: Usage,
494    /// The raw response returned by the completion model provider
495    pub raw_response: T,
496    /// Provider-assigned message ID (e.g. OpenAI Responses API `msg_` ID).
497    /// Used to pair reasoning input items with their output items in multi-turn.
498    pub message_id: Option<String>,
499}
500
501/// A trait for grabbing the token usage of a completion response.
502///
503/// Primarily designed for streamed completion responses in streamed multi-turn, as otherwise it would be impossible to do.
504pub trait GetTokenUsage {
505    /// Returns token usage for this response. Zero-valued usage is
506    /// [`Usage`]'s documented sentinel for missing provider usage metrics;
507    /// response types that carry no usage return [`Usage::new`].
508    fn token_usage(&self) -> crate::completion::Usage;
509}
510
511impl GetTokenUsage for () {
512    fn token_usage(&self) -> crate::completion::Usage {
513        crate::completion::Usage::new()
514    }
515}
516
517impl<T> GetTokenUsage for Option<T>
518where
519    T: GetTokenUsage,
520{
521    fn token_usage(&self) -> crate::completion::Usage {
522        if let Some(usage) = self {
523            usage.token_usage()
524        } else {
525            crate::completion::Usage::new()
526        }
527    }
528}
529
530/// Struct representing the token usage for a completion request.
531/// If tokens used are `0`, then the provider failed to supply token usage metrics.
532#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
533pub struct Usage {
534    /// The number of input ("prompt") tokens used in a given request.
535    pub input_tokens: u64,
536    /// The number of output ("completion") tokens used in a given request.
537    pub output_tokens: u64,
538    /// We store this separately as some providers may only report one number
539    pub total_tokens: u64,
540    /// The number of input tokens read from a provider-managed cache
541    pub cached_input_tokens: u64,
542    /// The number of input tokens written to a provider-managed cache
543    pub cache_creation_input_tokens: u64,
544    /// The number of tool-use prompt tokens used in a given request.
545    #[serde(default)]
546    pub tool_use_prompt_tokens: u64,
547    /// The number of tokens spent on internal reasoning / "thoughts" by reasoning-capable
548    /// models (e.g. Gemini thinking, Anthropic extended thinking, OpenAI o-series).
549    pub reasoning_tokens: u64,
550}
551
552impl Usage {
553    /// Creates a new instance of `Usage`.
554    pub fn new() -> Self {
555        Self {
556            input_tokens: 0,
557            output_tokens: 0,
558            total_tokens: 0,
559            cached_input_tokens: 0,
560            cache_creation_input_tokens: 0,
561            tool_use_prompt_tokens: 0,
562            reasoning_tokens: 0,
563        }
564    }
565
566    /// Whether any usage values are set and non-zero.
567    ///
568    /// Zero-valued usage is this type's documented sentinel for "the provider
569    /// supplied no usage metrics", so `false` means usage was not reported.
570    pub fn has_values(&self) -> bool {
571        *self != Self::new()
572    }
573}
574
575impl Default for Usage {
576    fn default() -> Self {
577        Self::new()
578    }
579}
580
581impl Add for Usage {
582    type Output = Self;
583
584    fn add(self, other: Self) -> Self::Output {
585        Self {
586            input_tokens: self.input_tokens + other.input_tokens,
587            output_tokens: self.output_tokens + other.output_tokens,
588            total_tokens: self.total_tokens + other.total_tokens,
589            cached_input_tokens: self.cached_input_tokens + other.cached_input_tokens,
590            cache_creation_input_tokens: self.cache_creation_input_tokens
591                + other.cache_creation_input_tokens,
592            tool_use_prompt_tokens: self.tool_use_prompt_tokens + other.tool_use_prompt_tokens,
593            reasoning_tokens: self.reasoning_tokens + other.reasoning_tokens,
594        }
595    }
596}
597
598impl AddAssign for Usage {
599    fn add_assign(&mut self, other: Self) {
600        self.input_tokens += other.input_tokens;
601        self.output_tokens += other.output_tokens;
602        self.total_tokens += other.total_tokens;
603        self.cached_input_tokens += other.cached_input_tokens;
604        self.cache_creation_input_tokens += other.cache_creation_input_tokens;
605        self.tool_use_prompt_tokens += other.tool_use_prompt_tokens;
606        self.reasoning_tokens += other.reasoning_tokens;
607    }
608}
609
610/// Trait defining a completion model that can be used to generate completion responses.
611/// This trait is meant to be implemented by the user to define a custom completion model,
612/// either from a third party provider (e.g.: OpenAI) or a local model.
613pub trait CompletionModel: Clone + WasmCompatSend + WasmCompatSync {
614    /// The raw response type returned by the underlying completion model.
615    type Response: WasmCompatSend + WasmCompatSync + Serialize + DeserializeOwned;
616    /// The raw response type returned by the underlying completion model when streaming.
617    type StreamingResponse: Clone
618        + Unpin
619        + WasmCompatSend
620        + WasmCompatSync
621        + Serialize
622        + DeserializeOwned
623        + GetTokenUsage;
624
625    /// Provider client type used to construct this model.
626    type Client;
627
628    /// Construct a model handle from a provider client and model identifier.
629    fn make(client: &Self::Client, model: impl Into<String>) -> Self;
630
631    /// Generates a completion response for the given completion request.
632    fn completion(
633        &self,
634        request: CompletionRequest,
635    ) -> impl std::future::Future<
636        Output = Result<CompletionResponse<Self::Response>, CompletionError>,
637    > + WasmCompatSend;
638
639    fn stream(
640        &self,
641        request: CompletionRequest,
642    ) -> impl std::future::Future<
643        Output = Result<StreamingCompletionResponse<Self::StreamingResponse>, CompletionError>,
644    > + WasmCompatSend;
645
646    /// Generates a completion request builder for the given `prompt`.
647    fn completion_request(&self, prompt: impl Into<Message>) -> CompletionRequestBuilder<Self> {
648        CompletionRequestBuilder::new(self.clone(), prompt)
649    }
650
651    /// Whether this provider's native structured output (`output_schema` ->
652    /// `format`/`response_format`) composes with tool calls in the same
653    /// multi-turn request without suppressing them.
654    ///
655    /// Defaults to `false` (the safe assumption: the native constraint may make
656    /// the model emit schema JSON instead of calling its tools — see issue
657    /// #1928). Providers that enforce structured output *and* tool use together
658    /// (e.g. OpenAI, Anthropic) override this to `true`, which lets the agent's
659    /// [`OutputMode::Auto`](crate::agent::OutputMode::Auto) keep using guaranteed
660    /// native structured output even when the agent has tools.
661    fn composes_native_output_with_tools(&self) -> bool {
662        false
663    }
664}
665
666/// Struct representing a general completion request that can be sent to a completion model provider.
667#[derive(Debug, Clone, Serialize, Deserialize)]
668pub struct CompletionRequest {
669    /// Optional model override for this request.
670    pub model: Option<String>,
671    /// Legacy preamble field preserved for backwards compatibility.
672    ///
673    /// New code should prefer a leading [`Message::System`]
674    /// in `chat_history` as the canonical representation of system instructions.
675    pub preamble: Option<String>,
676    /// The chat history to be sent to the completion model provider.
677    /// The very last message will always be the prompt (hence why there is *always* one)
678    pub chat_history: OneOrMany<Message>,
679    /// The documents to be sent to the completion model provider
680    pub documents: Vec<Document>,
681    /// The tools to be sent to the completion model provider
682    pub tools: Vec<ToolDefinition>,
683    /// The temperature to be sent to the completion model provider
684    pub temperature: Option<f64>,
685    /// The max tokens to be sent to the completion model provider
686    pub max_tokens: Option<u64>,
687    /// Whether tools are required to be used by the model provider or not before providing a response.
688    pub tool_choice: Option<ToolChoice>,
689    /// Additional provider-specific parameters to be sent to the completion model provider
690    pub additional_params: Option<serde_json::Value>,
691    /// Optional JSON Schema for structured output. When set, providers that support
692    /// native structured outputs will constrain the model's response to match this schema.
693    pub output_schema: Option<schemars::Schema>,
694}
695
696impl CompletionRequest {
697    /// Extracts a name from the output schema's `"title"` field, falling back to `"response_schema"`.
698    /// Useful for providers that require a name alongside the JSON Schema (e.g., OpenAI).
699    pub fn output_schema_name(&self) -> Option<String> {
700        self.output_schema.as_ref().map(|schema| {
701            schema
702                .as_object()
703                .and_then(|o| o.get("title"))
704                .and_then(|v| v.as_str())
705                .unwrap_or("response_schema")
706                .to_string()
707        })
708    }
709
710    /// Returns documents normalized into a message (if any).
711    /// Most providers do not accept documents directly as input, so it needs to convert into a
712    /// `Message` so that it can be incorporated into `chat_history`.
713    pub fn normalized_documents(&self) -> Option<Message> {
714        Self::normalized_documents_from(&self.documents)
715    }
716
717    fn normalized_documents_from(documents: &[Document]) -> Option<Message> {
718        if documents.is_empty() {
719            return None;
720        }
721
722        // Most providers will convert documents into a text unless it can handle document messages.
723        // We use `UserContent::document` for those who handle it directly!
724        let messages = documents
725            .iter()
726            .map(|doc| {
727                UserContent::document(
728                    doc.to_string(),
729                    // In the future, we can customize `Document` to pass these extra types through.
730                    // Most providers ditch these but they might want to use them.
731                    Some(DocumentMediaType::TXT),
732                )
733            })
734            .collect::<Vec<_>>();
735
736        OneOrMany::from_iter_optional(messages).map(|content| Message::User { content })
737    }
738
739    pub(crate) fn chat_history_with_documents(&self) -> Vec<Message> {
740        let mut chat_history = self.chat_history.iter().cloned().collect::<Vec<_>>();
741        if let Some(documents) = self.normalized_documents() {
742            let insert_at = chat_history
743                .iter()
744                .position(|message| !matches!(message, Message::System { .. }))
745                .unwrap_or(chat_history.len());
746            chat_history.insert(insert_at, documents);
747        }
748        chat_history
749    }
750
751    /// Adds a provider-hosted tool by storing it in `additional_params.tools`.
752    pub fn with_provider_tool(mut self, tool: ProviderToolDefinition) -> Self {
753        self.additional_params =
754            merge_provider_tools_into_additional_params(self.additional_params, vec![tool]);
755        self
756    }
757
758    /// Adds provider-hosted tools by storing them in `additional_params.tools`.
759    pub fn with_provider_tools(mut self, tools: Vec<ProviderToolDefinition>) -> Self {
760        self.additional_params =
761            merge_provider_tools_into_additional_params(self.additional_params, tools);
762        self
763    }
764}
765
766fn merge_provider_tools_into_additional_params(
767    additional_params: Option<serde_json::Value>,
768    provider_tools: Vec<ProviderToolDefinition>,
769) -> Option<serde_json::Value> {
770    if provider_tools.is_empty() {
771        return additional_params;
772    }
773
774    let mut provider_tools_json = provider_tools
775        .into_iter()
776        .map(|ProviderToolDefinition { kind, mut config }| {
777            // Force the provider tool type from the strongly-typed field.
778            config.insert("type".to_string(), serde_json::Value::String(kind));
779            serde_json::Value::Object(config)
780        })
781        .collect::<Vec<_>>();
782
783    let mut params_map = match additional_params {
784        Some(serde_json::Value::Object(map)) => map,
785        Some(serde_json::Value::Bool(stream)) => {
786            let mut map = serde_json::Map::new();
787            map.insert("stream".to_string(), serde_json::Value::Bool(stream));
788            map
789        }
790        _ => serde_json::Map::new(),
791    };
792
793    let mut merged_tools = match params_map.remove("tools") {
794        Some(serde_json::Value::Array(existing)) => existing,
795        _ => Vec::new(),
796    };
797    merged_tools.append(&mut provider_tools_json);
798    params_map.insert("tools".to_string(), serde_json::Value::Array(merged_tools));
799    Some(serde_json::Value::Object(params_map))
800}
801
802/// Builder struct for constructing a completion request.
803///
804/// Example usage:
805/// ```no_run
806/// use rig_core::{
807///     client::CompletionClient,
808///     providers::openai::{Client, self},
809///     completion::{CompletionModel, CompletionRequestBuilder},
810/// };
811///
812/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
813/// let openai = Client::new("your-openai-api-key")?;
814/// let model = openai.completion_model(openai::GPT_5_2);
815///
816/// // Create the completion request and execute it separately
817/// let request = CompletionRequestBuilder::new(model.clone(), "Who are you?".to_string())
818///     .preamble("You are Marvin from the Hitchhiker's Guide to the Galaxy.".to_string())
819///     .temperature(0.5)
820///     .build();
821///
822/// let response = model.completion(request).await?;
823/// # Ok(())
824/// # }
825/// ```
826///
827/// Alternatively, you can execute the completion request directly from the builder:
828/// ```no_run
829/// use rig_core::{
830///     client::CompletionClient,
831///     providers::openai::{Client, self},
832///     completion::CompletionRequestBuilder,
833/// };
834///
835/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
836/// let openai = Client::new("your-openai-api-key")?;
837/// let model = openai.completion_model(openai::GPT_5_2);
838///
839/// // Create the completion request and execute it directly
840/// let response = CompletionRequestBuilder::new(model, "Who are you?".to_string())
841///     .preamble("You are Marvin from the Hitchhiker's Guide to the Galaxy.".to_string())
842///     .temperature(0.5)
843///     .send()
844///     .await?;
845/// # Ok(())
846/// # }
847/// ```
848///
849/// Note: It is usually unnecessary to create a completion request builder directly.
850/// Instead, use the [CompletionModel::completion_request] method.
851pub struct CompletionRequestBuilder<M: CompletionModel> {
852    model: M,
853    prompt: Message,
854    request_model: Option<String>,
855    preamble: Option<String>,
856    chat_history: Vec<Message>,
857    documents: Vec<Document>,
858    tools: Vec<ToolDefinition>,
859    provider_tools: Vec<ProviderToolDefinition>,
860    temperature: Option<f64>,
861    max_tokens: Option<u64>,
862    tool_choice: Option<ToolChoice>,
863    additional_params: Option<serde_json::Value>,
864    output_schema: Option<schemars::Schema>,
865}
866
867impl<M: CompletionModel> CompletionRequestBuilder<M> {
868    pub fn new(model: M, prompt: impl Into<Message>) -> Self {
869        Self {
870            model,
871            prompt: prompt.into(),
872            request_model: None,
873            preamble: None,
874            chat_history: Vec::new(),
875            documents: Vec::new(),
876            tools: Vec::new(),
877            provider_tools: Vec::new(),
878            temperature: None,
879            max_tokens: None,
880            tool_choice: None,
881            additional_params: None,
882            output_schema: None,
883        }
884    }
885
886    /// Sets the preamble for the completion request.
887    pub fn preamble(mut self, preamble: String) -> Self {
888        // Legacy public API: funnel preamble into canonical system messages at build-time.
889        self.preamble = Some(preamble);
890        self
891    }
892
893    /// Overrides the model used for this request.
894    pub fn model(mut self, model: impl Into<String>) -> Self {
895        self.request_model = Some(model.into());
896        self
897    }
898
899    /// Overrides the model used for this request.
900    pub fn model_opt(mut self, model: Option<String>) -> Self {
901        self.request_model = model;
902        self
903    }
904
905    pub fn without_preamble(mut self) -> Self {
906        self.preamble = None;
907        self
908    }
909
910    /// Adds a message to the chat history for the completion request.
911    pub fn message(mut self, message: Message) -> Self {
912        self.chat_history.push(message);
913
914        self
915    }
916
917    /// Adds a list of messages to the chat history for the completion request.
918    pub fn messages(mut self, messages: impl IntoIterator<Item = Message>) -> Self {
919        self.chat_history.extend(messages);
920
921        self
922    }
923
924    /// Adds a document to the completion request.
925    pub fn document(mut self, document: Document) -> Self {
926        self.documents.push(document);
927        self
928    }
929
930    /// Adds a list of documents to the completion request.
931    pub fn documents(self, documents: impl IntoIterator<Item = Document>) -> Self {
932        documents
933            .into_iter()
934            .fold(self, |builder, doc| builder.document(doc))
935    }
936
937    /// Adds a tool to the completion request.
938    pub fn tool(mut self, tool: ToolDefinition) -> Self {
939        self.tools.push(tool);
940        self
941    }
942
943    /// Adds a list of tools to the completion request.
944    pub fn tools(self, tools: Vec<ToolDefinition>) -> Self {
945        tools
946            .into_iter()
947            .fold(self, |builder, tool| builder.tool(tool))
948    }
949
950    /// Adds a provider-hosted tool to the completion request.
951    pub fn provider_tool(mut self, tool: ProviderToolDefinition) -> Self {
952        self.provider_tools.push(tool);
953        self
954    }
955
956    /// Adds provider-hosted tools to the completion request.
957    pub fn provider_tools(self, tools: Vec<ProviderToolDefinition>) -> Self {
958        tools
959            .into_iter()
960            .fold(self, |builder, tool| builder.provider_tool(tool))
961    }
962
963    /// Adds additional parameters to the completion request.
964    /// This can be used to set additional provider-specific parameters. For example,
965    /// Cohere's completion models accept a `connectors` parameter that can be used to
966    /// specify the data connectors used by Cohere when executing the completion
967    /// (see `examples/cohere_connectors.rs`).
968    pub fn additional_params(mut self, additional_params: serde_json::Value) -> Self {
969        match self.additional_params {
970            Some(params) => {
971                self.additional_params = Some(json_utils::merge(params, additional_params));
972            }
973            None => {
974                self.additional_params = Some(additional_params);
975            }
976        }
977        self
978    }
979
980    /// Sets the additional parameters for the completion request.
981    /// This can be used to set additional provider-specific parameters. For example,
982    /// Cohere's completion models accept a `connectors` parameter that can be used to
983    /// specify the data connectors used by Cohere when executing the completion
984    /// (see `examples/cohere_connectors.rs`).
985    pub fn additional_params_opt(mut self, additional_params: Option<serde_json::Value>) -> Self {
986        self.additional_params = additional_params;
987        self
988    }
989
990    /// Sets the temperature for the completion request.
991    pub fn temperature(mut self, temperature: f64) -> Self {
992        self.temperature = Some(temperature);
993        self
994    }
995
996    /// Sets the temperature for the completion request.
997    pub fn temperature_opt(mut self, temperature: Option<f64>) -> Self {
998        self.temperature = temperature;
999        self
1000    }
1001
1002    /// Sets the max tokens for the completion request.
1003    /// Note: This is required if using Anthropic
1004    pub fn max_tokens(mut self, max_tokens: u64) -> Self {
1005        self.max_tokens = Some(max_tokens);
1006        self
1007    }
1008
1009    /// Sets the max tokens for the completion request.
1010    /// Note: This is required if using Anthropic
1011    pub fn max_tokens_opt(mut self, max_tokens: Option<u64>) -> Self {
1012        self.max_tokens = max_tokens;
1013        self
1014    }
1015
1016    /// Sets the thing.
1017    pub fn tool_choice(mut self, tool_choice: ToolChoice) -> Self {
1018        self.tool_choice = Some(tool_choice);
1019        self
1020    }
1021
1022    /// Sets the output schema for structured output. When set, providers that support
1023    /// native structured outputs will constrain the model's response to match this schema.
1024    /// NOTE: For direct type conversion, you may want to use `Agent::prompt_typed()` - using this method
1025    /// with `Agent::prompt()` will still output a String at the end, it'll just be compatible with whatever
1026    /// type you want to use here. This method is primarily an escape hatch for agents being used as tools
1027    /// to still be able to leverage structured outputs.
1028    pub fn output_schema(mut self, schema: schemars::Schema) -> Self {
1029        self.output_schema = Some(schema);
1030        self
1031    }
1032
1033    /// Sets the output schema for structured output from an optional value.
1034    /// NOTE: For direct type conversion, you may want to use `Agent::prompt_typed()` - using this method
1035    /// with `Agent::prompt()` will still output a String at the end, it'll just be compatible with whatever
1036    /// type you want to use here. This method is primarily an escape hatch for agents being used as tools
1037    /// to still be able to leverage structured outputs.
1038    pub fn output_schema_opt(mut self, schema: Option<schemars::Schema>) -> Self {
1039        self.output_schema = schema;
1040        self
1041    }
1042
1043    /// Builds the completion request.
1044    pub fn build(self) -> CompletionRequest {
1045        // Build the final message list, prepending preamble if present
1046        let mut chat_history = self.chat_history;
1047        let prompt = self.prompt;
1048        if let Some(preamble) = self.preamble {
1049            chat_history.insert(0, Message::system(preamble));
1050        }
1051
1052        chat_history.push(prompt.clone());
1053
1054        let chat_history =
1055            OneOrMany::from_iter_optional(chat_history).unwrap_or_else(|| OneOrMany::one(prompt));
1056        let additional_params = merge_provider_tools_into_additional_params(
1057            self.additional_params,
1058            self.provider_tools,
1059        );
1060
1061        CompletionRequest {
1062            model: self.request_model,
1063            preamble: None,
1064            chat_history,
1065            documents: self.documents,
1066            tools: self.tools,
1067            temperature: self.temperature,
1068            max_tokens: self.max_tokens,
1069            tool_choice: self.tool_choice,
1070            additional_params,
1071            output_schema: self.output_schema,
1072        }
1073    }
1074
1075    /// Sends the completion request to the completion model provider and returns the completion response.
1076    pub async fn send(self) -> Result<CompletionResponse<M::Response>, CompletionError> {
1077        let model = self.model.clone();
1078        model.completion(self.build()).await
1079    }
1080
1081    /// Stream the completion request
1082    pub async fn stream<'a>(
1083        self,
1084    ) -> Result<StreamingCompletionResponse<M::StreamingResponse>, CompletionError>
1085    where
1086        <M as CompletionModel>::StreamingResponse: 'a,
1087        Self: 'a,
1088    {
1089        let model = self.model.clone();
1090        model.stream(self.build()).await
1091    }
1092}
1093
1094#[cfg(test)]
1095mod tests {
1096    #[test]
1097    fn usage_has_values_reflects_the_zero_sentinel() {
1098        use super::Usage;
1099
1100        assert!(!Usage::new().has_values());
1101
1102        let mut usage = Usage::new();
1103        usage.reasoning_tokens = 1;
1104        assert!(usage.has_values());
1105    }
1106
1107    use super::*;
1108    use crate::test_utils::MockCompletionModel;
1109
1110    fn test_document(id: &str, text: &str) -> Document {
1111        Document {
1112            id: id.to_string(),
1113            text: text.to_string(),
1114            additional_props: HashMap::new(),
1115        }
1116    }
1117
1118    fn is_document_message(message: &Message, expected_id: &str) -> bool {
1119        let Message::User { content } = message else {
1120            return false;
1121        };
1122
1123        content.iter().any(|content| {
1124            matches!(
1125                content,
1126                UserContent::Document(document)
1127                    if document.data.to_string().contains(&format!("<file id: {expected_id}>"))
1128            )
1129        })
1130    }
1131
1132    #[test]
1133    fn test_document_display_without_metadata() {
1134        let doc = Document {
1135            id: "123".to_string(),
1136            text: "This is a test document.".to_string(),
1137            additional_props: HashMap::new(),
1138        };
1139
1140        let expected = "<file id: 123>\nThis is a test document.\n</file>\n";
1141        assert_eq!(format!("{doc}"), expected);
1142    }
1143
1144    #[test]
1145    fn test_document_display_with_metadata() {
1146        let mut additional_props = HashMap::new();
1147        additional_props.insert("author".to_string(), "John Doe".to_string());
1148        additional_props.insert("length".to_string(), "42".to_string());
1149
1150        let doc = Document {
1151            id: "123".to_string(),
1152            text: "This is a test document.".to_string(),
1153            additional_props,
1154        };
1155
1156        let expected = concat!(
1157            "<file id: 123>\n",
1158            "<metadata author: \"John Doe\" length: \"42\" />\n",
1159            "This is a test document.\n",
1160            "</file>\n"
1161        );
1162        assert_eq!(format!("{doc}"), expected);
1163    }
1164
1165    #[test]
1166    fn test_normalize_documents_with_documents() {
1167        let doc1 = Document {
1168            id: "doc1".to_string(),
1169            text: "Document 1 text.".to_string(),
1170            additional_props: HashMap::new(),
1171        };
1172
1173        let doc2 = Document {
1174            id: "doc2".to_string(),
1175            text: "Document 2 text.".to_string(),
1176            additional_props: HashMap::new(),
1177        };
1178
1179        let request = CompletionRequest {
1180            model: None,
1181            preamble: None,
1182            chat_history: OneOrMany::one("What is the capital of France?".into()),
1183            documents: vec![doc1, doc2],
1184            tools: Vec::new(),
1185            temperature: None,
1186            max_tokens: None,
1187            tool_choice: None,
1188            additional_params: None,
1189            output_schema: None,
1190        };
1191
1192        let expected = Message::User {
1193            content: OneOrMany::many(vec![
1194                UserContent::document(
1195                    "<file id: doc1>\nDocument 1 text.\n</file>\n".to_string(),
1196                    Some(DocumentMediaType::TXT),
1197                ),
1198                UserContent::document(
1199                    "<file id: doc2>\nDocument 2 text.\n</file>\n".to_string(),
1200                    Some(DocumentMediaType::TXT),
1201                ),
1202            ])
1203            .expect("There will be at least one document"),
1204        };
1205
1206        assert_eq!(request.normalized_documents(), Some(expected));
1207    }
1208
1209    #[test]
1210    fn test_normalize_documents_without_documents() {
1211        let request = CompletionRequest {
1212            model: None,
1213            preamble: None,
1214            chat_history: OneOrMany::one("What is the capital of France?".into()),
1215            documents: Vec::new(),
1216            tools: Vec::new(),
1217            temperature: None,
1218            max_tokens: None,
1219            tool_choice: None,
1220            additional_params: None,
1221            output_schema: None,
1222        };
1223
1224        assert_eq!(request.normalized_documents(), None);
1225    }
1226
1227    #[test]
1228    fn preamble_builder_funnels_to_system_message() {
1229        let request =
1230            CompletionRequestBuilder::new(MockCompletionModel::default(), Message::user("Prompt"))
1231                .preamble("System prompt".to_string())
1232                .message(Message::user("History"))
1233                .build();
1234
1235        assert_eq!(request.preamble, None);
1236
1237        let history = request.chat_history.into_iter().collect::<Vec<_>>();
1238        assert_eq!(history.len(), 3);
1239        assert!(matches!(
1240            &history[0],
1241            Message::System { content } if content == "System prompt"
1242        ));
1243        assert!(matches!(&history[1], Message::User { .. }));
1244        assert!(matches!(&history[2], Message::User { .. }));
1245    }
1246
1247    #[test]
1248    fn without_preamble_removes_legacy_preamble_injection() {
1249        let request =
1250            CompletionRequestBuilder::new(MockCompletionModel::default(), Message::user("Prompt"))
1251                .preamble("System prompt".to_string())
1252                .without_preamble()
1253                .build();
1254
1255        assert_eq!(request.preamble, None);
1256        let history = request.chat_history.into_iter().collect::<Vec<_>>();
1257        assert_eq!(history.len(), 1);
1258        assert!(matches!(&history[0], Message::User { .. }));
1259    }
1260
1261    #[test]
1262    fn build_places_documents_after_preamble_system_message() {
1263        let request =
1264            CompletionRequestBuilder::new(MockCompletionModel::default(), Message::user("Prompt"))
1265                .preamble("System prompt".to_string())
1266                .document(test_document("doc1", "Document text."))
1267                .build();
1268
1269        assert_eq!(request.documents.len(), 1);
1270
1271        let history = request.chat_history_with_documents();
1272        let history = history.iter().collect::<Vec<_>>();
1273        assert_eq!(history.len(), 3);
1274        assert!(matches!(
1275            history[0],
1276            Message::System { content } if content == "System prompt"
1277        ));
1278        assert!(is_document_message(history[1], "doc1"));
1279        assert!(matches!(history[2], Message::User { .. }));
1280    }
1281
1282    #[test]
1283    fn build_places_documents_after_leading_system_messages_before_prior_history() {
1284        let request =
1285            CompletionRequestBuilder::new(MockCompletionModel::default(), Message::user("Prompt"))
1286                .message(Message::system("System one"))
1287                .message(Message::system("System two"))
1288                .message(Message::user("Earlier user turn"))
1289                .message(Message::assistant("Earlier assistant turn"))
1290                .document(test_document("doc1", "Document text."))
1291                .build();
1292
1293        let history = request.chat_history_with_documents();
1294        let history = history.iter().collect::<Vec<_>>();
1295        assert_eq!(history.len(), 6);
1296        assert!(matches!(
1297            history[0],
1298            Message::System { content } if content == "System one"
1299        ));
1300        assert!(matches!(
1301            history[1],
1302            Message::System { content } if content == "System two"
1303        ));
1304        assert!(is_document_message(history[2], "doc1"));
1305        assert!(matches!(history[3], Message::User { .. }));
1306        assert!(matches!(history[4], Message::Assistant { .. }));
1307        assert!(matches!(history[5], Message::User { .. }));
1308    }
1309
1310    #[test]
1311    fn build_without_documents_keeps_message_order_unchanged() {
1312        let request =
1313            CompletionRequestBuilder::new(MockCompletionModel::default(), Message::user("Prompt"))
1314                .message(Message::system("System prompt"))
1315                .message(Message::user("Earlier user turn"))
1316                .build();
1317
1318        let history = request.chat_history.iter().collect::<Vec<_>>();
1319        assert_eq!(history.len(), 3);
1320        assert!(matches!(
1321            history[0],
1322            Message::System { content } if content == "System prompt"
1323        ));
1324        assert!(matches!(history[1], Message::User { .. }));
1325        assert!(matches!(history[2], Message::User { .. }));
1326    }
1327
1328    #[test]
1329    fn chat_history_with_documents_places_documents_after_leading_system_messages() {
1330        let request = CompletionRequest {
1331            model: None,
1332            preamble: None,
1333            chat_history: OneOrMany::many(vec![
1334                Message::system("System prompt"),
1335                Message::assistant("Earlier assistant turn"),
1336                Message::user("Earlier user turn"),
1337                Message::user("Prompt"),
1338            ])
1339            .unwrap(),
1340            documents: vec![test_document("doc1", "Document text.")],
1341            tools: Vec::new(),
1342            temperature: None,
1343            max_tokens: None,
1344            tool_choice: None,
1345            additional_params: None,
1346            output_schema: None,
1347        };
1348
1349        assert_eq!(request.documents.len(), 1);
1350
1351        let history = request.chat_history_with_documents();
1352        let history = history.iter().collect::<Vec<_>>();
1353        assert_eq!(history.len(), 5);
1354        assert!(matches!(history[0], Message::System { .. }));
1355        assert!(is_document_message(history[1], "doc1"));
1356        assert!(matches!(history[2], Message::Assistant { .. }));
1357        assert!(matches!(history[3], Message::User { .. }));
1358        assert!(matches!(history[4], Message::User { .. }));
1359    }
1360
1361    #[test]
1362    fn chat_history_with_documents_places_documents_before_mid_conversation_system_messages() {
1363        let request = CompletionRequest {
1364            model: None,
1365            preamble: None,
1366            chat_history: OneOrMany::many(vec![
1367                Message::system("Leading system prompt"),
1368                Message::assistant("Earlier assistant turn"),
1369                Message::system("Mid-conversation instruction"),
1370                Message::user("Prompt"),
1371            ])
1372            .unwrap(),
1373            documents: vec![test_document("doc1", "Document text.")],
1374            tools: Vec::new(),
1375            temperature: None,
1376            max_tokens: None,
1377            tool_choice: None,
1378            additional_params: None,
1379            output_schema: None,
1380        };
1381
1382        let history = request.chat_history_with_documents();
1383        let history = history.iter().collect::<Vec<_>>();
1384        assert_eq!(history.len(), 5);
1385        assert!(matches!(
1386            history[0],
1387            Message::System { content } if content == "Leading system prompt"
1388        ));
1389        assert!(is_document_message(history[1], "doc1"));
1390        assert!(matches!(history[2], Message::Assistant { .. }));
1391        assert!(matches!(
1392            history[3],
1393            Message::System { content } if content == "Mid-conversation instruction"
1394        ));
1395        assert!(matches!(history[4], Message::User { .. }));
1396    }
1397
1398    #[test]
1399    fn chat_history_with_documents_does_not_duplicate_documents() {
1400        let request = CompletionRequest {
1401            model: None,
1402            preamble: None,
1403            chat_history: OneOrMany::many(vec![
1404                Message::system("System prompt"),
1405                Message::user("Earlier user turn"),
1406                Message::assistant("Earlier assistant turn"),
1407                Message::user("Prompt"),
1408            ])
1409            .unwrap(),
1410            documents: vec![test_document("doc1", "Document text.")],
1411            tools: Vec::new(),
1412            temperature: None,
1413            max_tokens: None,
1414            tool_choice: None,
1415            additional_params: None,
1416            output_schema: None,
1417        };
1418
1419        let history = request.chat_history_with_documents();
1420        let document_messages = history
1421            .iter()
1422            .filter(|message| is_document_message(message, "doc1"))
1423            .count();
1424        assert_eq!(document_messages, 1);
1425    }
1426
1427    #[test]
1428    fn completion_error_provider_response_helpers_with_preserved_json_body() {
1429        let body = r#"{"error":{"code":"rate_limit","message":"slow down"}}"#;
1430        let error = CompletionError::ProviderResponse(provider_response::ProviderResponseError {
1431            status: None,
1432            body: body.to_string(),
1433        });
1434
1435        assert_eq!(error.provider_response_body(), Some(body));
1436        assert_eq!(error.provider_response_status(), None);
1437        assert_eq!(
1438            error
1439                .provider_response_json()
1440                .expect("fixture body should parse as valid JSON"),
1441            Some(serde_json::json!({
1442                "error": {
1443                    "code": "rate_limit",
1444                    "message": "slow down"
1445                }
1446            }))
1447        );
1448    }
1449
1450    #[test]
1451    fn completion_error_provider_response_helpers_with_preserved_status() {
1452        let body = r#"{"error":{"message":"too many requests"}}"#;
1453        let error = CompletionError::ProviderResponse(provider_response::ProviderResponseError {
1454            status: Some(http::StatusCode::TOO_MANY_REQUESTS),
1455            body: body.to_string(),
1456        });
1457
1458        assert_eq!(error.provider_response_body(), Some(body));
1459        assert_eq!(
1460            error.provider_response_status(),
1461            Some(http::StatusCode::TOO_MANY_REQUESTS)
1462        );
1463    }
1464
1465    #[test]
1466    fn completion_error_provider_response_helpers_with_preserved_plain_text_body() {
1467        let error = CompletionError::ProviderResponse(provider_response::ProviderResponseError {
1468            status: None,
1469            body: "provider exploded".to_string(),
1470        });
1471
1472        assert_eq!(error.provider_response_body(), Some("provider exploded"));
1473        assert_eq!(error.provider_response_status(), None);
1474        assert!(error.provider_response_json().is_err());
1475    }
1476
1477    #[test]
1478    fn completion_error_provider_error_is_not_a_provider_response() {
1479        // `ProviderError` also carries Rig-generated diagnostics, so the helpers
1480        // must not report its string as a provider response body.
1481        let error = CompletionError::ProviderError("stream transport failed".to_string());
1482
1483        assert_eq!(error.provider_response_body(), None);
1484        assert_eq!(error.provider_response_status(), None);
1485        assert_eq!(
1486            error
1487                .provider_response_json()
1488                .expect("no body is not an error"),
1489            None
1490        );
1491    }
1492
1493    #[test]
1494    fn completion_error_provider_response_helpers_with_http_non_success_body_and_status() {
1495        let body = r#"{"error":{"type":"invalid_request","message":"bad request"}}"#;
1496        let error = CompletionError::HttpError(http_client::Error::InvalidStatusCodeWithMessage(
1497            http::StatusCode::BAD_REQUEST,
1498            body.to_string(),
1499        ));
1500
1501        assert_eq!(error.provider_response_body(), Some(body));
1502        assert_eq!(
1503            error.provider_response_status(),
1504            Some(http::StatusCode::BAD_REQUEST)
1505        );
1506        assert_eq!(
1507            error.provider_response_json().expect("valid JSON body"),
1508            Some(serde_json::json!({
1509                "error": {
1510                    "type": "invalid_request",
1511                    "message": "bad request"
1512                }
1513            }))
1514        );
1515    }
1516
1517    #[test]
1518    fn completion_error_provider_response_helpers_with_unrelated_variant() {
1519        let error = CompletionError::ResponseError("failed to parse provider response".to_string());
1520
1521        assert_eq!(error.provider_response_body(), None);
1522        assert_eq!(error.provider_response_status(), None);
1523        assert_eq!(
1524            error
1525                .provider_response_json()
1526                .expect("no body is not an error"),
1527            None
1528        );
1529    }
1530
1531    #[test]
1532    fn prompt_error_provider_response_helpers_forward_wrapped_completion_error() {
1533        let body = r#"{"error":{"code":"invalid_request","message":"bad input"}}"#;
1534        let error = PromptError::CompletionError(CompletionError::ProviderResponse(
1535            provider_response::ProviderResponseError {
1536                status: None,
1537                body: body.to_string(),
1538            },
1539        ));
1540
1541        assert_eq!(error.provider_response_body(), Some(body));
1542        assert_eq!(error.provider_response_status(), None);
1543        assert_eq!(
1544            error.provider_response_json().expect("valid JSON body"),
1545            Some(serde_json::json!({
1546                "error": {
1547                    "code": "invalid_request",
1548                    "message": "bad input"
1549                }
1550            }))
1551        );
1552    }
1553
1554    #[test]
1555    fn prompt_error_provider_response_helpers_forward_http_status_and_body() {
1556        let body = r#"{"error":{"message":"unauthorized"}}"#;
1557        let error = PromptError::CompletionError(CompletionError::HttpError(
1558            http_client::Error::InvalidStatusCodeWithMessage(
1559                http::StatusCode::UNAUTHORIZED,
1560                body.to_string(),
1561            ),
1562        ));
1563
1564        assert_eq!(error.provider_response_body(), Some(body));
1565        assert_eq!(
1566            error.provider_response_status(),
1567            Some(http::StatusCode::UNAUTHORIZED)
1568        );
1569        assert_eq!(
1570            error.provider_response_json().expect("valid JSON body"),
1571            Some(serde_json::json!({
1572                "error": { "message": "unauthorized" }
1573            }))
1574        );
1575    }
1576
1577    #[test]
1578    fn prompt_error_provider_response_helpers_return_none_for_unrelated_variant() {
1579        let error = PromptError::PromptCancelled {
1580            chat_history: vec![Message::user("hi")],
1581            reason: "cancelled".to_string(),
1582        };
1583
1584        assert_eq!(error.provider_response_body(), None);
1585        assert_eq!(error.provider_response_status(), None);
1586        assert_eq!(
1587            error
1588                .provider_response_json()
1589                .expect("no body is not an error"),
1590            None
1591        );
1592    }
1593
1594    #[test]
1595    fn structured_output_error_provider_response_helpers_forward_prompt_error() {
1596        let body = r#"{"error":{"message":"bad input"}}"#;
1597        let error = StructuredOutputError::PromptError(Box::new(PromptError::CompletionError(
1598            CompletionError::ProviderResponse(provider_response::ProviderResponseError {
1599                status: Some(http::StatusCode::BAD_REQUEST),
1600                body: body.to_string(),
1601            }),
1602        )));
1603
1604        assert_eq!(error.provider_response_body(), Some(body));
1605        assert_eq!(
1606            error.provider_response_status(),
1607            Some(http::StatusCode::BAD_REQUEST)
1608        );
1609    }
1610
1611    #[test]
1612    fn provider_response_json_returns_none_for_empty_preserved_body() {
1613        let error = CompletionError::ProviderResponse(provider_response::ProviderResponseError {
1614            status: None,
1615            body: String::new(),
1616        });
1617
1618        assert_eq!(error.provider_response_body(), Some(""));
1619        assert_eq!(
1620            error
1621                .provider_response_json()
1622                .expect("empty body is not a JSON parse error"),
1623            None
1624        );
1625    }
1626}