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