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::http_client;
37use crate::message::ToolChoice;
38use crate::provider_response;
39use crate::streaming::StreamingCompletionResponse;
40use crate::wasm_compat::{WasmCompatSend, WasmCompatSync};
41use crate::{
42    json_utils,
43    message::{Message, UserContent},
44};
45
46use serde::{Deserialize, Serialize};
47use std::collections::HashMap;
48use std::ops::{Add, AddAssign};
49use thiserror::Error;
50
51// Errors
52/// Errors returned by completion models.
53///
54/// Inspect provider failures with [`Self::provider_response_body`],
55/// [`Self::provider_response_json`], and [`Self::provider_response_status`].
56/// These recover the provider's raw HTTP status and response body so you can
57/// branch on a provider error code or surface a precise diagnostic. The same
58/// helpers are available on `EmbeddingError`, `ImageGenerationError`,
59/// `AudioGenerationError`, `TranscriptionError`, and `RerankError`.
60///
61/// ```
62/// use rig_core::completion::CompletionError;
63///
64/// /// Log the provider's raw error response when a completion fails.
65/// fn report(error: &CompletionError) {
66///     if let Some(status) = error.provider_response_status() {
67///         // Note: this can be a 2xx status for providers that return an error
68///         // envelope alongside a success status — the error itself means failure.
69///         eprintln!("provider returned HTTP {status}");
70///     }
71///     match error.provider_response_json() {
72///         Ok(Some(json)) => eprintln!("provider error payload: {json}"),
73///         Ok(None) => eprintln!("no provider response body (e.g. a transport error)"),
74///         Err(_) => eprintln!(
75///             "provider response body was not valid JSON: {:?}",
76///             error.provider_response_body(),
77///         ),
78///     }
79/// }
80/// ```
81#[derive(Debug, Error)]
82pub enum CompletionError {
83    /// Http error (e.g.: connection error, timeout, etc.)
84    #[error("HttpError: {0}")]
85    HttpError(#[from] http_client::Error),
86
87    /// Json error (e.g.: serialization, deserialization)
88    #[error("JsonError: {0}")]
89    JsonError(#[from] serde_json::Error),
90
91    /// Url error (e.g.: invalid URL)
92    #[error("UrlError: {0}")]
93    UrlError(#[from] url::ParseError),
94
95    #[cfg(not(target_family = "wasm"))]
96    /// Error building the completion request
97    #[error("RequestError: {0}")]
98    RequestError(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
99
100    #[cfg(target_family = "wasm")]
101    /// Error building the completion request
102    #[error("RequestError: {0}")]
103    RequestError(#[from] Box<dyn std::error::Error + 'static>),
104
105    /// Error parsing the completion response
106    #[error("ResponseError: {0}")]
107    ResponseError(String),
108
109    /// Error returned by the completion model provider
110    #[error("ProviderError: {0}")]
111    ProviderError(String),
112
113    /// Raw error response preserved from the completion model provider
114    #[error("ProviderResponseError: {0}")]
115    ProviderResponse(provider_response::ProviderResponseError),
116}
117
118crate::provider_response::impl_provider_response_helpers!(CompletionError);
119
120impl CompletionError {
121    /// Maps an SSE transport error into a completion error without flattening HTTP failures.
122    ///
123    /// Non-success HTTP responses remain [`CompletionError::HttpError`] so provider response
124    /// helpers can read status and body. Other transport failures keep the existing
125    /// [`CompletionError::ProviderError`] display string behavior.
126    pub(crate) fn from_stream_transport(error: http_client::Error) -> Self {
127        if error.non_success_status().is_some() {
128            Self::HttpError(error)
129        } else {
130            Self::ProviderError(error.to_string())
131        }
132    }
133}
134
135#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
136pub struct Document {
137    /// Stable document identifier included in the serialized context block.
138    pub id: String,
139    /// Text content passed to the model as retrieval or static context.
140    pub text: String,
141    /// Additional string metadata rendered before the document text.
142    #[serde(flatten)]
143    pub additional_props: HashMap<String, String>,
144}
145
146impl std::fmt::Display for Document {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        write!(
149            f,
150            concat!("<file id: {}>\n", "{}\n", "</file>\n"),
151            self.id,
152            if self.additional_props.is_empty() {
153                self.text.clone()
154            } else {
155                let mut sorted_props = self.additional_props.iter().collect::<Vec<_>>();
156                sorted_props.sort_by(|a, b| a.0.cmp(b.0));
157                let metadata = sorted_props
158                    .iter()
159                    .map(|(k, v)| format!("{k}: {v:?}"))
160                    .collect::<Vec<_>>()
161                    .join(" ");
162                format!("<metadata {} />\n{}", metadata, self.text)
163            }
164        )
165    }
166}
167
168#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
169pub struct ToolDefinition {
170    /// Tool name exposed to the model. It must match the registered tool name.
171    pub name: String,
172    /// Human-readable description sent to the model.
173    pub description: String,
174    /// JSON Schema describing tool arguments.
175    pub parameters: serde_json::Value,
176}
177
178/// Provider-native tool definition.
179///
180/// Stored under `additional_params.tools` and forwarded by providers that support
181/// provider-managed tools.
182#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
183pub struct ProviderToolDefinition {
184    /// Tool type/kind name as expected by the target provider (for example `web_search`).
185    #[serde(rename = "type")]
186    pub kind: String,
187    /// Additional provider-specific configuration for this hosted tool.
188    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
189    pub config: serde_json::Map<String, serde_json::Value>,
190}
191
192impl ProviderToolDefinition {
193    /// Creates a provider-hosted tool definition by type.
194    pub fn new(kind: impl Into<String>) -> Self {
195        Self {
196            kind: kind.into(),
197            config: serde_json::Map::new(),
198        }
199    }
200
201    /// Adds a provider-specific configuration key/value.
202    pub fn with_config(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
203        self.config.insert(key.into(), value);
204        self
205    }
206}
207
208/// Why the model stopped generating, normalized across providers.
209///
210/// Providers report this under different names and vocabularies
211/// (`finish_reason`, `stop_reason`, `stopReason`, …). Each provider's response
212/// conversion maps its wire value onto these variants and preserves anything
213/// unmapped verbatim in [`FinishReason::Other`], so a provider adding a new
214/// terminal reason never silently reads as a natural stop. Closes #2090/#1886.
215///
216/// Provider *failure* statuses that arrive with parseable output (a Gemini
217/// Interactions `failed`/`cancelled` interaction, a Cohere `ERROR`) follow one
218/// policy: the response converts normally and the status is preserved verbatim
219/// as [`FinishReason::Other`], leaving the caller to decide whether a
220/// failure-flagged-but-parseable turn is usable. Statuses that arrive with no
221/// usable output surface as errors instead.
222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
223#[serde(rename_all = "snake_case")]
224pub enum FinishReason {
225    /// Natural end of the response.
226    Stop,
227    /// The response hit the output-token limit.
228    Length,
229    /// The model stopped to call one or more tools.
230    ToolCalls,
231    /// The provider filtered the content.
232    ContentFilter,
233    /// A provider-specific reason outside the normalized vocabulary, carried
234    /// verbatim in the provider's own wire spelling.
235    Other(String),
236}
237
238impl FinishReason {
239    /// Reconcile a provider's reported reason with what the turn actually
240    /// produced.
241    ///
242    /// Several providers report a plain `stop` on a turn that carried tool
243    /// calls (OpenAI-compatible gateways are the usual offenders). A caller
244    /// branching on [`FinishReason::ToolCalls`] to decide whether to run tools
245    /// would then miss the call entirely, so a natural stop is upgraded
246    /// whenever the turn emitted at least one tool call.
247    ///
248    /// Only [`FinishReason::Stop`] is upgraded: `Length`, `ContentFilter`, and
249    /// `Other` describe terminations that remain true regardless of the
250    /// content, and overriding them would lose information.
251    ///
252    /// This is the single place the upgrade happens. Construct normalized
253    /// responses through [`CompletionResponse::with_finish_reason`] or
254    /// [`CompletionResponse::with_optional_finish_reason`] (and, for streams,
255    /// [`crate::streaming::normalize_stream`]) so it is always applied.
256    pub fn reconcile_with_output(self, has_tool_call: bool) -> Self {
257        if has_tool_call && matches!(self, Self::Stop) {
258            Self::ToolCalls
259        } else {
260            self
261        }
262    }
263
264    /// Whether the provider cut the turn short instead of letting the model
265    /// finish.
266    ///
267    /// A turn that ended this way can legitimately carry *no content at all* —
268    /// an output-token cap consumed entirely by hidden reasoning tokens, or a
269    /// filter that removed everything the model produced — and the reason is
270    /// then the only diagnostic the caller has. Normalization keeps such an
271    /// empty turn rather than rejecting it as a malformed response, so a
272    /// caller can tell "you hit the cap" from "the provider misbehaved".
273    ///
274    /// [`Stop`](Self::Stop) and [`ToolCalls`](Self::ToolCalls) describe turns
275    /// that ran to completion, so an empty one really is a provider defect;
276    /// [`Other`](Self::Other) is unclassified and gets the strict treatment —
277    /// it carries a provider's own wire spelling with no normalized meaning.
278    ///
279    /// This is also the set rig-agent has a remedy for when a turn arrives
280    /// without an answer ("raise `max_tokens`" / "the provider filtered the
281    /// response"), and that is the same question: the reasons a provider may
282    /// hand back an answerless turn are the reasons there is something useful
283    /// to say about it. Both sides read this predicate so they cannot drift.
284    pub fn truncated_output(&self) -> bool {
285        matches!(self, Self::Length | Self::ContentFilter)
286    }
287}
288
289/// General completion response struct: the completion choice plus normalized
290/// response metadata. The completion choice contains one or more assistant
291/// content items.
292///
293/// This type is concrete — it carries no provider-typed payload. Callers who
294/// hold a concrete model and need a provider's own wire response *typed* call
295/// that model's inherent `raw_completion` method, which performs the same
296/// request and returns the provider's native type. Callers who do not hold the
297/// concrete model — an agent erases it at construction — read the same value,
298/// serialized, from [`CompletionResponse::raw`], which every provider seam
299/// populates.
300#[derive(Debug, Clone, Serialize, Deserialize)]
301#[serde(from = "CompletionResponseRepr")]
302pub struct CompletionResponse {
303    /// The completion choice (represented by one or more assistant message content)
304    /// returned by the completion model provider
305    pub choice: Vec<AssistantContent>,
306    /// Tokens used during prompting and responding
307    pub usage: Usage,
308    /// The identifier the provider assigned to the *assistant message* itself,
309    /// when it issued one — an OpenAI Responses output-message `msg_` ID or an
310    /// Anthropic `msg_` ID. Only IDs the provider would recognize on a replayed
311    /// assistant message belong here; identifiers that name the whole response
312    /// (an OpenAI chat `chatcmpl-` ID, a Gemini `responseId`) go in
313    /// [`CompletionResponse::response_id`] instead.
314    ///
315    /// The Responses API path uses it to pair reasoning input items with their
316    /// output items across turns, and it is what agent history promotes into
317    /// [`Message::Assistant`]'s `id`.
318    #[serde(default)]
319    pub message_id: Option<String>,
320    /// The identifier the provider assigned to the response as a whole, when it
321    /// reported one — an OpenAI chat `chatcmpl-` ID, a Gemini `responseId`, a
322    /// Cohere generation ID. Response-scoped: useful for logging, telemetry
323    /// (`gen_ai.response.id`), and support requests, but never replayed to a
324    /// provider as a message ID.
325    #[serde(default)]
326    pub response_id: Option<String>,
327    /// The provider's transport-level request identifier, taken from the HTTP
328    /// response headers (Anthropic `request-id`, OpenAI/xAI `x-request-id`) or
329    /// the provider SDK's response metadata (Bedrock) — the id provider
330    /// support asks for when investigating a request. Never the body's
331    /// `message.id`/response id; those are [`Self::message_id`] and
332    /// [`Self::response_id`]. `None` means the provider did not report one —
333    /// that is a documented outcome (e.g. Gemini sends no id header), never an
334    /// error.
335    #[serde(default, skip_serializing_if = "Option::is_none")]
336    pub provider_request_id: Option<String>,
337    /// Why the model stopped generating, when the provider reported it.
338    ///
339    /// Private so that every write flows through
340    /// [`CompletionResponse::with_finish_reason`] /
341    /// [`CompletionResponse::with_optional_finish_reason`], which apply
342    /// [`FinishReason::reconcile_with_output`] — a direct assignment would
343    /// silently skip the tool-call upgrade. Read via
344    /// [`CompletionResponse::finish_reason`].
345    #[serde(default)]
346    finish_reason: Option<FinishReason>,
347    /// Stable descriptor name of the provider that produced this response, for
348    /// example `"openai"`. Always populated, including for responses derived
349    /// from a stream that ended before its terminal record.
350    pub provider: String,
351    /// Provider-reported model identifier for the response.
352    ///
353    /// This is the model named by the wire response, not the model that was
354    /// requested; it is `None` when the provider reports no identifier.
355    #[serde(default)]
356    pub model: Option<String>,
357    /// The provider's own response for this call: the value the model's
358    /// inherent `raw_completion` would have returned, serialized. It is the
359    /// response as rig's wire type parsed it — fields that type does not model
360    /// are not here. Every provider seam populates it, unconditionally — the
361    /// same parity the pre-normalization `raw_response: T` had.
362    ///
363    /// An escape hatch for provider-specific data rig does not normalize — it
364    /// never replaces a normalized field, and every normalized field means the
365    /// same thing whatever this holds. `Value::Null` means the value was built
366    /// without a provider behind it — [`CompletionResponse::new`] without
367    /// `with_raw` (test doubles, hand-built responses), or a response
368    /// persisted before the field existed — never that the provider sent
369    /// nothing: no provider seam produces `Null`.
370    ///
371    /// Typed access is recoverable: provider raw types are `Deserialize`, so
372    /// `provider::CompletionResponse::deserialize(&raw)` returns the
373    /// provider's own type, and [`NormalizeCompletionResponse`] converts
374    /// forward.
375    #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
376    pub raw: serde_json::Value,
377}
378
379/// Response identity metadata for one completed model call: which provider
380/// objects this exact attempt produced. The three axes stay distinct —
381/// message-scoped, response-scoped, and transport — and every field is `None`
382/// when the provider did not report it: a documented outcome, never an error.
383#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
384pub struct ResponseIdentity {
385    /// Provider-assigned *assistant message* ID (e.g. an Anthropic or OpenAI
386    /// Responses `msg_…`) — an ID the provider would recognize on a replayed
387    /// assistant message.
388    #[serde(default, skip_serializing_if = "Option::is_none")]
389    pub message_id: Option<String>,
390    /// Provider-assigned *response-scoped* ID (e.g. an OpenAI `chatcmpl-` or
391    /// `resp_…` ID) — names the whole response, never replayed as a message
392    /// ID.
393    #[serde(default, skip_serializing_if = "Option::is_none")]
394    pub response_id: Option<String>,
395    /// The provider's *transport* request id (HTTP response header such as
396    /// Anthropic `request-id`, or provider SDK response metadata) — the id
397    /// provider support asks for. Never the body's message/response id.
398    #[serde(default, skip_serializing_if = "Option::is_none")]
399    pub provider_request_id: Option<String>,
400}
401
402impl CompletionResponse {
403    /// Create a response from its required parts; optional metadata starts
404    /// unset and is filled in with the `with_*` helpers.
405    pub fn new(choice: Vec<AssistantContent>, usage: Usage, provider: impl Into<String>) -> Self {
406        Self {
407            choice,
408            usage,
409            message_id: None,
410            response_id: None,
411            provider_request_id: None,
412            finish_reason: None,
413            provider: provider.into(),
414            model: None,
415            raw: serde_json::Value::Null,
416        }
417    }
418
419    /// Why the model stopped generating, when the provider reported it.
420    pub fn finish_reason(&self) -> Option<FinishReason> {
421        self.finish_reason.clone()
422    }
423
424    /// This response's identity metadata as one [`ResponseIdentity`] carrier.
425    pub fn identity(&self) -> ResponseIdentity {
426        ResponseIdentity {
427            message_id: self.message_id.clone(),
428            response_id: self.response_id.clone(),
429            provider_request_id: self.provider_request_id.clone(),
430        }
431    }
432
433    /// Attach the normalized finish reason, reconciled against the choice via
434    /// [`FinishReason::reconcile_with_output`].
435    pub fn with_finish_reason(self, finish_reason: FinishReason) -> Self {
436        self.with_optional_finish_reason(Some(finish_reason))
437    }
438
439    /// Attach the normalized finish reason when the provider reported one.
440    ///
441    /// This is the `Option` form of [`CompletionResponse::with_finish_reason`]
442    /// and applies the same reconciliation. Provider conversions that hold an
443    /// `Option<FinishReason>` use this rather than assigning the field, so the
444    /// tool-call upgrade is never skipped.
445    pub fn with_optional_finish_reason(mut self, finish_reason: Option<FinishReason>) -> Self {
446        let has_tool_call = self
447            .choice
448            .iter()
449            .any(|content| matches!(content, AssistantContent::ToolCall(_)));
450        self.finish_reason =
451            finish_reason.map(|reason| reason.reconcile_with_output(has_tool_call));
452        self
453    }
454}
455
456crate::provider_response::response_metadata_setters!(CompletionResponse);
457
458/// Wire-shape mirror of [`CompletionResponse`], used only for deserialization.
459///
460/// Serde must never construct an invariant-bearing value structurally: a plain
461/// derive would let `"finish_reason":"stop"` skip
462/// [`FinishReason::reconcile_with_output`] and `"message_id":""` skip the
463/// empty-string filtering. This mirror deserializes the exact wire shape and
464/// [`From`] funnels it through [`CompletionResponse::new`] and the `with_*`
465/// setters, so every deserialized value satisfies the same invariants as a
466/// constructed one. Serialization stays derived on [`CompletionResponse`]
467/// itself, so the wire format is unchanged.
468#[derive(Deserialize)]
469struct CompletionResponseRepr {
470    choice: Vec<AssistantContent>,
471    usage: Usage,
472    #[serde(default)]
473    message_id: Option<String>,
474    #[serde(default)]
475    response_id: Option<String>,
476    #[serde(default)]
477    provider_request_id: Option<String>,
478    #[serde(default)]
479    finish_reason: Option<FinishReason>,
480    provider: String,
481    #[serde(default)]
482    model: Option<String>,
483    // `default` because persisted responses predate the field; a missing key
484    // loads as `Null`, which is exactly what "no provider response behind this
485    // value" means.
486    #[serde(default)]
487    raw: serde_json::Value,
488}
489
490impl From<CompletionResponseRepr> for CompletionResponse {
491    fn from(repr: CompletionResponseRepr) -> Self {
492        let CompletionResponseRepr {
493            choice,
494            usage,
495            message_id,
496            response_id,
497            provider_request_id,
498            finish_reason,
499            provider,
500            model,
501            raw,
502        } = repr;
503        Self::new(choice, usage, provider)
504            .with_optional_message_id(message_id)
505            .with_optional_response_id(response_id)
506            .with_optional_provider_request_id(provider_request_id)
507            .with_optional_finish_reason(finish_reason)
508            .with_optional_model(model)
509            .with_raw(raw)
510    }
511}
512
513/// Convert a provider's own completion payload into the normalized
514/// [`CompletionResponse`].
515///
516/// The provider descriptor name is an *input* rather than something the
517/// conversion knows, because several providers share one wire shape — the
518/// OpenAI chat-completions payload is used by more than a dozen of them. A
519/// conversion that hardcoded a name would mislabel every provider but one, and
520/// a placeholder overwritten by the caller would be correct only by convention.
521///
522/// This is a trait rather than `TryFrom<(&str, T)>` for a concrete reason:
523/// a tuple is not a local type, so `impl TryFrom<(&str, TheirResponse)> for
524/// CompletionResponse` is rejected by the orphan rule in any crate other than
525/// `rig-core`. Implementing this trait on a provider's own response type is
526/// allowed anywhere, which keeps provider extensions implementable outside this
527/// crate.
528pub trait NormalizeCompletionResponse {
529    /// Normalize this payload, attributing it to `provider`.
530    fn normalize(self, provider: &str) -> Result<CompletionResponse, CompletionError>;
531}
532
533/// Struct representing the token usage for a completion request.
534/// If tokens used are `0`, then the provider failed to supply token usage metrics.
535#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
536pub struct Usage {
537    /// The number of input ("prompt") tokens used in a given request.
538    pub input_tokens: u64,
539    /// The number of output ("completion") tokens used in a given request.
540    pub output_tokens: u64,
541    /// We store this separately as some providers may only report one number
542    pub total_tokens: u64,
543    /// The number of input tokens read from a provider-managed cache
544    pub cached_input_tokens: u64,
545    /// The number of input tokens written to a provider-managed cache
546    pub cache_creation_input_tokens: u64,
547    /// The number of tool-use prompt tokens used in a given request.
548    #[serde(default)]
549    pub tool_use_prompt_tokens: u64,
550    /// The number of tokens spent on internal reasoning / "thoughts" by reasoning-capable
551    /// models (e.g. Gemini thinking, Anthropic extended thinking, OpenAI o-series).
552    pub reasoning_tokens: u64,
553}
554
555impl Usage {
556    /// Creates a new instance of `Usage`.
557    pub fn new() -> Self {
558        Self {
559            input_tokens: 0,
560            output_tokens: 0,
561            total_tokens: 0,
562            cached_input_tokens: 0,
563            cache_creation_input_tokens: 0,
564            tool_use_prompt_tokens: 0,
565            reasoning_tokens: 0,
566        }
567    }
568
569    /// Whether any usage values are set and non-zero.
570    ///
571    /// Zero-valued usage is this type's documented sentinel for "the provider
572    /// supplied no usage metrics", so `false` means usage was not reported.
573    pub fn has_values(&self) -> bool {
574        *self != Self::new()
575    }
576}
577
578impl Default for Usage {
579    fn default() -> Self {
580        Self::new()
581    }
582}
583
584impl Add for Usage {
585    type Output = Self;
586
587    fn add(mut self, other: Self) -> Self::Output {
588        self += other;
589        self
590    }
591}
592
593impl AddAssign for Usage {
594    fn add_assign(&mut self, other: Self) {
595        self.input_tokens += other.input_tokens;
596        self.output_tokens += other.output_tokens;
597        self.total_tokens += other.total_tokens;
598        self.cached_input_tokens += other.cached_input_tokens;
599        self.cache_creation_input_tokens += other.cache_creation_input_tokens;
600        self.tool_use_prompt_tokens += other.tool_use_prompt_tokens;
601        self.reasoning_tokens += other.reasoning_tokens;
602    }
603}
604
605/// Provider behavior that affects how runtimes prepare completion requests.
606///
607/// Capabilities are immutable facts about a model implementation rather than
608/// per-request state, so a runtime can snapshot this value when it erases a
609/// concrete model instead of retaining a callback into the provider.
610///
611/// Prefer building from [`ProviderCapabilities::new`] or [`Default`] and
612/// enabling flags with the `with_*` methods: that form keeps external
613/// implementations compiling when new capabilities are added, where a struct
614/// literal does not.
615#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
616pub struct ProviderCapabilities {
617    /// Whether this provider's native structured output (`output_schema` ->
618    /// `format`/`response_format`) composes with tool calls in the same
619    /// multi-turn request without suppressing them.
620    ///
621    /// `false` is the safe assumption: the native constraint may make the model
622    /// emit schema JSON instead of calling its tools — see issue #1928.
623    /// Providers that enforce structured output *and* tool use together (e.g.
624    /// OpenAI, Anthropic) set this to `true`, which lets runtimes keep
625    /// guaranteed native structured output active when tools are present.
626    pub composes_native_output_with_tools: bool,
627}
628
629impl ProviderCapabilities {
630    /// Create the conservative capability set used by default.
631    pub const fn new() -> Self {
632        Self {
633            composes_native_output_with_tools: false,
634        }
635    }
636
637    /// Declare whether native structured output composes with tool calls.
638    pub const fn with_native_output_tool_composition(mut self, supported: bool) -> Self {
639        self.composes_native_output_with_tools = supported;
640        self
641    }
642}
643
644/// Trait defining a completion model that can be used to generate completion responses.
645/// This trait is meant to be implemented by the user to define a custom completion model,
646/// either from a third party provider (e.g.: OpenAI) or a local model.
647///
648/// Implementations return Rig's normalized [`CompletionResponse`] and
649/// [`StreamingCompletionResponse`]; a provider's own wire types stay on the
650/// provider's side of this boundary, reachable through its inherent
651/// `raw_completion`/`raw_stream` methods. Model construction belongs to
652/// [`crate::client::completion::CompletionClient`], not to this trait.
653///
654/// The trait demands only async service behavior — no `Clone` supertrait, in
655/// the spirit of `tower::Service`: cloning or sharing a model is the caller's
656/// concern (wrap it in an `Arc` if needed). The [`Self::completion_request`]
657/// convenience gates on `Self: Clone` individually, which every built-in
658/// provider model satisfies.
659pub trait CompletionModel: WasmCompatSend + WasmCompatSync {
660    /// Generates a completion response for the given completion request.
661    fn completion(
662        &self,
663        request: CompletionRequest,
664    ) -> impl std::future::Future<Output = Result<CompletionResponse, CompletionError>> + WasmCompatSend;
665
666    /// Streams a completion response for the given completion request.
667    fn stream(
668        &self,
669        request: CompletionRequest,
670    ) -> impl std::future::Future<Output = Result<StreamingCompletionResponse, CompletionError>>
671    + WasmCompatSend;
672
673    /// Generates a completion request builder for the given `prompt`.
674    fn completion_request(&self, prompt: impl Into<Message>) -> CompletionRequestBuilder<Self>
675    where
676        Self: Sized + Clone,
677    {
678        CompletionRequestBuilder::new(self.clone(), prompt)
679    }
680
681    /// Provider behavior a runtime should account for when preparing requests.
682    ///
683    /// The default is conservative — see [`ProviderCapabilities`]. Override
684    /// this to declare the capabilities a provider actually supports.
685    fn capabilities(&self) -> ProviderCapabilities {
686        ProviderCapabilities::default()
687    }
688}
689
690/// A shared model is a model: `Arc<M>` forwards every method to `M`, so the
691/// "wrap it in an `Arc` if needed" guidance holds through the generic APIs
692/// (`CompletionRequestBuilder`, agent construction), not just at direct call
693/// sites. `Arc<M>: Clone` always holds, so [`CompletionModel::completion_request`]
694/// clones the `Arc` — never the model.
695impl<M: CompletionModel + ?Sized> CompletionModel for std::sync::Arc<M> {
696    fn completion(
697        &self,
698        request: CompletionRequest,
699    ) -> impl std::future::Future<Output = Result<CompletionResponse, CompletionError>> + WasmCompatSend
700    {
701        (**self).completion(request)
702    }
703
704    fn stream(
705        &self,
706        request: CompletionRequest,
707    ) -> impl std::future::Future<Output = Result<StreamingCompletionResponse, CompletionError>>
708    + WasmCompatSend {
709        (**self).stream(request)
710    }
711
712    fn capabilities(&self) -> ProviderCapabilities {
713        (**self).capabilities()
714    }
715}
716
717/// Struct representing a general completion request that can be sent to a completion model provider.
718#[derive(Debug, Clone, Serialize, Deserialize)]
719pub struct CompletionRequest {
720    /// Optional model override for this request.
721    pub model: Option<String>,
722    /// Legacy preamble field preserved for backwards compatibility.
723    ///
724    /// New code should prefer a leading [`Message::System`]
725    /// in `chat_history` as the canonical representation of system instructions.
726    pub preamble: Option<String>,
727    /// The chat history to be sent to the completion model provider.
728    /// The very last message is the prompt.
729    ///
730    /// This used to be a non-empty container, so "there is always at least one"
731    /// was a type guarantee. It is a `Vec` now and the field is public, so the
732    /// guarantee is a *rule* instead: it is checked by
733    /// [`CompletionRequest::validate_message_content`] at the request boundary.
734    pub chat_history: Vec<Message>,
735    /// The documents to be sent to the completion model provider
736    pub documents: Vec<Document>,
737    /// The tools to be sent to the completion model provider
738    pub tools: Vec<ToolDefinition>,
739    /// The temperature to be sent to the completion model provider
740    pub temperature: Option<f64>,
741    /// The max tokens to be sent to the completion model provider
742    pub max_tokens: Option<u64>,
743    /// Whether tools are required to be used by the model provider or not before providing a response.
744    pub tool_choice: Option<ToolChoice>,
745    /// Additional provider-specific parameters to be sent to the completion model provider
746    pub additional_params: Option<serde_json::Value>,
747    /// Optional JSON Schema for structured output. When set, providers that support
748    /// native structured outputs will constrain the model's response to match this schema.
749    pub output_schema: Option<schemars::Schema>,
750    /// Whether to record sensitive request, response, and tool content on GenAI
751    /// telemetry spans.
752    ///
753    /// Defaults to `false`. Enabling this can expose prompts, retrieved context,
754    /// tool results, model responses, and other sensitive or high-cardinality data
755    /// through OpenTelemetry span attributes, which can increase observability
756    /// backend storage and query costs. Only enable it when the caller has
757    /// explicitly opted in to content telemetry.
758    ///
759    /// Higher-level agent drivers use this flag for portable input, output, and
760    /// tool-content telemetry. Direct provider calls only forward the policy;
761    /// the exact content fields available there are provider- and
762    /// surface-dependent, especially for streaming responses that are consumed
763    /// after the provider returns.
764    ///
765    /// This is local observability policy and is never serialized into provider
766    /// request payloads.
767    #[serde(skip)]
768    pub record_telemetry_content: bool,
769}
770
771impl CompletionRequest {
772    /// Reject a request with no messages, or a message that carries no content.
773    ///
774    /// Removing the non-empty container removed two guarantees at once, and this
775    /// is where both are restated:
776    ///
777    /// - `chat_history` was non-empty by construction. As a `Vec` it is not, and
778    ///   the field is public, so `CompletionRequest { chat_history: vec![], .. }`
779    ///   is constructible and would reach a provider as `messages: []` — a remote
780    ///   400 in place of a local error that names the problem.
781    /// - Message content was likewise non-empty by construction, and every wire
782    ///   rejects an empty content block.
783    ///
784    /// The rule also covers the block list *inside* a tool result. A user
785    /// message carrying one `UserContent::ToolResult` is itself non-empty, but
786    /// `ToolResult::content` was non-empty by construction under the removed
787    /// container and is request-direction data just like the message content
788    /// around it — so its check is relocated here rather than dropped. Only a
789    /// tool result with *zero* blocks is rejected; a tool that legitimately
790    /// returned an empty string produces one block and still sends.
791    ///
792    /// This is the **request** direction only, and the asymmetry is deliberate.
793    /// Empty *assistant* content is a real provider outcome on the response path
794    /// — a tool-call-only turn, a content-filtered turn, a truncated stream — and
795    /// the agent layer drops such a turn rather than sending it, so it never
796    /// reaches here. The response direction is guarded per-wire instead, by
797    /// [`crate::message::require_non_empty`], because "this provider returned
798    /// nothing where its protocol promises content" is a judgement only the
799    /// provider's own conversion can make.
800    ///
801    /// `System` content is deliberately not checked. It is a `String` and always
802    /// has been, so the removed container never constrained it; rejecting an
803    /// empty one would be a new restriction rather than a relocated enforcement
804    /// point, and would break a history carrying a conditionally built preamble
805    /// that resolved to `""`.
806    ///
807    /// **Where this runs.** [`CompletionRequestBuilder::send`] and
808    /// [`CompletionRequestBuilder::stream`] call it, which covers both agent
809    /// surfaces too — the blocking and streaming turn drivers both issue their
810    /// request through the builder. Handing a request straight to a
811    /// [`CompletionModel`] bypasses it; call this yourself there.
812    pub fn validate_message_content(&self) -> Result<(), CompletionError> {
813        if self.chat_history.is_empty() {
814            return Err(CompletionError::RequestError(
815                "request has an empty chat history; providers require at least one message"
816                    .to_owned()
817                    .into(),
818            ));
819        }
820
821        let empty_message = |role: &str, index: usize| {
822            CompletionError::RequestError(
823                format!(
824                    "{role} message at index {index} has no content; \
825                     providers reject empty content blocks"
826                )
827                .into(),
828            )
829        };
830
831        // One match per message, with every per-variant rule in that
832        // variant's arm, so extending validation means extending one arm.
833        for (index, message) in self.chat_history.iter().enumerate() {
834            match message {
835                Message::System { .. } => {}
836                Message::Assistant { content, .. } => {
837                    if content.is_empty() {
838                        return Err(empty_message("assistant", index));
839                    }
840                }
841                Message::User { content } => {
842                    if content.is_empty() {
843                        return Err(empty_message("user", index));
844                    }
845
846                    for (position, item) in content.iter().enumerate() {
847                        // Exhaustive on purpose: a future variant that carries
848                        // its own request-direction block list must decide here
849                        // whether its emptiness is checked, instead of slipping
850                        // past a wildcard un-validated.
851                        match item {
852                            UserContent::ToolResult(result) if result.content.is_empty() => {
853                                let name = &result.name;
854                                return Err(CompletionError::RequestError(
855                                    format!(
856                                        "tool result for `{name}` at index {position} of the \
857                                         user message at index {index} has no content; \
858                                         providers reject empty content blocks"
859                                    )
860                                    .into(),
861                                ));
862                            }
863                            UserContent::ToolResult(_)
864                            | UserContent::Text(_)
865                            | UserContent::Image(_)
866                            | UserContent::Audio(_)
867                            | UserContent::Video(_)
868                            | UserContent::Document(_) => {}
869                        }
870                    }
871                }
872            }
873        }
874
875        Ok(())
876    }
877
878    /// Extracts a name from the output schema's `"title"` field, falling back to `"response_schema"`.
879    /// Useful for providers that require a name alongside the JSON Schema (e.g., OpenAI).
880    pub fn output_schema_name(&self) -> Option<String> {
881        self.output_schema.as_ref().map(|schema| {
882            schema
883                .as_object()
884                .and_then(|o| o.get("title"))
885                .and_then(|v| v.as_str())
886                .unwrap_or("response_schema")
887                .to_string()
888        })
889    }
890
891    /// Returns documents normalized into a message (if any).
892    /// Most providers do not accept documents directly as input, so it needs to convert into a
893    /// `Message` so that it can be incorporated into `chat_history`.
894    pub fn normalized_documents(&self) -> Option<Message> {
895        Self::normalized_documents_from(&self.documents)
896    }
897
898    fn normalized_documents_from(documents: &[Document]) -> Option<Message> {
899        if documents.is_empty() {
900            return None;
901        }
902
903        // Most providers will convert documents into a text unless it can handle document messages.
904        // We use `UserContent::document` for those who handle it directly!
905        let messages = documents
906            .iter()
907            .map(|doc| {
908                UserContent::document(
909                    doc.to_string(),
910                    // In the future, we can customize `Document` to pass these extra types through.
911                    // Most providers ditch these but they might want to use them.
912                    Some(DocumentMediaType::TXT),
913                )
914            })
915            .collect::<Vec<_>>();
916
917        crate::message::non_empty(messages).map(|content| Message::User { content })
918    }
919
920    pub(crate) fn chat_history_with_documents(&self) -> Vec<Message> {
921        let mut chat_history = self.chat_history.clone();
922        if let Some(documents) = self.normalized_documents() {
923            insert_after_leading_system(&mut chat_history, documents);
924        }
925        chat_history
926    }
927}
928
929/// Insert `message` at the first non-system position so document context lands
930/// after any leading system messages; telemetry and the sent request must
931/// agree on this placement.
932fn insert_after_leading_system(chat_history: &mut Vec<Message>, message: Message) {
933    let insert_at = chat_history
934        .iter()
935        .position(|message| !matches!(message, Message::System { .. }))
936        .unwrap_or(chat_history.len());
937    chat_history.insert(insert_at, message);
938}
939
940fn merge_provider_tools_into_additional_params(
941    additional_params: Option<serde_json::Value>,
942    provider_tools: Vec<ProviderToolDefinition>,
943) -> Option<serde_json::Value> {
944    if provider_tools.is_empty() {
945        return additional_params;
946    }
947
948    let mut provider_tools_json = provider_tools
949        .into_iter()
950        .map(|ProviderToolDefinition { kind, mut config }| {
951            // Force the provider tool type from the strongly-typed field.
952            config.insert("type".to_string(), serde_json::Value::String(kind));
953            serde_json::Value::Object(config)
954        })
955        .collect::<Vec<_>>();
956
957    let mut params_map = match additional_params {
958        Some(serde_json::Value::Object(map)) => map,
959        Some(serde_json::Value::Bool(stream)) => {
960            let mut map = serde_json::Map::new();
961            map.insert("stream".to_string(), serde_json::Value::Bool(stream));
962            map
963        }
964        _ => serde_json::Map::new(),
965    };
966
967    let mut merged_tools = match params_map.remove("tools") {
968        Some(serde_json::Value::Array(existing)) => existing,
969        _ => Vec::new(),
970    };
971    merged_tools.append(&mut provider_tools_json);
972    params_map.insert("tools".to_string(), serde_json::Value::Array(merged_tools));
973    Some(serde_json::Value::Object(params_map))
974}
975
976/// Builder struct for constructing a completion request.
977///
978/// Example usage:
979/// ```no_run
980/// use rig_core::{
981///     client::CompletionClient,
982///     providers::openai::{Client, self},
983///     completion::{CompletionModel, CompletionRequestBuilder},
984/// };
985///
986/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
987/// let openai = Client::new("your-openai-api-key")?;
988/// let model = openai.completion_model(openai::GPT_5_2);
989///
990/// // Create the completion request and execute it separately
991/// let request = CompletionRequestBuilder::new(model.clone(), "Who are you?".to_string())
992///     .preamble("You are Marvin from the Hitchhiker's Guide to the Galaxy.".to_string())
993///     .temperature(0.5)
994///     .build();
995///
996/// let response = model.completion(request).await?;
997/// # Ok(())
998/// # }
999/// ```
1000///
1001/// Alternatively, you can execute the completion request directly from the builder:
1002/// ```no_run
1003/// use rig_core::{
1004///     client::CompletionClient,
1005///     providers::openai::{Client, self},
1006///     completion::CompletionRequestBuilder,
1007/// };
1008///
1009/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
1010/// let openai = Client::new("your-openai-api-key")?;
1011/// let model = openai.completion_model(openai::GPT_5_2);
1012///
1013/// // Create the completion request and execute it directly
1014/// let response = CompletionRequestBuilder::new(model, "Who are you?".to_string())
1015///     .preamble("You are Marvin from the Hitchhiker's Guide to the Galaxy.".to_string())
1016///     .temperature(0.5)
1017///     .send()
1018///     .await?;
1019/// # Ok(())
1020/// # }
1021/// ```
1022///
1023/// Note: It is usually unnecessary to create a completion request builder directly.
1024/// Instead, use the [CompletionModel::completion_request] method.
1025pub struct CompletionRequestBuilder<M: CompletionModel> {
1026    model: M,
1027    prompt: Message,
1028    request_model: Option<String>,
1029    preamble: Option<String>,
1030    chat_history: Vec<Message>,
1031    documents: Vec<Document>,
1032    tools: Vec<ToolDefinition>,
1033    provider_tools: Vec<ProviderToolDefinition>,
1034    temperature: Option<f64>,
1035    max_tokens: Option<u64>,
1036    tool_choice: Option<ToolChoice>,
1037    additional_params: Option<serde_json::Value>,
1038    output_schema: Option<schemars::Schema>,
1039    record_telemetry_content: bool,
1040}
1041
1042impl<M: CompletionModel> CompletionRequestBuilder<M> {
1043    pub fn new(model: M, prompt: impl Into<Message>) -> Self {
1044        Self {
1045            model,
1046            prompt: prompt.into(),
1047            request_model: None,
1048            preamble: None,
1049            chat_history: Vec::new(),
1050            documents: Vec::new(),
1051            tools: Vec::new(),
1052            provider_tools: Vec::new(),
1053            temperature: None,
1054            max_tokens: None,
1055            tool_choice: None,
1056            additional_params: None,
1057            output_schema: None,
1058            record_telemetry_content: false,
1059        }
1060    }
1061
1062    /// Sets the preamble for the completion request.
1063    pub fn preamble(mut self, preamble: String) -> Self {
1064        // Legacy public API: funnel preamble into canonical system messages at build-time.
1065        self.preamble = Some(preamble);
1066        self
1067    }
1068
1069    /// Overrides the model used for this request.
1070    pub fn model(mut self, model: impl Into<String>) -> Self {
1071        self.request_model = Some(model.into());
1072        self
1073    }
1074
1075    /// Overrides the model used for this request.
1076    pub fn model_opt(mut self, model: Option<String>) -> Self {
1077        self.request_model = model;
1078        self
1079    }
1080
1081    pub fn without_preamble(mut self) -> Self {
1082        self.preamble = None;
1083        self
1084    }
1085
1086    /// Adds a message to the chat history for the completion request.
1087    pub fn message(mut self, message: Message) -> Self {
1088        self.chat_history.push(message);
1089
1090        self
1091    }
1092
1093    /// Adds a list of messages to the chat history for the completion request.
1094    pub fn messages(mut self, messages: impl IntoIterator<Item = Message>) -> Self {
1095        self.chat_history.extend(messages);
1096
1097        self
1098    }
1099
1100    /// Adds a document to the completion request.
1101    pub fn document(mut self, document: Document) -> Self {
1102        self.documents.push(document);
1103        self
1104    }
1105
1106    /// Adds a list of documents to the completion request.
1107    pub fn documents(self, documents: impl IntoIterator<Item = Document>) -> Self {
1108        documents
1109            .into_iter()
1110            .fold(self, |builder, doc| builder.document(doc))
1111    }
1112
1113    /// Adds a tool to the completion request.
1114    pub fn tool(mut self, tool: ToolDefinition) -> Self {
1115        self.tools.push(tool);
1116        self
1117    }
1118
1119    /// Adds a list of tools to the completion request.
1120    pub fn tools(self, tools: Vec<ToolDefinition>) -> Self {
1121        tools
1122            .into_iter()
1123            .fold(self, |builder, tool| builder.tool(tool))
1124    }
1125
1126    /// Adds a provider-hosted tool to the completion request.
1127    pub fn provider_tool(mut self, tool: ProviderToolDefinition) -> Self {
1128        self.provider_tools.push(tool);
1129        self
1130    }
1131
1132    /// Adds provider-hosted tools to the completion request.
1133    pub fn provider_tools(self, tools: Vec<ProviderToolDefinition>) -> Self {
1134        tools
1135            .into_iter()
1136            .fold(self, |builder, tool| builder.provider_tool(tool))
1137    }
1138
1139    /// Adds additional parameters to the completion request.
1140    /// This can be used to set additional provider-specific parameters. For example,
1141    /// Cohere's completion models accept a `connectors` parameter that can be used to
1142    /// specify the data connectors used by Cohere when executing the completion
1143    /// (see `examples/cohere_connectors.rs`).
1144    pub fn additional_params(mut self, additional_params: serde_json::Value) -> Self {
1145        match self.additional_params {
1146            Some(params) => {
1147                self.additional_params = Some(json_utils::merge(params, additional_params));
1148            }
1149            None => {
1150                self.additional_params = Some(additional_params);
1151            }
1152        }
1153        self
1154    }
1155
1156    /// Sets the additional parameters for the completion request.
1157    /// This can be used to set additional provider-specific parameters. For example,
1158    /// Cohere's completion models accept a `connectors` parameter that can be used to
1159    /// specify the data connectors used by Cohere when executing the completion
1160    /// (see `examples/cohere_connectors.rs`).
1161    pub fn additional_params_opt(mut self, additional_params: Option<serde_json::Value>) -> Self {
1162        self.additional_params = additional_params;
1163        self
1164    }
1165
1166    /// Sets the temperature for the completion request.
1167    pub fn temperature(mut self, temperature: f64) -> Self {
1168        self.temperature = Some(temperature);
1169        self
1170    }
1171
1172    /// Sets the temperature for the completion request.
1173    pub fn temperature_opt(mut self, temperature: Option<f64>) -> Self {
1174        self.temperature = temperature;
1175        self
1176    }
1177
1178    /// Sets the max tokens for the completion request.
1179    /// Note: This is required if using Anthropic
1180    pub fn max_tokens(mut self, max_tokens: u64) -> Self {
1181        self.max_tokens = Some(max_tokens);
1182        self
1183    }
1184
1185    /// Sets the max tokens for the completion request.
1186    /// Note: This is required if using Anthropic
1187    pub fn max_tokens_opt(mut self, max_tokens: Option<u64>) -> Self {
1188        self.max_tokens = max_tokens;
1189        self
1190    }
1191
1192    /// Sets the thing.
1193    pub fn tool_choice(mut self, tool_choice: ToolChoice) -> Self {
1194        self.tool_choice = Some(tool_choice);
1195        self
1196    }
1197
1198    /// Sets the output schema for structured output. When set, providers that support
1199    /// native structured outputs will constrain the model's response to match this schema.
1200    /// NOTE: For direct type conversion, you may want to use `Agent::prompt_typed()` - using this method
1201    /// with `Agent::prompt()` will still output a String at the end, it'll just be compatible with whatever
1202    /// type you want to use here. This method is primarily an escape hatch for agents being used as tools
1203    /// to still be able to leverage structured outputs.
1204    pub fn output_schema(mut self, schema: schemars::Schema) -> Self {
1205        self.output_schema = Some(schema);
1206        self
1207    }
1208
1209    /// Sets the output schema for structured output from an optional value.
1210    /// NOTE: For direct type conversion, you may want to use `Agent::prompt_typed()` - using this method
1211    /// with `Agent::prompt()` will still output a String at the end, it'll just be compatible with whatever
1212    /// type you want to use here. This method is primarily an escape hatch for agents being used as tools
1213    /// to still be able to leverage structured outputs.
1214    pub fn output_schema_opt(mut self, schema: Option<schemars::Schema>) -> Self {
1215        self.output_schema = schema;
1216        self
1217    }
1218
1219    /// Opt in or out of recording sensitive request, response, and tool content
1220    /// on GenAI telemetry spans for this request.
1221    ///
1222    /// Defaults to `false`. Enabling this can expose prompts, retrieved context,
1223    /// tool results, model responses, and other sensitive or high-cardinality data
1224    /// through OpenTelemetry span attributes, which can increase observability
1225    /// backend storage and query costs. Only enable it when content telemetry is
1226    /// acceptable for this request. Structural metadata and token
1227    /// usage remain available when this is disabled.
1228    ///
1229    /// This low-level builder only stores the opt-in on the built request. It
1230    /// does not guarantee portable input/output message fields for direct model
1231    /// calls; exact coverage is provider- and surface-dependent. Agent APIs own
1232    /// normalized input/output recording and provide the consistent surface.
1233    pub fn record_content_telemetry(mut self, enabled: bool) -> Self {
1234        self.record_telemetry_content = enabled;
1235        self
1236    }
1237
1238    /// Returns the normalized input messages used by runtime telemetry.
1239    pub fn messages_for_telemetry(&self) -> Vec<Message> {
1240        let mut chat_history = self.chat_history.clone();
1241        if let Some(preamble) = &self.preamble {
1242            chat_history.insert(0, Message::system(preamble.clone()));
1243        }
1244        chat_history.push(self.prompt.clone());
1245
1246        if let Some(documents) = CompletionRequest::normalized_documents_from(&self.documents) {
1247            insert_after_leading_system(&mut chat_history, documents);
1248        }
1249
1250        chat_history
1251    }
1252
1253    /// Builds the completion request.
1254    pub fn build(self) -> CompletionRequest {
1255        self.into_model_and_request().1
1256    }
1257
1258    /// Moves the model out and builds the request from the remaining fields.
1259    ///
1260    /// `build`, `send`, and `stream` all funnel through this single
1261    /// destructuring, so the built request cannot drift between them and the
1262    /// terminal methods need no model clone.
1263    fn into_model_and_request(self) -> (M, CompletionRequest) {
1264        let model = self.model;
1265        // Build the final message list, prepending preamble if present
1266        let mut chat_history = self.chat_history;
1267        let prompt = self.prompt;
1268        if let Some(preamble) = self.preamble {
1269            chat_history.insert(0, Message::system(preamble));
1270        }
1271
1272        // The push is what makes the history non-empty, so the fallback that
1273        // used to follow could never be taken — and it forced a clone of the
1274        // prompt to feed it.
1275        chat_history.push(prompt);
1276        let additional_params = merge_provider_tools_into_additional_params(
1277            self.additional_params,
1278            self.provider_tools,
1279        );
1280
1281        let request = CompletionRequest {
1282            model: self.request_model,
1283            preamble: None,
1284            chat_history,
1285            documents: self.documents,
1286            tools: self.tools,
1287            temperature: self.temperature,
1288            max_tokens: self.max_tokens,
1289            tool_choice: self.tool_choice,
1290            additional_params,
1291            output_schema: self.output_schema,
1292            record_telemetry_content: self.record_telemetry_content,
1293        };
1294        (model, request)
1295    }
1296
1297    /// Sends the completion request to the completion model provider and returns the completion response.
1298    pub async fn send(self) -> Result<CompletionResponse, CompletionError> {
1299        let (model, request) = self.into_model_and_request();
1300        request.validate_message_content()?;
1301        model.completion(request).await
1302    }
1303
1304    /// Stream the completion request
1305    pub async fn stream(self) -> Result<StreamingCompletionResponse, CompletionError> {
1306        let (model, request) = self.into_model_and_request();
1307        request.validate_message_content()?;
1308        model.stream(request).await
1309    }
1310}
1311
1312#[cfg(test)]
1313mod tests {
1314    use super::{CompletionResponse, FinishReason, ProviderCapabilities, Usage};
1315    use crate::message::AssistantContent;
1316
1317    mod message_content_validation {
1318        use super::super::CompletionRequest;
1319        use crate::message::{AssistantContent, Message, UserContent};
1320
1321        fn request(chat_history: Vec<Message>) -> CompletionRequest {
1322            CompletionRequest {
1323                model: None,
1324                preamble: None,
1325                chat_history,
1326                documents: Vec::new(),
1327                tools: Vec::new(),
1328                temperature: None,
1329                max_tokens: None,
1330                tool_choice: None,
1331                additional_params: None,
1332                output_schema: None,
1333                record_telemetry_content: false,
1334            }
1335        }
1336
1337        #[test]
1338        fn a_populated_history_passes() {
1339            let request = request(vec![Message::user("hello")]);
1340            assert!(request.validate_message_content().is_ok());
1341        }
1342
1343        #[test]
1344        fn an_empty_history_is_rejected() {
1345            // `chat_history` was non-empty by construction until the container
1346            // was removed; the field is public, so this is now constructible.
1347            let error = request(Vec::new())
1348                .validate_message_content()
1349                .expect_err("an empty history must not reach a provider");
1350            assert!(
1351                error.to_string().contains("empty chat history"),
1352                "unexpected error: {error}"
1353            );
1354        }
1355
1356        #[test]
1357        fn an_empty_user_message_is_rejected_by_index_and_role() {
1358            let error = request(vec![
1359                Message::user("hello"),
1360                Message::User {
1361                    content: Vec::new(),
1362                },
1363            ])
1364            .validate_message_content()
1365            .expect_err("an empty user message must not reach a provider");
1366            let message = error.to_string();
1367            assert!(message.contains("user message at index 1"), "{message}");
1368        }
1369
1370        #[test]
1371        fn an_empty_assistant_message_is_rejected_by_index_and_role() {
1372            let error = request(vec![Message::Assistant {
1373                id: None,
1374                content: Vec::new(),
1375            }])
1376            .validate_message_content()
1377            .expect_err("an empty assistant message must not reach a provider");
1378            let message = error.to_string();
1379            assert!(
1380                message.contains("assistant message at index 0"),
1381                "{message}"
1382            );
1383        }
1384
1385        #[test]
1386        fn an_empty_system_message_is_not_rejected() {
1387            // System content is a `String` and always has been, so the removed
1388            // container never constrained it. Rejecting an empty one would be a
1389            // new restriction, and would break a history carrying a
1390            // conditionally built preamble that resolved to `""`.
1391            let request = request(vec![
1392                Message::System {
1393                    content: String::new(),
1394                },
1395                Message::user("hello"),
1396            ]);
1397            assert!(request.validate_message_content().is_ok());
1398        }
1399
1400        #[test]
1401        fn a_block_less_tool_result_is_rejected_naming_the_tool() {
1402            use crate::message::{ToolCallId, ToolResult, ToolResultContent};
1403            // `ToolResult::content` was non-empty by construction until the
1404            // container was removed; the message around it has one item, so the
1405            // message-level check alone would let this reach the wire as
1406            // `"content": []`.
1407            let error = request(vec![
1408                Message::user("hello"),
1409                Message::User {
1410                    content: vec![UserContent::ToolResult(ToolResult {
1411                        call: ToolCallId::new_or_mint("call_1"),
1412                        provider: None,
1413                        name: "lookup".to_owned(),
1414                        content: Vec::<ToolResultContent>::new(),
1415                    })],
1416                },
1417            ])
1418            .validate_message_content()
1419            .expect_err("a block-less tool result must not reach a provider");
1420            let message = error.to_string();
1421            assert!(message.contains("`lookup`"), "{message}");
1422            assert!(message.contains("index 0"), "{message}");
1423            assert!(message.contains("user message at index 1"), "{message}");
1424        }
1425
1426        #[test]
1427        fn a_tool_result_with_one_empty_string_block_is_accepted() {
1428            use crate::message::{ToolCallId, ToolResult, ToolResultContent};
1429            // The guard is on the cardinality of the block list, not on the
1430            // blocks' content. A tool that legitimately returned an empty
1431            // string produces one block and must still send — this pins the
1432            // "no blocks" / "no content" distinction so a future tightening
1433            // pass cannot collapse it.
1434            let request = request(vec![Message::User {
1435                content: vec![UserContent::ToolResult(ToolResult {
1436                    call: ToolCallId::new_or_mint("call_1"),
1437                    provider: None,
1438                    name: "lookup".to_owned(),
1439                    content: vec![ToolResultContent::text("")],
1440                })],
1441            }]);
1442            assert!(request.validate_message_content().is_ok());
1443        }
1444
1445        #[test]
1446        fn the_legacy_fabricated_sentinel_still_passes() {
1447            // Histories persisted before message content became a `Vec` encode a
1448            // content-less assistant turn as a single empty text part. That is a
1449            // one-element list, so it validates — the rule is block count, not
1450            // block content — and, being caller-supplied history, it is never
1451            // filtered: it goes to the wire as-is (where some providers reject
1452            // it). Callers migrating pre-`Vec` histories drop such turns
1453            // themselves; see MIGRATING.
1454            let request = request(vec![
1455                Message::user("hello"),
1456                Message::Assistant {
1457                    id: None,
1458                    content: vec![AssistantContent::text("")],
1459                },
1460                Message::User {
1461                    content: vec![UserContent::text("and again")],
1462                },
1463            ]);
1464            assert!(request.validate_message_content().is_ok());
1465        }
1466    }
1467
1468    fn tool_call_choice() -> Vec<AssistantContent> {
1469        vec![AssistantContent::tool_call(
1470            "call_1",
1471            "lookup",
1472            serde_json::json!({"query": "rig"}),
1473        )]
1474    }
1475
1476    #[test]
1477    fn normalized_response_round_trips_through_serde() {
1478        let response = CompletionResponse::new(
1479            vec![AssistantContent::text("hello")],
1480            Usage {
1481                input_tokens: 3,
1482                output_tokens: 2,
1483                total_tokens: 5,
1484                cached_input_tokens: 1,
1485                cache_creation_input_tokens: 0,
1486                tool_use_prompt_tokens: 0,
1487                reasoning_tokens: 1,
1488            },
1489            "example",
1490        )
1491        .with_message_id("msg_123")
1492        .with_finish_reason(FinishReason::Stop)
1493        .with_model("provider-model-v2");
1494
1495        let encoded = serde_json::to_value(&response).expect("serialize response");
1496        let decoded =
1497            serde_json::from_value::<CompletionResponse>(encoded.clone()).expect("deserialize");
1498
1499        assert_eq!(
1500            serde_json::to_value(decoded).expect("re-serialize"),
1501            encoded
1502        );
1503    }
1504
1505    /// Serde must not be a back door around `reconcile_with_output`: a
1506    /// persisted `"stop"` next to a tool-call choice deserializes as
1507    /// `ToolCalls`, exactly as if it had gone through the setter.
1508    #[test]
1509    fn deserializing_stop_with_a_tool_call_reconciles_to_tool_calls() {
1510        let mut encoded = serde_json::to_value(CompletionResponse::new(
1511            tool_call_choice(),
1512            Usage::new(),
1513            "example",
1514        ))
1515        .expect("serialize response");
1516        encoded["finish_reason"] = serde_json::json!("stop");
1517
1518        let decoded =
1519            serde_json::from_value::<CompletionResponse>(encoded).expect("deserialize response");
1520
1521        assert_eq!(decoded.finish_reason(), Some(FinishReason::ToolCalls));
1522    }
1523
1524    /// Serde must not be a back door around the empty-string filtering either:
1525    /// a persisted `""` identifier deserializes as `None`.
1526    #[test]
1527    fn deserializing_empty_identifiers_yields_none() {
1528        let mut encoded = serde_json::to_value(CompletionResponse::new(
1529            vec![AssistantContent::text("hello")],
1530            Usage::new(),
1531            "example",
1532        ))
1533        .expect("serialize response");
1534        encoded["message_id"] = serde_json::json!("");
1535        encoded["response_id"] = serde_json::json!("");
1536        encoded["model"] = serde_json::json!("");
1537
1538        let decoded =
1539            serde_json::from_value::<CompletionResponse>(encoded).expect("deserialize response");
1540
1541        assert_eq!(decoded.message_id, None);
1542        assert_eq!(decoded.response_id, None);
1543        assert_eq!(decoded.model, None);
1544    }
1545
1546    #[test]
1547    fn unknown_finish_reason_survives_a_serde_round_trip_verbatim() {
1548        let reason = FinishReason::Other("provider_specific_stop".to_owned());
1549        let encoded = serde_json::to_string(&reason).expect("serialize");
1550        let decoded = serde_json::from_str::<FinishReason>(&encoded).expect("deserialize");
1551
1552        assert_eq!(decoded, reason);
1553    }
1554
1555    #[test]
1556    fn stop_with_a_tool_call_reconciles_to_tool_calls() {
1557        let response = CompletionResponse::new(tool_call_choice(), Usage::new(), "example")
1558            .with_finish_reason(FinishReason::Stop);
1559
1560        assert_eq!(response.finish_reason, Some(FinishReason::ToolCalls));
1561    }
1562
1563    /// The `Option` setter is what provider conversions actually reach for, so
1564    /// it must reconcile identically — a provider holding an `Option` must not
1565    /// have to choose between ergonomics and correctness.
1566    #[test]
1567    fn optional_setter_reconciles_exactly_like_the_plain_setter() {
1568        let via_option = CompletionResponse::new(tool_call_choice(), Usage::new(), "example")
1569            .with_optional_finish_reason(Some(FinishReason::Stop));
1570        let via_plain = CompletionResponse::new(tool_call_choice(), Usage::new(), "example")
1571            .with_finish_reason(FinishReason::Stop);
1572
1573        assert_eq!(via_option.finish_reason, Some(FinishReason::ToolCalls));
1574        assert_eq!(via_option.finish_reason, via_plain.finish_reason);
1575    }
1576
1577    #[test]
1578    fn reconciliation_only_upgrades_a_natural_stop() {
1579        // A truncated tool call is still a truncation; a filtered one is still
1580        // filtered. Overriding either would lose why the turn actually ended.
1581        for reason in [
1582            FinishReason::Length,
1583            FinishReason::ContentFilter,
1584            FinishReason::Other("provider_specific".to_owned()),
1585        ] {
1586            let response = CompletionResponse::new(tool_call_choice(), Usage::new(), "example")
1587                .with_finish_reason(reason.clone());
1588
1589            assert_eq!(response.finish_reason, Some(reason));
1590        }
1591    }
1592
1593    #[test]
1594    fn reconciliation_leaves_a_stop_without_tool_calls_alone() {
1595        let response = CompletionResponse::new(
1596            vec![AssistantContent::text("done")],
1597            Usage::new(),
1598            "example",
1599        )
1600        .with_finish_reason(FinishReason::Stop);
1601
1602        assert_eq!(response.finish_reason, Some(FinishReason::Stop));
1603    }
1604
1605    #[test]
1606    fn provider_capabilities_are_externally_configurable_from_default() {
1607        let capabilities =
1608            ProviderCapabilities::default().with_native_output_tool_composition(true);
1609
1610        assert!(capabilities.composes_native_output_with_tools);
1611        assert!(!ProviderCapabilities::new().composes_native_output_with_tools);
1612        assert_eq!(ProviderCapabilities::new(), ProviderCapabilities::default());
1613    }
1614
1615    #[test]
1616    fn usage_has_values_reflects_the_zero_sentinel() {
1617        use super::Usage;
1618
1619        assert!(!Usage::new().has_values());
1620
1621        let mut usage = Usage::new();
1622        usage.reasoning_tokens = 1;
1623        assert!(usage.has_values());
1624    }
1625
1626    use super::*;
1627    use crate::test_utils::MockCompletionModel;
1628
1629    #[test]
1630    fn completion_request_content_telemetry_is_opt_in_and_not_serialized() {
1631        let default_request =
1632            CompletionRequestBuilder::new(MockCompletionModel::default(), "completion prompt")
1633                .build();
1634        assert!(!default_request.record_telemetry_content);
1635
1636        let default_json = serde_json::to_value(&default_request).expect("serialize request");
1637        assert!(
1638            default_json.get("record_telemetry_content").is_none(),
1639            "safe default should not serialize the telemetry opt-in field"
1640        );
1641        let default_roundtrip: CompletionRequest =
1642            serde_json::from_value(default_json).expect("deserialize default request");
1643        assert!(!default_roundtrip.record_telemetry_content);
1644
1645        let opt_in_request =
1646            CompletionRequestBuilder::new(MockCompletionModel::default(), "completion prompt")
1647                .record_content_telemetry(true)
1648                .build();
1649        assert!(opt_in_request.record_telemetry_content);
1650
1651        let opt_in_json = serde_json::to_value(&opt_in_request).expect("serialize opt-in request");
1652        assert!(
1653            opt_in_json.get("record_telemetry_content").is_none(),
1654            "local telemetry policy must not be serialized into provider requests"
1655        );
1656        let legacy_roundtrip: CompletionRequest =
1657            serde_json::from_value(opt_in_json).expect("deserialize legacy request");
1658        assert!(
1659            !legacy_roundtrip.record_telemetry_content,
1660            "missing field should deserialize to the safe default"
1661        );
1662    }
1663
1664    /// The deserialization mirror carries `raw`: a response with a captured
1665    /// payload survives serialize → deserialize with the payload intact, a
1666    /// response serialized before the field existed still loads with `raw`
1667    /// unset, and an unset `raw` is not written.
1668    #[test]
1669    fn normalized_response_raw_round_trips_through_serde_mirror() {
1670        let payload = serde_json::json!({
1671            "id": "chatcmpl-1",
1672            "system_fingerprint": "fp_abc",
1673            "choices": [{"finish_reason": "stop"}]
1674        });
1675        let response = CompletionResponse::new(
1676            vec![AssistantContent::text("hello")],
1677            Usage::new(),
1678            "example",
1679        )
1680        .with_response_id("chatcmpl-1")
1681        .with_raw(payload.clone());
1682
1683        let encoded = serde_json::to_value(&response).expect("serialize response");
1684        assert_eq!(encoded["raw"], payload);
1685        let decoded: CompletionResponse =
1686            serde_json::from_value(encoded.clone()).expect("deserialize response");
1687        assert_eq!(decoded.raw, payload);
1688        assert_eq!(decoded.response_id.as_deref(), Some("chatcmpl-1"));
1689        assert_eq!(
1690            serde_json::to_value(&decoded).expect("re-serialize"),
1691            encoded
1692        );
1693
1694        let legacy = serde_json::json!({
1695            "choice": [{"type": "text", "text": "hello"}],
1696            "usage": serde_json::to_value(Usage::new()).unwrap(),
1697            "provider": "example"
1698        });
1699        let decoded: CompletionResponse = serde_json::from_value(legacy).expect("legacy loads");
1700        assert!(decoded.raw.is_null());
1701
1702        let bare = serde_json::to_value(CompletionResponse::new(
1703            vec![AssistantContent::text("hello")],
1704            Usage::new(),
1705            "example",
1706        ))
1707        .unwrap();
1708        assert!(bare.get("raw").is_none());
1709    }
1710
1711    fn test_document(id: &str, text: &str) -> Document {
1712        Document {
1713            id: id.to_string(),
1714            text: text.to_string(),
1715            additional_props: HashMap::new(),
1716        }
1717    }
1718
1719    #[test]
1720    fn message_telemetry_includes_normalized_documents() {
1721        let builder = CompletionRequestBuilder::new(MockCompletionModel::default(), "prompt")
1722            .preamble("system".to_string())
1723            .message(Message::user("history"))
1724            .document(test_document("doc1", "static context secret"));
1725
1726        let messages = builder.messages_for_telemetry();
1727        assert_eq!(messages.len(), 4);
1728        assert!(matches!(messages[0], Message::System { .. }));
1729        assert!(is_document_message(&messages[1], "doc1"));
1730        assert!(matches!(
1731            &messages[2],
1732            Message::User { content }
1733                if matches!(content.first(), Some(UserContent::Text(text)) if text.text == "history")
1734        ));
1735        assert!(matches!(
1736            &messages[3],
1737            Message::User { content }
1738                if matches!(content.first(), Some(UserContent::Text(text)) if text.text == "prompt")
1739        ));
1740
1741        let request = builder.build();
1742        assert_eq!(messages, request.chat_history_with_documents());
1743    }
1744
1745    fn is_document_message(message: &Message, expected_id: &str) -> bool {
1746        let Message::User { content } = message else {
1747            return false;
1748        };
1749
1750        content.iter().any(|content| {
1751            matches!(
1752                content,
1753                UserContent::Document(document)
1754                    if document.data.to_string().contains(&format!("<file id: {expected_id}>"))
1755            )
1756        })
1757    }
1758
1759    #[test]
1760    fn test_document_display_without_metadata() {
1761        let doc = Document {
1762            id: "123".to_string(),
1763            text: "This is a test document.".to_string(),
1764            additional_props: HashMap::new(),
1765        };
1766
1767        let expected = "<file id: 123>\nThis is a test document.\n</file>\n";
1768        assert_eq!(format!("{doc}"), expected);
1769    }
1770
1771    #[test]
1772    fn test_document_display_with_metadata() {
1773        let mut additional_props = HashMap::new();
1774        additional_props.insert("author".to_string(), "John Doe".to_string());
1775        additional_props.insert("length".to_string(), "42".to_string());
1776
1777        let doc = Document {
1778            id: "123".to_string(),
1779            text: "This is a test document.".to_string(),
1780            additional_props,
1781        };
1782
1783        let expected = concat!(
1784            "<file id: 123>\n",
1785            "<metadata author: \"John Doe\" length: \"42\" />\n",
1786            "This is a test document.\n",
1787            "</file>\n"
1788        );
1789        assert_eq!(format!("{doc}"), expected);
1790    }
1791
1792    #[test]
1793    fn test_normalize_documents_with_documents() {
1794        let doc1 = Document {
1795            id: "doc1".to_string(),
1796            text: "Document 1 text.".to_string(),
1797            additional_props: HashMap::new(),
1798        };
1799
1800        let doc2 = Document {
1801            id: "doc2".to_string(),
1802            text: "Document 2 text.".to_string(),
1803            additional_props: HashMap::new(),
1804        };
1805
1806        let request = CompletionRequest {
1807            model: None,
1808            preamble: None,
1809            chat_history: vec!["What is the capital of France?".into()],
1810            documents: vec![doc1, doc2],
1811            tools: Vec::new(),
1812            temperature: None,
1813            max_tokens: None,
1814            tool_choice: None,
1815            additional_params: None,
1816            output_schema: None,
1817            record_telemetry_content: false,
1818        };
1819
1820        let expected = Message::User {
1821            content: vec![
1822                UserContent::document(
1823                    "<file id: doc1>\nDocument 1 text.\n</file>\n".to_string(),
1824                    Some(DocumentMediaType::TXT),
1825                ),
1826                UserContent::document(
1827                    "<file id: doc2>\nDocument 2 text.\n</file>\n".to_string(),
1828                    Some(DocumentMediaType::TXT),
1829                ),
1830            ],
1831        };
1832
1833        assert_eq!(request.normalized_documents(), Some(expected));
1834    }
1835
1836    #[test]
1837    fn test_normalize_documents_without_documents() {
1838        let request = CompletionRequest {
1839            model: None,
1840            preamble: None,
1841            chat_history: vec!["What is the capital of France?".into()],
1842            documents: Vec::new(),
1843            tools: Vec::new(),
1844            temperature: None,
1845            max_tokens: None,
1846            tool_choice: None,
1847            additional_params: None,
1848            output_schema: None,
1849            record_telemetry_content: false,
1850        };
1851
1852        assert_eq!(request.normalized_documents(), None);
1853    }
1854
1855    #[test]
1856    fn preamble_builder_funnels_to_system_message() {
1857        let request =
1858            CompletionRequestBuilder::new(MockCompletionModel::default(), Message::user("Prompt"))
1859                .preamble("System prompt".to_string())
1860                .message(Message::user("History"))
1861                .build();
1862
1863        assert_eq!(request.preamble, None);
1864
1865        let history = request.chat_history.into_iter().collect::<Vec<_>>();
1866        assert_eq!(history.len(), 3);
1867        assert!(matches!(
1868            &history[0],
1869            Message::System { content } if content == "System prompt"
1870        ));
1871        assert!(matches!(&history[1], Message::User { .. }));
1872        assert!(matches!(&history[2], Message::User { .. }));
1873    }
1874
1875    #[test]
1876    fn without_preamble_removes_legacy_preamble_injection() {
1877        let request =
1878            CompletionRequestBuilder::new(MockCompletionModel::default(), Message::user("Prompt"))
1879                .preamble("System prompt".to_string())
1880                .without_preamble()
1881                .build();
1882
1883        assert_eq!(request.preamble, None);
1884        let history = request.chat_history.into_iter().collect::<Vec<_>>();
1885        assert_eq!(history.len(), 1);
1886        assert!(matches!(&history[0], Message::User { .. }));
1887    }
1888
1889    #[test]
1890    fn build_places_documents_after_preamble_system_message() {
1891        let request =
1892            CompletionRequestBuilder::new(MockCompletionModel::default(), Message::user("Prompt"))
1893                .preamble("System prompt".to_string())
1894                .document(test_document("doc1", "Document text."))
1895                .build();
1896
1897        assert_eq!(request.documents.len(), 1);
1898
1899        let history = request.chat_history_with_documents();
1900        let history = history.iter().collect::<Vec<_>>();
1901        assert_eq!(history.len(), 3);
1902        assert!(matches!(
1903            history[0],
1904            Message::System { content } if content == "System prompt"
1905        ));
1906        assert!(is_document_message(history[1], "doc1"));
1907        assert!(matches!(history[2], Message::User { .. }));
1908    }
1909
1910    #[test]
1911    fn build_places_documents_after_leading_system_messages_before_prior_history() {
1912        let request =
1913            CompletionRequestBuilder::new(MockCompletionModel::default(), Message::user("Prompt"))
1914                .message(Message::system("System one"))
1915                .message(Message::system("System two"))
1916                .message(Message::user("Earlier user turn"))
1917                .message(Message::assistant("Earlier assistant turn"))
1918                .document(test_document("doc1", "Document text."))
1919                .build();
1920
1921        let history = request.chat_history_with_documents();
1922        let history = history.iter().collect::<Vec<_>>();
1923        assert_eq!(history.len(), 6);
1924        assert!(matches!(
1925            history[0],
1926            Message::System { content } if content == "System one"
1927        ));
1928        assert!(matches!(
1929            history[1],
1930            Message::System { content } if content == "System two"
1931        ));
1932        assert!(is_document_message(history[2], "doc1"));
1933        assert!(matches!(history[3], Message::User { .. }));
1934        assert!(matches!(history[4], Message::Assistant { .. }));
1935        assert!(matches!(history[5], Message::User { .. }));
1936    }
1937
1938    #[test]
1939    fn build_without_documents_keeps_message_order_unchanged() {
1940        let request =
1941            CompletionRequestBuilder::new(MockCompletionModel::default(), Message::user("Prompt"))
1942                .message(Message::system("System prompt"))
1943                .message(Message::user("Earlier user turn"))
1944                .build();
1945
1946        let history = request.chat_history.iter().collect::<Vec<_>>();
1947        assert_eq!(history.len(), 3);
1948        assert!(matches!(
1949            history[0],
1950            Message::System { content } if content == "System prompt"
1951        ));
1952        assert!(matches!(history[1], Message::User { .. }));
1953        assert!(matches!(history[2], Message::User { .. }));
1954    }
1955
1956    #[test]
1957    fn chat_history_with_documents_places_documents_after_leading_system_messages() {
1958        let request = CompletionRequest {
1959            model: None,
1960            preamble: None,
1961            chat_history: vec![
1962                Message::system("System prompt"),
1963                Message::assistant("Earlier assistant turn"),
1964                Message::user("Earlier user turn"),
1965                Message::user("Prompt"),
1966            ],
1967            documents: vec![test_document("doc1", "Document text.")],
1968            tools: Vec::new(),
1969            temperature: None,
1970            max_tokens: None,
1971            tool_choice: None,
1972            additional_params: None,
1973            output_schema: None,
1974            record_telemetry_content: false,
1975        };
1976
1977        assert_eq!(request.documents.len(), 1);
1978
1979        let history = request.chat_history_with_documents();
1980        let history = history.iter().collect::<Vec<_>>();
1981        assert_eq!(history.len(), 5);
1982        assert!(matches!(history[0], Message::System { .. }));
1983        assert!(is_document_message(history[1], "doc1"));
1984        assert!(matches!(history[2], Message::Assistant { .. }));
1985        assert!(matches!(history[3], Message::User { .. }));
1986        assert!(matches!(history[4], Message::User { .. }));
1987    }
1988
1989    #[test]
1990    fn chat_history_with_documents_places_documents_before_mid_conversation_system_messages() {
1991        let request = CompletionRequest {
1992            model: None,
1993            preamble: None,
1994            chat_history: vec![
1995                Message::system("Leading system prompt"),
1996                Message::assistant("Earlier assistant turn"),
1997                Message::system("Mid-conversation instruction"),
1998                Message::user("Prompt"),
1999            ],
2000            documents: vec![test_document("doc1", "Document text.")],
2001            tools: Vec::new(),
2002            temperature: None,
2003            max_tokens: None,
2004            tool_choice: None,
2005            additional_params: None,
2006            output_schema: None,
2007            record_telemetry_content: false,
2008        };
2009
2010        let history = request.chat_history_with_documents();
2011        let history = history.iter().collect::<Vec<_>>();
2012        assert_eq!(history.len(), 5);
2013        assert!(matches!(
2014            history[0],
2015            Message::System { content } if content == "Leading system prompt"
2016        ));
2017        assert!(is_document_message(history[1], "doc1"));
2018        assert!(matches!(history[2], Message::Assistant { .. }));
2019        assert!(matches!(
2020            history[3],
2021            Message::System { content } if content == "Mid-conversation instruction"
2022        ));
2023        assert!(matches!(history[4], Message::User { .. }));
2024    }
2025
2026    #[test]
2027    fn chat_history_with_documents_does_not_duplicate_documents() {
2028        let request = CompletionRequest {
2029            model: None,
2030            preamble: None,
2031            chat_history: vec![
2032                Message::system("System prompt"),
2033                Message::user("Earlier user turn"),
2034                Message::assistant("Earlier assistant turn"),
2035                Message::user("Prompt"),
2036            ],
2037            documents: vec![test_document("doc1", "Document text.")],
2038            tools: Vec::new(),
2039            temperature: None,
2040            max_tokens: None,
2041            tool_choice: None,
2042            additional_params: None,
2043            output_schema: None,
2044            record_telemetry_content: false,
2045        };
2046
2047        let history = request.chat_history_with_documents();
2048        let document_messages = history
2049            .iter()
2050            .filter(|message| is_document_message(message, "doc1"))
2051            .count();
2052        assert_eq!(document_messages, 1);
2053    }
2054
2055    #[test]
2056    fn completion_error_provider_response_helpers_with_preserved_json_body() {
2057        let body = r#"{"error":{"code":"rate_limit","message":"slow down"}}"#;
2058        let error = CompletionError::ProviderResponse(
2059            provider_response::ProviderResponseError::without_status(body.to_string()),
2060        );
2061
2062        assert_eq!(error.provider_response_body(), Some(body));
2063        assert_eq!(error.provider_response_status(), None);
2064        assert_eq!(
2065            error
2066                .provider_response_json()
2067                .expect("fixture body should parse as valid JSON"),
2068            Some(serde_json::json!({
2069                "error": {
2070                    "code": "rate_limit",
2071                    "message": "slow down"
2072                }
2073            }))
2074        );
2075    }
2076
2077    #[test]
2078    fn completion_error_provider_response_helpers_with_preserved_status() {
2079        let body = r#"{"error":{"message":"too many requests"}}"#;
2080        let error =
2081            CompletionError::ProviderResponse(provider_response::ProviderResponseError::new(
2082                http::StatusCode::TOO_MANY_REQUESTS,
2083                body.to_string(),
2084            ));
2085
2086        assert_eq!(error.provider_response_body(), Some(body));
2087        assert_eq!(
2088            error.provider_response_status(),
2089            Some(http::StatusCode::TOO_MANY_REQUESTS)
2090        );
2091    }
2092
2093    #[test]
2094    fn completion_error_provider_response_helpers_with_preserved_plain_text_body() {
2095        let error = CompletionError::ProviderResponse(
2096            provider_response::ProviderResponseError::without_status(
2097                "provider exploded".to_string(),
2098            ),
2099        );
2100
2101        assert_eq!(error.provider_response_body(), Some("provider exploded"));
2102        assert_eq!(error.provider_response_status(), None);
2103        assert!(error.provider_response_json().is_err());
2104    }
2105
2106    #[test]
2107    fn completion_error_provider_error_is_not_a_provider_response() {
2108        // `ProviderError` also carries Rig-generated diagnostics, so the helpers
2109        // must not report its string as a provider response body.
2110        let error = CompletionError::ProviderError("stream transport failed".to_string());
2111
2112        assert_eq!(error.provider_response_body(), None);
2113        assert_eq!(error.provider_response_status(), None);
2114        assert_eq!(
2115            error
2116                .provider_response_json()
2117                .expect("no body is not an error"),
2118            None
2119        );
2120    }
2121
2122    #[test]
2123    fn completion_error_provider_response_helpers_with_http_non_success_body_and_status() {
2124        let body = r#"{"error":{"type":"invalid_request","message":"bad request"}}"#;
2125        let error = CompletionError::HttpError(http_client::Error::InvalidStatusCodeWithMessage(
2126            http::StatusCode::BAD_REQUEST,
2127            body.to_string(),
2128        ));
2129
2130        assert_eq!(error.provider_response_body(), Some(body));
2131        assert_eq!(
2132            error.provider_response_status(),
2133            Some(http::StatusCode::BAD_REQUEST)
2134        );
2135        assert_eq!(
2136            error.provider_response_json().expect("valid JSON body"),
2137            Some(serde_json::json!({
2138                "error": {
2139                    "type": "invalid_request",
2140                    "message": "bad request"
2141                }
2142            }))
2143        );
2144    }
2145
2146    #[test]
2147    fn completion_error_provider_response_helpers_with_unrelated_variant() {
2148        let error = CompletionError::ResponseError("failed to parse provider response".to_string());
2149
2150        assert_eq!(error.provider_response_body(), None);
2151        assert_eq!(error.provider_response_status(), None);
2152        assert_eq!(
2153            error
2154                .provider_response_json()
2155                .expect("no body is not an error"),
2156            None
2157        );
2158    }
2159
2160    #[test]
2161    fn provider_response_json_returns_none_for_empty_preserved_body() {
2162        let error = CompletionError::ProviderResponse(
2163            provider_response::ProviderResponseError::without_status(String::new()),
2164        );
2165
2166        assert_eq!(error.provider_response_body(), Some(""));
2167        assert_eq!(
2168            error
2169                .provider_response_json()
2170                .expect("empty body is not a JSON parse error"),
2171            None
2172        );
2173    }
2174}
2175
2176#[cfg(test)]
2177mod response_identity_tests {
2178    use super::*;
2179
2180    /// Serde compatibility (rig#2265): responses persisted before
2181    /// `provider_request_id` existed still load, with the field `None`.
2182    #[test]
2183    fn completion_response_without_request_id_still_deserializes() {
2184        let response: CompletionResponse = serde_json::from_str(
2185            r#"{"choice": [{"type": "text", "text": "hi"}],
2186                "usage": {"input_tokens": 1, "output_tokens": 1, "total_tokens": 2,
2187                          "cached_input_tokens": 0, "cache_creation_input_tokens": 0,
2188                          "reasoning_tokens": 0},
2189                "provider": "test"}"#,
2190        )
2191        .expect("pre-identity CompletionResponse JSON should load");
2192        assert_eq!(response.provider_request_id, None);
2193        assert_eq!(response.identity(), ResponseIdentity::default());
2194    }
2195
2196    /// The identity accessor mirrors the flat fields exactly.
2197    #[test]
2198    fn identity_accessor_mirrors_flat_fields() {
2199        let response = CompletionResponse::new(
2200            vec![crate::completion::AssistantContent::text("hi")],
2201            Usage::new(),
2202            "test",
2203        )
2204        .with_message_id("msg_1")
2205        .with_response_id("resp_1")
2206        .with_provider_request_id("req_1");
2207        assert_eq!(
2208            response.identity(),
2209            ResponseIdentity {
2210                message_id: Some("msg_1".into()),
2211                response_id: Some("resp_1".into()),
2212                provider_request_id: Some("req_1".into()),
2213            }
2214        );
2215    }
2216}