Skip to main content

openai_interface/responses/
mod.rs

1//! The Responses API: create model responses and retrieve stored ones.
2//!
3//! The Responses API is the successor of the Chat Completions API. Instead
4//! of `messages` it takes an `input` that is either a plain string or a list
5//! of input items (`message`, `function_call`, `function_call_output`,
6//! `reasoning`, ...), and instead of `choices` it returns a list of output
7//! items.
8//!
9//! Request types live in [`create::request`], the shared response object and
10//! the streaming event types in this module and [`create`], and the retrieve
11//! endpoint in [`retrieve`].
12//!
13//! Provider support is partial — see
14//! [the DeepSeek Responses API guide](https://api-docs.deepseek.com/guides/responses_api)
15//! and
16//! [the Qwen Responses API reference](https://www.alibabacloud.com/help/zh/model-studio/qwen-api-via-openai-responses).
17//! Notably DeepSeek's implementation is stateless (`store`,
18//! `previous_response_id` and retrieval are not supported there), while Qwen
19//! supports multi-turn conversations via `previous_response_id`.
20//!
21//! # Example
22//!
23//! ```rust,no_run
24//! use openai_interface::responses::create::request::{Input, RequestBody};
25//! use openai_interface::rest::{default_client, post::PostNoStream, RequestOptions};
26//!
27//! const DEEPSEEK_URL: &'static str = "https://api.deepseek.com";
28//! const DEEPSEEK_MODEL: &'static str = "deepseek-v4-flash";
29//!
30//! #[tokio::main]
31//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
32//!     // Needs the `ferritls` cargo feature; drop this line if you install
33//!     // your own rustls crypto provider (see `openai_interface::rest`).
34//!     # #[cfg(feature = "ferritls")]
35//!     openai_interface::rest::install_crypto_provider().ok();
36//!
37//!     let request = RequestBody {
38//!         model: DEEPSEEK_MODEL.to_string(),
39//!         input: Input::Text("Hello!".to_string()),
40//!         ..Default::default()
41//!     };
42//!
43//!     let response = request
44//!         .get_response(&default_client(), DEEPSEEK_URL, &RequestOptions::bearer("YOUR_API_KEY"))
45//!         .await?;
46//!     println!("{}", response.output_text());
47//!     Ok(())
48//! }
49//! ```
50
51pub mod cancel;
52pub mod create;
53pub mod delete;
54pub mod input_items;
55pub mod retrieve;
56
57use std::collections::HashMap;
58
59use serde::Deserialize;
60
61use crate::chat::ServiceTier;
62use crate::responses::create::request::Truncation;
63
64/// The response object returned by the Responses API (non-streaming create,
65/// the final streaming events, and retrieve).
66#[derive(Debug, Deserialize, Clone)]
67pub struct Response {
68    /// Unique identifier for this response.
69    pub id: String,
70
71    /// Unix timestamp (in seconds) of when this response was created.
72    pub created_at: u64,
73
74    /// An error object returned when the model fails to generate a response.
75    #[serde(default)]
76    pub error: Option<ResponseError>,
77
78    /// Details about why the response is incomplete, if it is.
79    #[serde(default)]
80    pub incomplete_details: Option<IncompleteDetails>,
81
82    /// The system (or developer) message inserted into the model's context,
83    /// echoed back when one was supplied.
84    #[serde(default)]
85    pub instructions: Option<String>,
86
87    /// Set of key-value pairs attached to the response, echoed back when
88    /// `metadata` was supplied in the request.
89    #[serde(default)]
90    pub metadata: Option<HashMap<String, String>>,
91
92    /// The model that generated the response.
93    pub model: String,
94
95    /// The object type, which is always `response`.
96    pub object: ResponseObject,
97
98    /// The output items generated by the model (messages, reasoning, tool
99    /// calls, ...).
100    #[serde(default)]
101    pub output: Vec<ResponseOutputItem>,
102
103    /// Whether the model ran tool calls in parallel.
104    #[serde(default)]
105    pub parallel_tool_calls: Option<bool>,
106
107    /// The unique ID of the previous response this one builds upon, when
108    /// `previous_response_id` was used.
109    #[serde(default)]
110    pub previous_response_id: Option<String>,
111
112    /// The sampling temperature used.
113    #[serde(default)]
114    pub temperature: Option<f32>,
115
116    /// The nucleus sampling threshold used.
117    #[serde(default)]
118    pub top_p: Option<f32>,
119
120    /// Whether the response is stored for later retrieval.
121    #[serde(default)]
122    pub store: Option<bool>,
123
124    /// The status of the response generation.
125    #[serde(default)]
126    pub status: Option<ResponseStatus>,
127
128    /// Usage statistics for the request.
129    #[serde(default)]
130    pub usage: Option<ResponseUsage>,
131
132    /// Whether the response ran in the background (echo of the request
133    /// parameter).
134    #[serde(default)]
135    pub background: Option<bool>,
136
137    /// Unix timestamp (in seconds) of when this response completed. Only
138    /// present when the status is `completed`.
139    #[serde(default)]
140    pub completed_at: Option<u64>,
141
142    /// Upper bound for the number of tokens that could be generated (echo
143    /// of the request parameter).
144    #[serde(default)]
145    pub max_output_tokens: Option<u32>,
146
147    /// Maximum number of built-in tool calls processed in this response
148    /// (echo of the request parameter).
149    #[serde(default)]
150    pub max_tool_calls: Option<u32>,
151
152    /// Number of most likely tokens returned at each output position (echo
153    /// of the request parameter).
154    #[serde(default)]
155    pub top_logprobs: Option<u32>,
156
157    /// The truncation strategy used for the response (echo of the request
158    /// parameter).
159    #[serde(default)]
160    pub truncation: Option<Truncation>,
161
162    /// The service tier actually used to serve the request. May differ from
163    /// the value requested.
164    #[serde(default)]
165    pub service_tier: Option<ServiceTier>,
166
167    /// End-user identifier (echo of the request parameter).
168    #[serde(default)]
169    pub user: Option<String>,
170
171    /// Stable identifier used for usage-policy violation detection (echo of
172    /// the request parameter).
173    #[serde(default)]
174    pub safety_identifier: Option<String>,
175
176    /// Prompt cache key (echo of the request parameter).
177    #[serde(default)]
178    pub prompt_cache_key: Option<String>,
179}
180
181impl Response {
182    /// Whether this response completed successfully.
183    pub fn is_completed(&self) -> bool {
184        matches!(self.status, Some(ResponseStatus::Completed))
185    }
186
187    /// Aggregates all `output_text` content of the output `message` items
188    /// into a single string, like the `output_text` property of the official
189    /// SDKs. Returns an empty string when there is no text output.
190    pub fn output_text(&self) -> String {
191        let mut text = String::new();
192        for item in &self.output {
193            if let ResponseOutputItem::Message(message) = item {
194                for part in &message.content {
195                    if let OutputContent::OutputText { text: t, .. } = part {
196                        text.push_str(t);
197                    }
198                }
199            }
200        }
201        text
202    }
203
204    /// Aggregates the plain-text reasoning content of the output `reasoning`
205    /// items into a single string. Returns an empty string when there is no
206    /// plaintext reasoning content (e.g. when the provider only returns
207    /// reasoning summaries or encrypted content).
208    pub fn reasoning_text(&self) -> String {
209        let mut text = String::new();
210        for item in &self.output {
211            if let ResponseOutputItem::Reasoning(reasoning) = item {
212                for part in reasoning.content.as_deref().unwrap_or_default() {
213                    let ReasoningContent::ReasoningText { text: t } = part;
214                    text.push_str(t);
215                }
216            }
217        }
218        text
219    }
220}
221
222/// The `object` field of a response object, always `response`.
223#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
224pub enum ResponseObject {
225    #[serde(rename = "response")]
226    Response,
227}
228
229/// The status of the response generation.
230#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
231#[serde(rename_all = "snake_case")]
232pub enum ResponseStatus {
233    InProgress,
234    Completed,
235    Incomplete,
236    Failed,
237    Cancelled,
238    Queued,
239}
240
241/// An error object returned when the model fails to generate a response.
242#[derive(Debug, Deserialize, Clone)]
243pub struct ResponseError {
244    /// The machine-readable error code, if the provider sends one.
245    #[serde(default)]
246    pub code: Option<String>,
247    /// A human-readable description of the error.
248    #[serde(default)]
249    pub message: Option<String>,
250}
251
252/// Details about why the response is incomplete.
253#[derive(Debug, Deserialize, Clone)]
254pub struct IncompleteDetails {
255    /// The reason why the response is incomplete.
256    #[serde(default)]
257    pub reason: Option<IncompleteReason>,
258}
259
260/// Why a response is incomplete.
261#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
262#[serde(rename_all = "snake_case")]
263pub enum IncompleteReason {
264    MaxOutputTokens,
265    MaxMessages,
266    ContentFilter,
267}
268
269/// An item in the `output` array of a [`Response`].
270///
271/// Item types not modeled explicitly are preserved as
272/// [`ResponseOutputItem::Other`] so that unknown or provider-specific items
273/// do not break deserialization.
274#[derive(Debug, Deserialize, Clone)]
275#[serde(tag = "type", rename_all = "snake_case")]
276pub enum ResponseOutputItem {
277    /// A message output from the model.
278    Message(ResponseOutputMessage),
279    /// A reasoning item from the model (chain-of-thought and/or summaries).
280    Reasoning(ResponseReasoningItem),
281    /// A call to a user-defined function.
282    FunctionCall(ResponseFunctionCall),
283    /// A web search performed by the server-side web search tool.
284    WebSearchCall(ResponseWebSearchCall),
285    /// Qwen: a call to a tool on an MCP server.
286    #[cfg(feature = "qwen")]
287    McpCall(McpCall),
288    /// Qwen: a knowledge-base (file search) tool call.
289    #[cfg(feature = "qwen")]
290    FileSearchCall(FileSearchCall),
291    /// Qwen: a code interpreter tool call.
292    #[cfg(feature = "qwen")]
293    CodeInterpreterCall(CodeInterpreterCall),
294    /// Qwen: a web page extraction tool call.
295    #[cfg(feature = "qwen")]
296    WebExtractorCall(WebExtractorCall),
297    /// Qwen: a text-to-image search tool call.
298    #[cfg(feature = "qwen")]
299    WebSearchImageCall(WebSearchImageCall),
300    /// Qwen: an image-to-image search tool call.
301    #[cfg(feature = "qwen")]
302    ImageSearchCall(ImageSearchCall),
303    /// Any item type not covered by the variants above.
304    #[serde(other)]
305    Other,
306}
307
308/// A message output from the model.
309#[derive(Debug, Deserialize, Clone)]
310pub struct ResponseOutputMessage {
311    /// The unique ID of the message.
312    #[serde(default)]
313    pub id: Option<String>,
314    /// The entity that produced the message. Always `assistant`.
315    #[serde(default)]
316    pub role: Option<String>,
317    /// The content of the message (`output_text` and/or `refusal` parts).
318    #[serde(default)]
319    pub content: Vec<OutputContent>,
320    /// The status of the message item.
321    #[serde(default)]
322    pub status: Option<ItemStatus>,
323}
324
325/// The status of an output item.
326#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
327#[serde(rename_all = "snake_case")]
328pub enum ItemStatus {
329    InProgress,
330    Completed,
331    Incomplete,
332}
333
334/// A content part of an output [`ResponseOutputMessage`].
335#[derive(Debug, Deserialize, Clone)]
336#[serde(tag = "type", rename_all = "snake_case")]
337pub enum OutputContent {
338    /// A text output part.
339    OutputText {
340        /// The text content.
341        text: String,
342        /// Annotations for the text, e.g. citations from web search.
343        #[serde(default)]
344        annotations: Vec<Annotation>,
345    },
346    /// A refusal output part.
347    Refusal {
348        /// The refusal message.
349        refusal: String,
350    },
351}
352
353/// An annotation on an output text part.
354#[derive(Debug, Deserialize, Clone)]
355#[serde(tag = "type", rename_all = "snake_case")]
356pub enum Annotation {
357    /// A citation of a web page returned by the web search tool.
358    UrlCitation {
359        /// The URL of the cited web page.
360        url: String,
361        /// The title of the cited web page.
362        #[serde(default)]
363        title: Option<String>,
364        /// The index of the cited web page in the search results.
365        #[serde(default)]
366        url_citation_index: Option<u32>,
367        /// The start index of the cited text in the output.
368        #[serde(default)]
369        start_index: Option<u32>,
370        /// The end index of the cited text in the output.
371        #[serde(default)]
372        end_index: Option<u32>,
373    },
374}
375
376/// A call to a user-defined function.
377#[derive(Debug, Deserialize, Clone)]
378pub struct ResponseFunctionCall {
379    /// The unique ID of the function call.
380    #[serde(default)]
381    pub id: Option<String>,
382    /// The ID used to pair this call with its `function_call_output` when
383    /// passing the result back in a follow-up request.
384    #[serde(default)]
385    pub call_id: Option<String>,
386    /// The name of the function to call.
387    #[serde(default)]
388    pub name: Option<String>,
389    /// The arguments to call the function with, as a JSON string.
390    #[serde(default)]
391    pub arguments: Option<String>,
392    /// The status of the function call item.
393    #[serde(default)]
394    pub status: Option<ItemStatus>,
395}
396
397/// A reasoning item (chain-of-thought and/or summaries) from the model.
398#[derive(Debug, Deserialize, Clone)]
399pub struct ResponseReasoningItem {
400    /// The unique ID of the reasoning item.
401    #[serde(default)]
402    pub id: Option<String>,
403    /// The reasoning content. Providers that expose chain-of-thought text
404    /// (e.g. DeepSeek, Qwen) return `reasoning_text` parts here; OpenAI
405    /// omits it unless requested.
406    #[serde(default)]
407    pub content: Option<Vec<ReasoningContent>>,
408    /// Summaries of the reasoning content.
409    #[serde(default)]
410    pub summary: Vec<ReasoningSummary>,
411    /// The status of the reasoning item.
412    #[serde(default)]
413    pub status: Option<ItemStatus>,
414}
415
416/// A content part of a [`ResponseReasoningItem`].
417///
418/// Currently only the plaintext `reasoning_text` part is modeled; OpenAI
419/// also defines `summary_text` parts here, but this crate carries summaries
420/// in [`ResponseReasoningItem::summary`] instead.
421#[derive(Debug, Deserialize, Clone)]
422#[serde(tag = "type", rename_all = "snake_case")]
423pub enum ReasoningContent {
424    /// Plaintext chain-of-thought text.
425    ReasoningText {
426        /// The reasoning text.
427        text: String,
428    },
429}
430
431/// A summary part of a [`ResponseReasoningItem`].
432#[derive(Debug, Deserialize, Clone)]
433pub struct ReasoningSummary {
434    /// The type of the summary part, always `summary_text`.
435    #[serde(rename = "type")]
436    pub summary_type: Option<String>,
437    /// The summary text.
438    #[serde(default)]
439    pub text: Option<String>,
440}
441
442/// A web search performed by the server-side web search tool.
443#[derive(Debug, Deserialize, Clone)]
444pub struct ResponseWebSearchCall {
445    /// The unique ID of the web search call.
446    #[serde(default)]
447    pub id: Option<String>,
448    /// The status of the web search call.
449    #[serde(default)]
450    pub status: Option<ItemStatus>,
451    /// The search action performed by the server.
452    #[serde(default)]
453    pub action: Option<WebSearchAction>,
454}
455
456/// The search action of a [`ResponseWebSearchCall`].
457#[derive(Debug, Deserialize, Clone)]
458pub struct WebSearchAction {
459    /// The type of the action, e.g. `search`.
460    #[serde(rename = "type")]
461    pub action_type: Option<String>,
462    /// The search query.
463    #[serde(default)]
464    pub query: Option<String>,
465    /// The sources cited by the search.
466    #[serde(default)]
467    pub sources: Vec<WebSearchSource>,
468}
469
470/// A source cited by a web search.
471#[derive(Debug, Deserialize, Clone)]
472pub struct WebSearchSource {
473    /// The type of the source.
474    #[serde(rename = "type")]
475    pub source_type: Option<String>,
476    /// The URL of the source.
477    #[serde(default)]
478    pub url: Option<String>,
479}
480
481/// Qwen: a call to a tool on an MCP server.
482#[cfg(feature = "qwen")]
483#[derive(Debug, Deserialize, Clone)]
484pub struct McpCall {
485    /// The unique ID of the MCP call.
486    #[serde(default)]
487    pub id: Option<String>,
488    /// The name of the MCP tool that was called.
489    #[serde(default)]
490    pub name: Option<String>,
491    /// The label of the MCP server running the tool.
492    #[serde(default)]
493    pub server_label: Option<String>,
494    /// The call arguments, as a JSON string.
495    #[serde(default)]
496    pub arguments: Option<String>,
497    /// The result returned by the MCP server, as a JSON string.
498    #[serde(default)]
499    pub output: Option<String>,
500    /// The status of the MCP call.
501    #[serde(default)]
502    pub status: Option<ItemStatus>,
503}
504
505/// Qwen: a knowledge-base (file search) tool call.
506#[cfg(feature = "qwen")]
507#[derive(Debug, Deserialize, Clone)]
508pub struct FileSearchCall {
509    /// The unique ID of the file search call.
510    #[serde(default)]
511    pub id: Option<String>,
512    /// The search queries generated by the model.
513    #[serde(default)]
514    pub queries: Vec<String>,
515    /// The knowledge-base search results.
516    #[serde(default)]
517    pub results: Vec<serde_json::Value>,
518    /// The status of the file search call.
519    #[serde(default)]
520    pub status: Option<ItemStatus>,
521}
522
523/// Qwen: a code interpreter tool call.
524#[cfg(feature = "qwen")]
525#[derive(Debug, Deserialize, Clone)]
526pub struct CodeInterpreterCall {
527    /// The unique ID of the code interpreter call.
528    #[serde(default)]
529    pub id: Option<String>,
530    /// The code generated and executed by the model.
531    #[serde(default)]
532    pub code: Option<String>,
533    /// The outputs of the code execution.
534    #[serde(default)]
535    pub outputs: Vec<serde_json::Value>,
536    /// The identifier of the code interpreter container.
537    #[serde(default)]
538    pub container_id: Option<String>,
539    /// The status of the code interpreter call.
540    #[serde(default)]
541    pub status: Option<ItemStatus>,
542}
543
544/// Qwen: a web page extraction tool call.
545#[cfg(feature = "qwen")]
546#[derive(Debug, Deserialize, Clone)]
547pub struct WebExtractorCall {
548    /// The unique ID of the web extractor call.
549    #[serde(default)]
550    pub id: Option<String>,
551    /// A description of what information should be extracted from the page.
552    #[serde(default)]
553    pub goal: Option<String>,
554    /// The extracted page content.
555    #[serde(default)]
556    pub output: Option<String>,
557    /// The URLs that were extracted.
558    #[serde(default)]
559    pub urls: Vec<String>,
560    /// The status of the web extractor call.
561    #[serde(default)]
562    pub status: Option<ItemStatus>,
563}
564
565/// Qwen: a text-to-image search tool call.
566#[cfg(feature = "qwen")]
567#[derive(Debug, Deserialize, Clone)]
568pub struct WebSearchImageCall {
569    /// The unique ID of the image search call.
570    #[serde(default)]
571    pub id: Option<String>,
572    /// The tool name, fixed to `web_search_image`.
573    #[serde(default)]
574    pub name: Option<String>,
575    /// The call arguments, as a JSON string containing the search keywords.
576    #[serde(default)]
577    pub arguments: Option<String>,
578    /// The image search results, as a JSON string.
579    #[serde(default)]
580    pub output: Option<String>,
581    /// The status of the image search call.
582    #[serde(default)]
583    pub status: Option<ItemStatus>,
584}
585
586/// Qwen: an image-to-image search tool call.
587#[cfg(feature = "qwen")]
588#[derive(Debug, Deserialize, Clone)]
589pub struct ImageSearchCall {
590    /// The unique ID of the image search call.
591    #[serde(default)]
592    pub id: Option<String>,
593    /// The tool name, fixed to `image_search`.
594    #[serde(default)]
595    pub name: Option<String>,
596    /// The call arguments, as a JSON string containing the image index and
597    /// bounding box.
598    #[serde(default)]
599    pub arguments: Option<String>,
600    /// The image search results, as a JSON string.
601    #[serde(default)]
602    pub output: Option<String>,
603    /// The status of the image search call.
604    #[serde(default)]
605    pub status: Option<ItemStatus>,
606}
607
608/// Token usage statistics of a [`Response`].
609#[derive(Debug, Deserialize, Clone)]
610pub struct ResponseUsage {
611    /// The number of input tokens.
612    pub input_tokens: usize,
613    /// A breakdown of the input tokens.
614    #[serde(default)]
615    pub input_tokens_details: Option<InputTokensDetails>,
616    /// The number of output tokens.
617    pub output_tokens: usize,
618    /// A breakdown of the output tokens.
619    #[serde(default)]
620    pub output_tokens_details: Option<OutputTokensDetails>,
621    /// The total number of tokens used (input + output).
622    pub total_tokens: usize,
623    /// Qwen: per-segment token usage details, including the billing type of
624    /// each segment.
625    #[cfg(feature = "qwen")]
626    #[serde(default)]
627    pub x_details: Option<Vec<serde_json::Value>>,
628    /// Qwen: built-in tool usage statistics, e.g.
629    /// `{"file_search": {"count": 1}}`.
630    #[cfg(feature = "qwen")]
631    #[serde(default)]
632    pub x_tools: Option<serde_json::Value>,
633}
634
635/// A breakdown of the input tokens.
636#[derive(Debug, Deserialize, Clone)]
637pub struct InputTokensDetails {
638    /// The number of input tokens retrieved from the cache.
639    #[serde(default)]
640    pub cached_tokens: Option<usize>,
641    /// The number of input tokens written to the cache.
642    #[serde(default)]
643    pub cache_write_tokens: Option<usize>,
644}
645
646/// A breakdown of the output tokens.
647#[derive(Debug, Deserialize, Clone)]
648pub struct OutputTokensDetails {
649    /// The number of reasoning tokens generated by the model.
650    #[serde(default)]
651    pub reasoning_tokens: Option<usize>,
652}
653
654crate::impl_from_str!(Response);
655
656#[cfg(test)]
657mod tests {
658    //! Offline deserialization tests. The fixtures below are copied from the
659    //! documented examples of the respective providers, not invented.
660
661    use std::str::FromStr;
662
663    use super::*;
664
665    /// Documented DeepSeek response example, from
666    /// <https://api-docs.deepseek.com/api/create-response>.
667    const DEEPSEEK_RESPONSE_JSON: &str = r#"
668    {
669      "id": "24778070-1c36-4ae0-a4bd-870afc7fc13e",
670      "object": "response",
671      "created_at": 1753000000,
672      "status": "completed",
673      "model": "deepseek-v4-flash",
674      "output": [
675        {
676          "type": "reasoning",
677          "id": "rs_1",
678          "status": "completed",
679          "content": [
680            {
681              "type": "reasoning_text",
682              "text": "The user greets me. I should reply politely."
683            }
684          ],
685          "summary": []
686        },
687        {
688          "type": "message",
689          "id": "msg_1",
690          "status": "completed",
691          "role": "assistant",
692          "content": [
693            {
694              "type": "output_text",
695              "text": "Hello! How can I help you today?",
696              "annotations": []
697            }
698          ]
699        }
700      ],
701      "usage": {
702        "input_tokens": 22,
703        "input_tokens_details": { "cached_tokens": 0 },
704        "output_tokens": 29,
705        "output_tokens_details": { "reasoning_tokens": 27 },
706        "total_tokens": 51
707      },
708      "store": false,
709      "parallel_tool_calls": true,
710      "previous_response_id": null,
711      "error": null,
712      "incomplete_details": null
713    }"#;
714
715    #[test]
716    fn parses_documented_deepseek_response() {
717        let response = Response::from_str(DEEPSEEK_RESPONSE_JSON).unwrap();
718        assert_eq!(response.object, ResponseObject::Response);
719        assert_eq!(response.model, "deepseek-v4-flash");
720        assert!(response.is_completed());
721        assert_eq!(response.store, Some(false));
722        assert_eq!(response.parallel_tool_calls, Some(true));
723        assert_eq!(response.output.len(), 2);
724        assert_eq!(response.output_text(), "Hello! How can I help you today?");
725        assert_eq!(
726            response.reasoning_text(),
727            "The user greets me. I should reply politely."
728        );
729        let usage = response.usage.unwrap();
730        assert_eq!(usage.total_tokens, 51);
731        assert_eq!(usage.input_tokens_details.unwrap().cached_tokens, Some(0));
732        assert_eq!(
733            usage.output_tokens_details.unwrap().reasoning_tokens,
734            Some(27)
735        );
736    }
737
738    #[test]
739    fn unknown_output_item_type_is_preserved() {
740        let json = r#"{
741            "id": "resp_1",
742            "object": "response",
743            "created_at": 0,
744            "status": "completed",
745            "model": "m",
746            "output": [{"type": "something_future"}]
747        }"#;
748        let response = Response::from_str(json).unwrap();
749        assert!(matches!(
750            response.output.first(),
751            Some(ResponseOutputItem::Other)
752        ));
753    }
754}
755
756#[cfg(test)]
757mod integration {
758    //! Integration tests against the DeepSeek and Qwen Responses
759    //! implementations. They self-skip when the API key environment
760    //! variable is absent; `keys/key_env.sh` provides the keys.
761    //!
762    //! All assertions are based on the documented behavior of the two
763    //! providers, not on assumed server responses.
764
765    use futures_util::StreamExt;
766
767    use crate::responses::create::request::{Input, RequestBody};
768    use crate::responses::create::response::ResponseStreamEvent;
769    use crate::rest::{
770        RequestOptions, default_client,
771        delete::DeleteNoStream,
772        get::GetNoStream,
773        post::{PostNoStream, PostStream},
774    };
775
776    use super::{Response, ResponseObject};
777
778    const DEEPSEEK_URL: &str = "https://api.deepseek.com";
779    const DEEPSEEK_MODEL: &str = "deepseek-v4-flash";
780
781    const QWEN_URL: &str = "https://dashscope.aliyuncs.com/compatible-mode/v1";
782    const QWEN_MODEL: &str = "qwen3-max";
783
784    fn deepseek_api_key() -> Option<String> {
785        std::env::var("DEEPSEEK_API_KEY")
786            .ok()
787            .map(|key| key.trim().to_string())
788            .filter(|key| !key.is_empty())
789    }
790
791    fn qwen_api_key() -> Option<String> {
792        std::env::var("QWEN_API_KEY")
793            .ok()
794            .map(|key| key.trim().to_string())
795            .filter(|key| !key.is_empty())
796    }
797
798    async fn run_stream(
799        request: &RequestBody,
800        client: &reqwest::Client,
801        base_url: &str,
802        options: &RequestOptions,
803    ) -> anyhow::Result<Response> {
804        let mut stream = request
805            .get_stream_response(client, base_url, options)
806            .await?;
807
808        let mut final_response: Option<Response> = None;
809        while let Some(event) = stream.next().await {
810            let event: ResponseStreamEvent = event?;
811            println!(
812                "responses stream event: {:?}",
813                std::mem::discriminant(&event)
814            );
815            if let Some(response) = event.final_response() {
816                final_response = Some(response.clone());
817            }
818        }
819
820        let response =
821            final_response.expect("the stream should end with a terminal response event");
822        Ok(response)
823    }
824
825    /// DeepSeek non-streaming create. Documented at
826    /// <https://api-docs.deepseek.com/api/create-response>.
827    #[tokio::test]
828    async fn test_deepseek_response_no_stream() -> Result<(), anyhow::Error> {
829        let Some(api_key) = deepseek_api_key() else {
830            println!("Skipping: set DEEPSEEK_API_KEY to run this test");
831            return Ok(());
832        };
833
834        let request = RequestBody {
835            model: DEEPSEEK_MODEL.to_string(),
836            input: Input::Text("用一句话介绍你自己。".to_string()),
837            instructions: Some("You are a helpful assistant.".to_string()),
838            ..Default::default()
839        };
840
841        let response = request
842            .get_response(
843                &default_client(),
844                DEEPSEEK_URL,
845                &RequestOptions::bearer(&api_key),
846            )
847            .await?;
848
849        println!("deepseek responses no-stream: {response:#?}");
850        assert_eq!(response.object, ResponseObject::Response);
851        assert!(response.is_completed());
852        // DeepSeek's Responses API is stateless: it never stores.
853        assert_eq!(response.store, Some(false));
854        assert!(!response.output_text().is_empty());
855        Ok(())
856    }
857
858    /// DeepSeek streaming create: the stream ends with
859    /// `response.completed` (no `data: [DONE]` sentinel).
860    #[tokio::test]
861    async fn test_deepseek_response_stream() -> Result<(), anyhow::Error> {
862        let Some(api_key) = deepseek_api_key() else {
863            println!("Skipping: set DEEPSEEK_API_KEY to run this test");
864            return Ok(());
865        };
866
867        let request = RequestBody {
868            model: DEEPSEEK_MODEL.to_string(),
869            input: Input::Text("用一句话介绍你自己。".to_string()),
870            stream: Some(true),
871            ..Default::default()
872        };
873
874        let response = run_stream(
875            &request,
876            &default_client(),
877            DEEPSEEK_URL,
878            &RequestOptions::bearer(&api_key),
879        )
880        .await?;
881
882        println!("deepseek responses stream final: {response:#?}");
883        assert!(response.is_completed());
884        assert!(!response.output_text().is_empty());
885        Ok(())
886    }
887
888    /// Qwen non-streaming create. Documented at
889    /// <https://help.aliyun.com/zh/model-studio/qwen-api-via-openai-responses>.
890    #[tokio::test]
891    async fn test_qwen_response_no_stream() -> Result<(), anyhow::Error> {
892        let Some(api_key) = qwen_api_key() else {
893            println!("Skipping: set QWEN_API_KEY to run this test");
894            return Ok(());
895        };
896
897        let request = RequestBody {
898            model: QWEN_MODEL.to_string(),
899            input: Input::Text("用一句话介绍你自己。".to_string()),
900            instructions: Some("You are a helpful assistant.".to_string()),
901            // Do not store this response; the store/retrieve/delete cycle
902            // has its own dedicated test.
903            store: Some(false),
904            ..Default::default()
905        };
906
907        let response = request
908            .get_response(
909                &default_client(),
910                QWEN_URL,
911                &RequestOptions::bearer(&api_key),
912            )
913            .await?;
914
915        println!("qwen responses no-stream: {response:#?}");
916        assert_eq!(response.object, ResponseObject::Response);
917        assert!(response.is_completed());
918        assert!(!response.output_text().is_empty());
919        Ok(())
920    }
921
922    /// Qwen streaming create.
923    #[tokio::test]
924    async fn test_qwen_response_stream() -> Result<(), anyhow::Error> {
925        let Some(api_key) = qwen_api_key() else {
926            println!("Skipping: set QWEN_API_KEY to run this test");
927            return Ok(());
928        };
929
930        let request = RequestBody {
931            model: QWEN_MODEL.to_string(),
932            input: Input::Text("用一句话介绍你自己。".to_string()),
933            stream: Some(true),
934            store: Some(false),
935            ..Default::default()
936        };
937
938        let response = run_stream(
939            &request,
940            &default_client(),
941            QWEN_URL,
942            &RequestOptions::bearer(&api_key),
943        )
944        .await?;
945
946        println!("qwen responses stream final: {response:#?}");
947        assert!(response.is_completed());
948        assert!(!response.output_text().is_empty());
949        Ok(())
950    }
951
952    /// Qwen multi-turn conversation via `previous_response_id`.
953    #[tokio::test]
954    async fn test_qwen_response_previous_response_id() -> Result<(), anyhow::Error> {
955        let Some(api_key) = qwen_api_key() else {
956            println!("Skipping: set QWEN_API_KEY to run this test");
957            return Ok(());
958        };
959
960        let client = default_client();
961
962        // The first response must be stored so that it can be referenced
963        // via `previous_response_id` in the second turn.
964        let first = RequestBody {
965            model: QWEN_MODEL.to_string(),
966            input: Input::Text("法国的首都是哪里?".to_string()),
967            ..Default::default()
968        }
969        .get_response(&client, QWEN_URL, &RequestOptions::bearer(&api_key))
970        .await?;
971        assert!(first.is_completed());
972
973        let second = RequestBody {
974            model: QWEN_MODEL.to_string(),
975            input: Input::Text("它的人口大约是多少?".to_string()),
976            previous_response_id: Some(first.id.clone()),
977            store: Some(false),
978            ..Default::default()
979        }
980        .get_response(&client, QWEN_URL, &RequestOptions::bearer(&api_key))
981        .await?;
982
983        println!("qwen responses second turn: {second:#?}");
984        assert!(second.is_completed());
985        assert_eq!(
986            second.previous_response_id.as_deref(),
987            Some(first.id.as_str())
988        );
989        assert!(!second.output_text().is_empty());
990        Ok(())
991    }
992
993    /// Qwen store / retrieve / delete cycle.
994    #[tokio::test]
995    async fn test_qwen_response_store_retrieve_delete() -> Result<(), anyhow::Error> {
996        let Some(api_key) = qwen_api_key() else {
997            println!("Skipping: set QWEN_API_KEY to run this test");
998            return Ok(());
999        };
1000
1001        let client = default_client();
1002
1003        let created = RequestBody {
1004            model: QWEN_MODEL.to_string(),
1005            input: Input::Text("说一个法语单词。".to_string()),
1006            store: Some(true),
1007            ..Default::default()
1008        }
1009        .get_response(&client, QWEN_URL, &RequestOptions::bearer(&api_key))
1010        .await?;
1011        assert!(created.is_completed());
1012
1013        // Retrieve the stored response.
1014        let retrieved = crate::responses::retrieve::RetrieveRequest {
1015            response_id: &created.id,
1016        }
1017        .get_response(&client, QWEN_URL, &RequestOptions::bearer(&api_key))
1018        .await?;
1019        assert_eq!(retrieved.id, created.id);
1020        assert_eq!(retrieved.output_text(), created.output_text());
1021
1022        // Delete it again.
1023        let deleted = crate::responses::delete::DeleteRequest {
1024            response_id: &created.id,
1025        }
1026        .get_response(&client, QWEN_URL, &RequestOptions::bearer(&api_key))
1027        .await?;
1028        assert_eq!(deleted.id, created.id);
1029        assert!(deleted.deleted);
1030
1031        Ok(())
1032    }
1033}