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