Skip to main content

rig_core/completion/
request.rs

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