1use super::{Client, responses_api::streaming::StreamingCompletionResponse};
11use super::{ImageUrl, InputAudio, SystemContent};
12use crate::completion::CompletionError;
13use crate::json_utils;
14use crate::message::{AudioMediaType, Document, DocumentSourceKind, MessageError, MimeType, Text};
15use crate::one_or_many::string_or_one_or_many;
16
17use crate::{OneOrMany, completion, message};
18use serde::{Deserialize, Serialize};
19use serde_json::{Map, Value};
20
21use std::convert::Infallible;
22use std::ops::Add;
23use std::str::FromStr;
24
25pub mod streaming;
26
27#[derive(Debug, Deserialize, Serialize, Clone)]
30pub struct CompletionRequest {
31 pub input: OneOrMany<InputItem>,
33 pub model: String,
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub instructions: Option<String>,
38 #[serde(skip_serializing_if = "Option::is_none")]
40 pub max_output_tokens: Option<u64>,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub stream: Option<bool>,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub temperature: Option<f64>,
47 #[serde(skip_serializing_if = "Vec::is_empty")]
51 pub tools: Vec<ResponsesToolDefinition>,
52 #[serde(flatten)]
54 pub additional_parameters: AdditionalParameters,
55}
56
57impl CompletionRequest {
58 pub fn with_structured_outputs<S>(mut self, schema_name: S, schema: serde_json::Value) -> Self
59 where
60 S: Into<String>,
61 {
62 self.additional_parameters.text = Some(TextConfig::structured_output(schema_name, schema));
63
64 self
65 }
66
67 pub fn with_reasoning(mut self, reasoning: Reasoning) -> Self {
68 self.additional_parameters.reasoning = Some(reasoning);
69
70 self
71 }
72}
73
74#[derive(Debug, Deserialize, Serialize, Clone)]
76pub struct InputItem {
77 #[serde(skip_serializing_if = "Option::is_none")]
81 role: Option<Role>,
82 #[serde(flatten)]
84 input: InputContent,
85}
86
87#[derive(Debug, Deserialize, Serialize, Clone)]
89#[serde(rename_all = "lowercase")]
90pub enum Role {
91 User,
92 Assistant,
93 System,
94}
95
96#[derive(Debug, Deserialize, Serialize, Clone)]
98#[serde(tag = "type", rename_all = "snake_case")]
99pub enum InputContent {
100 Message(Message),
101 Reasoning(OpenAIReasoning),
102 FunctionCall(OutputFunctionCall),
103 FunctionCallOutput(ToolResult),
104}
105
106#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
107pub struct OpenAIReasoning {
108 id: String,
109 pub summary: Vec<ReasoningSummary>,
110 pub encrypted_content: Option<String>,
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub status: Option<ToolStatus>,
113}
114
115#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
116#[serde(tag = "type", rename_all = "snake_case")]
117pub enum ReasoningSummary {
118 SummaryText { text: String },
119}
120
121impl ReasoningSummary {
122 fn new(input: &str) -> Self {
123 Self::SummaryText {
124 text: input.to_string(),
125 }
126 }
127
128 pub fn text(&self) -> String {
129 let ReasoningSummary::SummaryText { text } = self;
130 text.clone()
131 }
132}
133
134#[derive(Debug, Deserialize, Serialize, Clone)]
136pub struct ToolResult {
137 call_id: String,
139 output: String,
141 status: ToolStatus,
143}
144
145impl From<Message> for InputItem {
146 fn from(value: Message) -> Self {
147 match value {
148 Message::User { .. } => Self {
149 role: Some(Role::User),
150 input: InputContent::Message(value),
151 },
152 Message::Assistant { ref content, .. } => {
153 let role = if content
154 .clone()
155 .iter()
156 .any(|x| matches!(x, AssistantContentType::Reasoning(_)))
157 {
158 None
159 } else {
160 Some(Role::Assistant)
161 };
162 Self {
163 role,
164 input: InputContent::Message(value),
165 }
166 }
167 Message::System { .. } => Self {
168 role: Some(Role::System),
169 input: InputContent::Message(value),
170 },
171 Message::ToolResult {
172 tool_call_id,
173 output,
174 } => Self {
175 role: None,
176 input: InputContent::FunctionCallOutput(ToolResult {
177 call_id: tool_call_id,
178 output,
179 status: ToolStatus::Completed,
180 }),
181 },
182 }
183 }
184}
185
186impl TryFrom<crate::completion::Message> for Vec<InputItem> {
187 type Error = CompletionError;
188
189 fn try_from(value: crate::completion::Message) -> Result<Self, Self::Error> {
190 match value {
191 crate::completion::Message::User { content } => {
192 let mut items = Vec::new();
193
194 for user_content in content {
195 match user_content {
196 crate::message::UserContent::Text(Text { text }) => {
197 items.push(InputItem {
198 role: Some(Role::User),
199 input: InputContent::Message(Message::User {
200 content: OneOrMany::one(UserContent::InputText { text }),
201 name: None,
202 }),
203 });
204 }
205 crate::message::UserContent::ToolResult(
206 crate::completion::message::ToolResult {
207 call_id,
208 content: tool_content,
209 ..
210 },
211 ) => {
212 for tool_result_content in tool_content {
213 let crate::completion::message::ToolResultContent::Text(Text {
214 text,
215 }) = tool_result_content
216 else {
217 return Err(CompletionError::ProviderError(
218 "This thing only supports text!".to_string(),
219 ));
220 };
221 items.push(InputItem {
223 role: None,
224 input: InputContent::FunctionCallOutput(ToolResult {
225 call_id: call_id
226 .clone()
227 .expect("The call ID of this tool should exist!"),
228 output: text,
229 status: ToolStatus::Completed,
230 }),
231 });
232 }
233 }
234 crate::message::UserContent::Document(Document { data, .. }) => {
236 items.push(InputItem {
237 role: Some(Role::User),
238 input: InputContent::Message(Message::User {
239 content: OneOrMany::one(UserContent::InputText { text: data }),
240 name: None,
241 }),
242 })
243 }
244 crate::message::UserContent::Image(crate::message::Image {
245 data,
246 media_type,
247 detail,
248 ..
249 }) => {
250 let url = match data {
251 DocumentSourceKind::Base64(data) => {
252 let media_type = if let Some(media_type) = media_type {
253 media_type.to_mime_type().to_string()
254 } else {
255 String::new()
256 };
257 format!("data:{media_type};base64,{data}")
258 }
259 DocumentSourceKind::Url(url) => url,
260 DocumentSourceKind::Unknown => return Err(CompletionError::RequestError("Attempted to create an OpenAI Responses AI image input from unknown variant".into()))
261 };
262 items.push(InputItem {
263 role: Some(Role::User),
264 input: InputContent::Message(Message::User {
265 content: OneOrMany::one(UserContent::InputImage {
266 image_url: ImageUrl {
267 url,
268 detail: detail.unwrap_or_default(),
269 },
270 }),
271 name: None,
272 }),
273 });
274 }
275 message => {
276 return Err(CompletionError::ProviderError(format!(
277 "Unsupported message: {message:?}"
278 )));
279 }
280 }
281 }
282
283 Ok(items)
284 }
285 crate::completion::Message::Assistant { id, content } => {
286 let mut items = Vec::new();
287
288 for assistant_content in content {
289 match assistant_content {
290 crate::message::AssistantContent::Text(Text { text }) => {
291 let id = id.as_ref().unwrap_or(&String::default()).clone();
292 items.push(InputItem {
293 role: Some(Role::Assistant),
294 input: InputContent::Message(Message::Assistant {
295 content: OneOrMany::one(AssistantContentType::Text(
296 AssistantContent::OutputText(Text { text }),
297 )),
298 id,
299 name: None,
300 status: ToolStatus::Completed,
301 }),
302 });
303 }
304 crate::message::AssistantContent::ToolCall(crate::message::ToolCall {
305 id: tool_id,
306 call_id,
307 function,
308 }) => {
309 items.push(InputItem {
310 role: None,
311 input: InputContent::FunctionCall(OutputFunctionCall {
312 arguments: function.arguments,
313 call_id: call_id.expect("The tool call ID should exist!"),
314 id: tool_id,
315 name: function.name,
316 status: ToolStatus::Completed,
317 }),
318 });
319 }
320 crate::message::AssistantContent::Reasoning(
321 crate::message::Reasoning { id, reasoning },
322 ) => {
323 items.push(InputItem {
324 role: None,
325 input: InputContent::Reasoning(OpenAIReasoning {
326 id: id
327 .expect("An OpenAI-generated ID is required when using OpenAI reasoning items"),
328 summary: reasoning.into_iter().map(|x| ReasoningSummary::new(&x)).collect(),
329 encrypted_content: None,
330 status: None,
331 }),
332 });
333 }
334 }
335 }
336
337 Ok(items)
338 }
339 }
340 }
341}
342
343impl From<OneOrMany<String>> for Vec<ReasoningSummary> {
344 fn from(value: OneOrMany<String>) -> Self {
345 value.iter().map(|x| ReasoningSummary::new(x)).collect()
346 }
347}
348
349#[derive(Debug, Deserialize, Serialize, Clone)]
351pub struct ResponsesToolDefinition {
352 pub name: String,
354 pub parameters: serde_json::Value,
356 pub strict: bool,
358 #[serde(rename = "type")]
360 pub kind: String,
361 pub description: String,
363}
364
365fn add_props_false(schema: &mut serde_json::Value) {
369 if let Value::Object(obj) = schema {
370 let is_object_schema = obj.get("type") == Some(&Value::String("object".to_string()))
371 || obj.contains_key("properties");
372
373 if is_object_schema && !obj.contains_key("additionalProperties") {
374 obj.insert("additionalProperties".to_string(), Value::Bool(false));
375 }
376
377 if let Some(defs) = obj.get_mut("$defs")
378 && let Value::Object(defs_obj) = defs
379 {
380 for (_, def_schema) in defs_obj.iter_mut() {
381 add_props_false(def_schema);
382 }
383 }
384
385 if let Some(properties) = obj.get_mut("properties")
386 && let Value::Object(props) = properties
387 {
388 for (_, prop_value) in props.iter_mut() {
389 add_props_false(prop_value);
390 }
391 }
392
393 if let Some(items) = obj.get_mut("items") {
394 add_props_false(items);
395 }
396
397 for key in ["anyOf", "oneOf", "allOf"] {
399 if let Some(variants) = obj.get_mut(key)
400 && let Value::Array(variants_array) = variants
401 {
402 for variant in variants_array.iter_mut() {
403 add_props_false(variant);
404 }
405 }
406 }
407 }
408}
409
410impl From<completion::ToolDefinition> for ResponsesToolDefinition {
411 fn from(value: completion::ToolDefinition) -> Self {
412 let completion::ToolDefinition {
413 name,
414 mut parameters,
415 description,
416 } = value;
417
418 add_props_false(&mut parameters);
419
420 Self {
421 name,
422 parameters,
423 description,
424 kind: "function".to_string(),
425 strict: true,
426 }
427 }
428}
429
430#[derive(Clone, Debug, Serialize, Deserialize)]
433pub struct ResponsesUsage {
434 pub input_tokens: u64,
436 #[serde(skip_serializing_if = "Option::is_none")]
438 pub input_tokens_details: Option<InputTokensDetails>,
439 pub output_tokens: u64,
441 pub output_tokens_details: OutputTokensDetails,
443 pub total_tokens: u64,
445}
446
447impl ResponsesUsage {
448 pub(crate) fn new() -> Self {
450 Self {
451 input_tokens: 0,
452 input_tokens_details: Some(InputTokensDetails::new()),
453 output_tokens: 0,
454 output_tokens_details: OutputTokensDetails::new(),
455 total_tokens: 0,
456 }
457 }
458}
459
460impl Add for ResponsesUsage {
461 type Output = Self;
462
463 fn add(self, rhs: Self) -> Self::Output {
464 let input_tokens = self.input_tokens + rhs.input_tokens;
465 let input_tokens_details = self.input_tokens_details.map(|lhs| {
466 if let Some(tokens) = rhs.input_tokens_details {
467 lhs + tokens
468 } else {
469 lhs
470 }
471 });
472 let output_tokens = self.output_tokens + rhs.output_tokens;
473 let output_tokens_details = self.output_tokens_details + rhs.output_tokens_details;
474 let total_tokens = self.total_tokens + rhs.total_tokens;
475 Self {
476 input_tokens,
477 input_tokens_details,
478 output_tokens,
479 output_tokens_details,
480 total_tokens,
481 }
482 }
483}
484
485#[derive(Clone, Debug, Serialize, Deserialize)]
487pub struct InputTokensDetails {
488 pub cached_tokens: u64,
490}
491
492impl InputTokensDetails {
493 pub(crate) fn new() -> Self {
494 Self { cached_tokens: 0 }
495 }
496}
497
498impl Add for InputTokensDetails {
499 type Output = Self;
500 fn add(self, rhs: Self) -> Self::Output {
501 Self {
502 cached_tokens: self.cached_tokens + rhs.cached_tokens,
503 }
504 }
505}
506
507#[derive(Clone, Debug, Serialize, Deserialize)]
509pub struct OutputTokensDetails {
510 pub reasoning_tokens: u64,
512}
513
514impl OutputTokensDetails {
515 pub(crate) fn new() -> Self {
516 Self {
517 reasoning_tokens: 0,
518 }
519 }
520}
521
522impl Add for OutputTokensDetails {
523 type Output = Self;
524 fn add(self, rhs: Self) -> Self::Output {
525 Self {
526 reasoning_tokens: self.reasoning_tokens + rhs.reasoning_tokens,
527 }
528 }
529}
530
531#[derive(Clone, Debug, Default, Serialize, Deserialize)]
533pub struct IncompleteDetailsReason {
534 pub reason: String,
536}
537
538#[derive(Clone, Debug, Default, Serialize, Deserialize)]
540pub struct ResponseError {
541 pub code: String,
543 pub message: String,
545}
546
547#[derive(Clone, Debug, Deserialize, Serialize)]
549#[serde(rename_all = "snake_case")]
550pub enum ResponseObject {
551 Response,
552}
553
554#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
556#[serde(rename_all = "snake_case")]
557pub enum ResponseStatus {
558 InProgress,
559 Completed,
560 Failed,
561 Cancelled,
562 Queued,
563 Incomplete,
564}
565
566impl TryFrom<(String, crate::completion::CompletionRequest)> for CompletionRequest {
568 type Error = CompletionError;
569 fn try_from(
570 (model, req): (String, crate::completion::CompletionRequest),
571 ) -> Result<Self, Self::Error> {
572 let input = {
573 let mut partial_history = vec![];
574 if let Some(docs) = req.normalized_documents() {
575 partial_history.push(docs);
576 }
577 partial_history.extend(req.chat_history);
578
579 let mut full_history: Vec<InputItem> = Vec::new();
581
582 full_history.extend(
584 partial_history
585 .into_iter()
586 .map(|x| <Vec<InputItem>>::try_from(x).unwrap())
587 .collect::<Vec<Vec<InputItem>>>()
588 .into_iter()
589 .flatten()
590 .collect::<Vec<InputItem>>(),
591 );
592
593 full_history
594 };
595
596 let input = OneOrMany::many(input)
597 .expect("This should never panic - if it does, please file a bug report");
598
599 let stream = req
600 .additional_params
601 .clone()
602 .unwrap_or(Value::Null)
603 .as_bool();
604
605 let additional_parameters = if let Some(map) = req.additional_params {
606 serde_json::from_value::<AdditionalParameters>(map).expect("Converting additional parameters to AdditionalParameters should never fail as every field is an Option")
607 } else {
608 AdditionalParameters::default()
610 };
611
612 Ok(Self {
613 input,
614 model,
615 instructions: req.preamble,
616 max_output_tokens: req.max_tokens,
617 stream,
618 tools: req
619 .tools
620 .into_iter()
621 .map(ResponsesToolDefinition::from)
622 .collect(),
623 temperature: req.temperature,
624 additional_parameters,
625 })
626 }
627}
628
629#[derive(Clone)]
631pub struct ResponsesCompletionModel {
632 pub(crate) client: Client,
634 pub model: String,
636}
637
638impl ResponsesCompletionModel {
639 pub fn new(client: Client, model: &str) -> Self {
641 Self {
642 client,
643 model: model.to_string(),
644 }
645 }
646
647 pub fn completions_api(self) -> crate::providers::openai::completion::CompletionModel {
649 crate::providers::openai::completion::CompletionModel::new(self.client, &self.model)
650 }
651
652 pub(crate) fn create_completion_request(
654 &self,
655 completion_request: crate::completion::CompletionRequest,
656 ) -> Result<CompletionRequest, CompletionError> {
657 let req = CompletionRequest::try_from((self.model.clone(), completion_request))?;
658
659 Ok(req)
660 }
661}
662
663#[derive(Clone, Debug, Serialize, Deserialize)]
665pub struct CompletionResponse {
666 pub id: String,
668 pub object: ResponseObject,
670 pub created_at: u64,
672 pub status: ResponseStatus,
674 pub error: Option<ResponseError>,
676 pub incomplete_details: Option<IncompleteDetailsReason>,
678 pub instructions: Option<String>,
680 pub max_output_tokens: Option<u64>,
682 pub model: String,
684 pub usage: Option<ResponsesUsage>,
686 pub output: Vec<Output>,
688 pub tools: Vec<ResponsesToolDefinition>,
690 #[serde(flatten)]
692 pub additional_parameters: AdditionalParameters,
693}
694
695#[derive(Clone, Debug, Deserialize, Serialize, Default)]
698pub struct AdditionalParameters {
699 #[serde(skip_serializing_if = "Option::is_none")]
701 pub background: Option<bool>,
702 #[serde(skip_serializing_if = "Option::is_none")]
704 pub text: Option<TextConfig>,
705 #[serde(skip_serializing_if = "Option::is_none")]
707 pub include: Option<Vec<Include>>,
708 #[serde(skip_serializing_if = "Option::is_none")]
710 pub top_p: Option<f64>,
711 #[serde(skip_serializing_if = "Option::is_none")]
713 pub truncation: Option<TruncationStrategy>,
714 #[serde(skip_serializing_if = "Option::is_none")]
716 pub user: Option<String>,
717 #[serde(skip_serializing_if = "Map::is_empty", default)]
719 pub metadata: serde_json::Map<String, serde_json::Value>,
720 #[serde(skip_serializing_if = "Option::is_none")]
722 pub parallel_tool_calls: Option<bool>,
723 #[serde(skip_serializing_if = "Option::is_none")]
725 pub previous_response_id: Option<String>,
726 #[serde(skip_serializing_if = "Option::is_none")]
728 pub reasoning: Option<Reasoning>,
729 #[serde(skip_serializing_if = "Option::is_none")]
731 pub service_tier: Option<OpenAIServiceTier>,
732 #[serde(skip_serializing_if = "Option::is_none")]
734 pub store: Option<bool>,
735}
736
737impl AdditionalParameters {
738 pub fn to_json(self) -> serde_json::Value {
739 serde_json::to_value(self).expect("this should never fail since a struct that impls Deserialize will always be valid JSON")
740 }
741}
742
743#[derive(Clone, Debug, Default, Serialize, Deserialize)]
747#[serde(rename_all = "snake_case")]
748pub enum TruncationStrategy {
749 Auto,
750 #[default]
751 Disabled,
752}
753
754#[derive(Clone, Debug, Serialize, Deserialize)]
757pub struct TextConfig {
758 pub format: TextFormat,
759}
760
761impl TextConfig {
762 pub(crate) fn structured_output<S>(name: S, schema: serde_json::Value) -> Self
763 where
764 S: Into<String>,
765 {
766 Self {
767 format: TextFormat::JsonSchema(StructuredOutputsInput {
768 name: name.into(),
769 schema,
770 strict: true,
771 }),
772 }
773 }
774}
775
776#[derive(Clone, Debug, Serialize, Deserialize, Default)]
779#[serde(tag = "type")]
780#[serde(rename_all = "snake_case")]
781pub enum TextFormat {
782 JsonSchema(StructuredOutputsInput),
783 #[default]
784 Text,
785}
786
787#[derive(Clone, Debug, Serialize, Deserialize)]
789pub struct StructuredOutputsInput {
790 pub name: String,
792 pub schema: serde_json::Value,
794 pub strict: bool,
796}
797
798#[derive(Clone, Debug, Default, Serialize, Deserialize)]
800pub struct Reasoning {
801 pub effort: Option<ReasoningEffort>,
803 #[serde(skip_serializing_if = "Option::is_none")]
805 pub summary: Option<ReasoningSummaryLevel>,
806}
807
808impl Reasoning {
809 pub fn new() -> Self {
811 Self {
812 effort: None,
813 summary: None,
814 }
815 }
816
817 pub fn with_effort(mut self, reasoning_effort: ReasoningEffort) -> Self {
819 self.effort = Some(reasoning_effort);
820
821 self
822 }
823
824 pub fn with_summary_level(mut self, reasoning_summary_level: ReasoningSummaryLevel) -> Self {
826 self.summary = Some(reasoning_summary_level);
827
828 self
829 }
830}
831
832#[derive(Clone, Debug, Default, Serialize, Deserialize)]
834#[serde(rename_all = "snake_case")]
835pub enum OpenAIServiceTier {
836 #[default]
837 Auto,
838 Default,
839 Flex,
840}
841
842#[derive(Clone, Debug, Default, Serialize, Deserialize)]
844#[serde(rename_all = "snake_case")]
845pub enum ReasoningEffort {
846 Minimal,
847 Low,
848 #[default]
849 Medium,
850 High,
851}
852
853#[derive(Clone, Debug, Default, Serialize, Deserialize)]
855#[serde(rename_all = "snake_case")]
856pub enum ReasoningSummaryLevel {
857 #[default]
858 Auto,
859 Concise,
860 Detailed,
861}
862
863#[derive(Clone, Debug, Deserialize, Serialize)]
866pub enum Include {
867 #[serde(rename = "file_search_call.results")]
868 FileSearchCallResults,
869 #[serde(rename = "message.input_image.image_url")]
870 MessageInputImageImageUrl,
871 #[serde(rename = "computer_call.output.image_url")]
872 ComputerCallOutputOutputImageUrl,
873 #[serde(rename = "reasoning.encrypted_content")]
874 ReasoningEncryptedContent,
875 #[serde(rename = "code_interpreter_call.outputs")]
876 CodeInterpreterCallOutputs,
877}
878
879#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
881#[serde(tag = "type")]
882#[serde(rename_all = "snake_case")]
883pub enum Output {
884 Message(OutputMessage),
885 #[serde(alias = "function_call")]
886 FunctionCall(OutputFunctionCall),
887 Reasoning {
888 id: String,
889 summary: Vec<ReasoningSummary>,
890 },
891}
892
893impl From<Output> for Vec<completion::AssistantContent> {
894 fn from(value: Output) -> Self {
895 let res: Vec<completion::AssistantContent> = match value {
896 Output::Message(OutputMessage { content, .. }) => content
897 .into_iter()
898 .map(completion::AssistantContent::from)
899 .collect(),
900 Output::FunctionCall(OutputFunctionCall {
901 id,
902 arguments,
903 call_id,
904 name,
905 ..
906 }) => vec![completion::AssistantContent::tool_call_with_call_id(
907 id, call_id, name, arguments,
908 )],
909 Output::Reasoning { id, summary } => {
910 let summary: Vec<String> = summary.into_iter().map(|x| x.text()).collect();
911
912 vec![completion::AssistantContent::Reasoning(
913 message::Reasoning::multi(summary).with_id(id),
914 )]
915 }
916 };
917
918 res
919 }
920}
921
922#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
923pub struct OutputReasoning {
924 id: String,
925 summary: Vec<ReasoningSummary>,
926 status: ToolStatus,
927}
928
929#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
931pub struct OutputFunctionCall {
932 pub id: String,
933 #[serde(with = "json_utils::stringified_json")]
934 pub arguments: serde_json::Value,
935 pub call_id: String,
936 pub name: String,
937 pub status: ToolStatus,
938}
939
940#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
942#[serde(rename_all = "snake_case")]
943pub enum ToolStatus {
944 InProgress,
945 Completed,
946 Incomplete,
947}
948
949#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
951pub struct OutputMessage {
952 pub id: String,
954 pub role: OutputRole,
956 pub status: ResponseStatus,
958 pub content: Vec<AssistantContent>,
960}
961
962#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
964#[serde(rename_all = "snake_case")]
965pub enum OutputRole {
966 Assistant,
967}
968
969impl completion::CompletionModel for ResponsesCompletionModel {
970 type Response = CompletionResponse;
971 type StreamingResponse = StreamingCompletionResponse;
972
973 #[cfg_attr(feature = "worker", worker::send)]
974 async fn completion(
975 &self,
976 completion_request: crate::completion::CompletionRequest,
977 ) -> Result<completion::CompletionResponse<Self::Response>, CompletionError> {
978 let request = self.create_completion_request(completion_request)?;
979 let request = serde_json::to_value(request)?;
980
981 tracing::debug!("OpenAI input: {}", serde_json::to_string_pretty(&request)?);
982
983 let response = self.client.post("/responses").json(&request).send().await?;
984
985 if response.status().is_success() {
986 let t = response.text().await?;
987 tracing::debug!(target: "rig", "OpenAI response: {}", t);
988
989 let response = serde_json::from_str::<Self::Response>(&t)?;
990 response.try_into()
991 } else {
992 Err(CompletionError::ProviderError(response.text().await?))
993 }
994 }
995
996 #[cfg_attr(feature = "worker", worker::send)]
997 async fn stream(
998 &self,
999 request: crate::completion::CompletionRequest,
1000 ) -> Result<
1001 crate::streaming::StreamingCompletionResponse<Self::StreamingResponse>,
1002 CompletionError,
1003 > {
1004 Self::stream(self, request).await
1005 }
1006}
1007
1008impl TryFrom<CompletionResponse> for completion::CompletionResponse<CompletionResponse> {
1009 type Error = CompletionError;
1010
1011 fn try_from(response: CompletionResponse) -> Result<Self, Self::Error> {
1012 if response.output.is_empty() {
1013 return Err(CompletionError::ResponseError(
1014 "Response contained no parts".to_owned(),
1015 ));
1016 }
1017
1018 let content: Vec<completion::AssistantContent> = response
1019 .output
1020 .iter()
1021 .cloned()
1022 .flat_map(<Vec<completion::AssistantContent>>::from)
1023 .collect();
1024
1025 let choice = OneOrMany::many(content).map_err(|_| {
1026 CompletionError::ResponseError(
1027 "Response contained no message or tool call (empty)".to_owned(),
1028 )
1029 })?;
1030
1031 let usage = response
1032 .usage
1033 .as_ref()
1034 .map(|usage| completion::Usage {
1035 input_tokens: usage.input_tokens,
1036 output_tokens: usage.output_tokens,
1037 total_tokens: usage.total_tokens,
1038 })
1039 .unwrap_or_default();
1040
1041 Ok(completion::CompletionResponse {
1042 choice,
1043 usage,
1044 raw_response: response,
1045 })
1046 }
1047}
1048
1049#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1051#[serde(tag = "role", rename_all = "lowercase")]
1052pub enum Message {
1053 #[serde(alias = "developer")]
1054 System {
1055 #[serde(deserialize_with = "string_or_one_or_many")]
1056 content: OneOrMany<SystemContent>,
1057 #[serde(skip_serializing_if = "Option::is_none")]
1058 name: Option<String>,
1059 },
1060 User {
1061 #[serde(deserialize_with = "string_or_one_or_many")]
1062 content: OneOrMany<UserContent>,
1063 #[serde(skip_serializing_if = "Option::is_none")]
1064 name: Option<String>,
1065 },
1066 Assistant {
1067 content: OneOrMany<AssistantContentType>,
1068 #[serde(skip_serializing_if = "String::is_empty")]
1069 id: String,
1070 #[serde(skip_serializing_if = "Option::is_none")]
1071 name: Option<String>,
1072 status: ToolStatus,
1073 },
1074 #[serde(rename = "tool")]
1075 ToolResult {
1076 tool_call_id: String,
1077 output: String,
1078 },
1079}
1080
1081#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)]
1083#[serde(rename_all = "lowercase")]
1084pub enum ToolResultContentType {
1085 #[default]
1086 Text,
1087}
1088
1089impl Message {
1090 pub fn system(content: &str) -> Self {
1091 Message::System {
1092 content: OneOrMany::one(content.to_owned().into()),
1093 name: None,
1094 }
1095 }
1096}
1097
1098#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1101#[serde(tag = "type", rename_all = "snake_case")]
1102pub enum AssistantContent {
1103 OutputText(Text),
1104 Refusal { refusal: String },
1105}
1106
1107impl From<AssistantContent> for completion::AssistantContent {
1108 fn from(value: AssistantContent) -> Self {
1109 match value {
1110 AssistantContent::Refusal { refusal } => {
1111 completion::AssistantContent::Text(Text { text: refusal })
1112 }
1113 AssistantContent::OutputText(Text { text }) => {
1114 completion::AssistantContent::Text(Text { text })
1115 }
1116 }
1117 }
1118}
1119
1120#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1122#[serde(untagged)]
1123pub enum AssistantContentType {
1124 Text(AssistantContent),
1125 ToolCall(OutputFunctionCall),
1126 Reasoning(OpenAIReasoning),
1127}
1128
1129#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1131#[serde(tag = "type", rename_all = "snake_case")]
1132pub enum UserContent {
1133 InputText {
1134 text: String,
1135 },
1136 InputImage {
1137 image_url: ImageUrl,
1138 },
1139 Audio {
1140 input_audio: InputAudio,
1141 },
1142 #[serde(rename = "tool")]
1143 ToolResult {
1144 tool_call_id: String,
1145 output: String,
1146 },
1147}
1148
1149impl TryFrom<message::Message> for Vec<Message> {
1150 type Error = message::MessageError;
1151
1152 fn try_from(message: message::Message) -> Result<Self, Self::Error> {
1153 match message {
1154 message::Message::User { content } => {
1155 let (tool_results, other_content): (Vec<_>, Vec<_>) = content
1156 .into_iter()
1157 .partition(|content| matches!(content, message::UserContent::ToolResult(_)));
1158
1159 if !tool_results.is_empty() {
1162 tool_results
1163 .into_iter()
1164 .map(|content| match content {
1165 message::UserContent::ToolResult(message::ToolResult {
1166 call_id,
1167 content,
1168 ..
1169 }) => Ok::<_, message::MessageError>(Message::ToolResult {
1170 tool_call_id: call_id.expect("The tool call ID should exist"),
1171 output: {
1172 let res = content.first();
1173 match res {
1174 completion::message::ToolResultContent::Text(Text {
1175 text,
1176 }) => text,
1177 _ => return Err(MessageError::ConversionError("This API only currently supports text tool results".into()))
1178 }
1179 },
1180 }),
1181 _ => unreachable!(),
1182 })
1183 .collect::<Result<Vec<_>, _>>()
1184 } else {
1185 let other_content = other_content
1186 .into_iter()
1187 .map(|content| match content {
1188 message::UserContent::Text(message::Text { text }) => {
1189 Ok(UserContent::InputText { text })
1190 }
1191 message::UserContent::Image(message::Image {
1192 data,
1193 detail,
1194 media_type,
1195 ..
1196 }) => {
1197 let url = match data {
1198 DocumentSourceKind::Base64(data) => {
1199 let media_type = if let Some(media_type) = media_type {
1200 media_type.to_mime_type().to_string()
1201 } else {
1202 String::new()
1203 };
1204 format!("data:{media_type};base64,{data}")
1205 }
1206 DocumentSourceKind::Url(url) => url,
1207 DocumentSourceKind::Unknown => return Err(MessageError::ConversionError("Attempted to convert unknown image type to OpenAI image input".to_string()))
1208 };
1209
1210 Ok(UserContent::InputImage {
1211 image_url: ImageUrl {
1212 url,
1213 detail: detail.unwrap_or_default(),
1214 },
1215 })
1216 }
1217 message::UserContent::Document(message::Document { data, .. }) => {
1218 Ok(UserContent::InputText { text: data })
1219 }
1220 message::UserContent::Audio(message::Audio {
1221 data,
1222 media_type,
1223 ..
1224 }) => Ok(UserContent::Audio {
1225 input_audio: InputAudio {
1226 data,
1227 format: match media_type {
1228 Some(media_type) => media_type,
1229 None => AudioMediaType::MP3,
1230 },
1231 },
1232 }),
1233 _ => unreachable!(),
1234 })
1235 .collect::<Result<Vec<_>, _>>()?;
1236
1237 let other_content = OneOrMany::many(other_content).expect(
1238 "There must be other content here if there were no tool result content",
1239 );
1240
1241 Ok(vec![Message::User {
1242 content: other_content,
1243 name: None,
1244 }])
1245 }
1246 }
1247 message::Message::Assistant { content, id } => {
1248 let assistant_message_id = id;
1249
1250 match content.first() {
1251 crate::message::AssistantContent::Text(Text { text }) => {
1252 Ok(vec![Message::Assistant {
1253 id: assistant_message_id
1254 .expect("The assistant message ID should exist"),
1255 status: ToolStatus::Completed,
1256 content: OneOrMany::one(AssistantContentType::Text(
1257 AssistantContent::OutputText(Text { text }),
1258 )),
1259 name: None,
1260 }])
1261 }
1262 crate::message::AssistantContent::ToolCall(crate::message::ToolCall {
1263 id,
1264 call_id,
1265 function,
1266 }) => Ok(vec![Message::Assistant {
1267 content: OneOrMany::one(AssistantContentType::ToolCall(
1268 OutputFunctionCall {
1269 call_id: call_id.expect("The call ID should exist"),
1270 arguments: function.arguments,
1271 id,
1272 name: function.name,
1273 status: ToolStatus::Completed,
1274 },
1275 )),
1276 id: assistant_message_id.expect("The assistant message ID should exist!"),
1277 name: None,
1278 status: ToolStatus::Completed,
1279 }]),
1280 crate::message::AssistantContent::Reasoning(crate::message::Reasoning {
1281 id,
1282 reasoning,
1283 }) => Ok(vec![Message::Assistant {
1284 content: OneOrMany::one(AssistantContentType::Reasoning(OpenAIReasoning {
1285 id: id.expect("An OpenAI-generated ID is required when using OpenAI reasoning items"),
1286 summary: reasoning.into_iter().map(|x| ReasoningSummary::SummaryText { text: x }).collect(),
1287 encrypted_content: None,
1288 status: Some(ToolStatus::Completed),
1289 })),
1290 id: assistant_message_id.expect("The assistant message ID should exist!"),
1291 name: None,
1292 status: (ToolStatus::Completed),
1293 }]),
1294 }
1295 }
1296 }
1297 }
1298}
1299
1300impl FromStr for UserContent {
1301 type Err = Infallible;
1302
1303 fn from_str(s: &str) -> Result<Self, Self::Err> {
1304 Ok(UserContent::InputText {
1305 text: s.to_string(),
1306 })
1307 }
1308}