1use super::completion::ToolChoice;
11use super::{Client, responses_api::streaming::StreamingCompletionResponse};
12use super::{InputAudio, SystemContent};
13use crate::completion::CompletionError;
14use crate::http_client;
15use crate::http_client::HttpClientExt;
16use crate::json_utils;
17use crate::message::{
18 AudioMediaType, Document, DocumentMediaType, DocumentSourceKind, ImageDetail, MessageError,
19 MimeType, Text,
20};
21use crate::one_or_many::string_or_one_or_many;
22
23use crate::wasm_compat::{WasmCompatSend, WasmCompatSync};
24use crate::{OneOrMany, completion, message};
25use serde::{Deserialize, Serialize};
26use serde_json::{Map, Value};
27use tracing::{Instrument, Level, enabled, info_span};
28
29use std::convert::Infallible;
30use std::ops::Add;
31use std::str::FromStr;
32
33pub mod streaming;
34
35#[derive(Debug, Deserialize, Serialize, Clone)]
38pub struct CompletionRequest {
39 pub input: OneOrMany<InputItem>,
41 pub model: String,
43 #[serde(skip_serializing_if = "Option::is_none")]
45 pub instructions: Option<String>,
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub max_output_tokens: Option<u64>,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub stream: Option<bool>,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub temperature: Option<f64>,
55 #[serde(skip_serializing_if = "Option::is_none")]
58 tool_choice: Option<ToolChoice>,
59 #[serde(skip_serializing_if = "Vec::is_empty")]
61 pub tools: Vec<ResponsesToolDefinition>,
62 #[serde(flatten)]
64 pub additional_parameters: AdditionalParameters,
65}
66
67impl CompletionRequest {
68 pub fn with_structured_outputs<S>(mut self, schema_name: S, schema: serde_json::Value) -> Self
69 where
70 S: Into<String>,
71 {
72 self.additional_parameters.text = Some(TextConfig::structured_output(schema_name, schema));
73
74 self
75 }
76
77 pub fn with_reasoning(mut self, reasoning: Reasoning) -> Self {
78 self.additional_parameters.reasoning = Some(reasoning);
79
80 self
81 }
82}
83
84#[derive(Debug, Deserialize, Serialize, Clone)]
86pub struct InputItem {
87 #[serde(skip_serializing_if = "Option::is_none")]
91 role: Option<Role>,
92 #[serde(flatten)]
94 input: InputContent,
95}
96
97#[derive(Debug, Deserialize, Serialize, Clone)]
99#[serde(rename_all = "lowercase")]
100pub enum Role {
101 User,
102 Assistant,
103 System,
104}
105
106#[derive(Debug, Deserialize, Serialize, Clone)]
108#[serde(tag = "type", rename_all = "snake_case")]
109pub enum InputContent {
110 Message(Message),
111 Reasoning(OpenAIReasoning),
112 FunctionCall(OutputFunctionCall),
113 FunctionCallOutput(ToolResult),
114}
115
116#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
117pub struct OpenAIReasoning {
118 id: String,
119 pub summary: Vec<ReasoningSummary>,
120 pub encrypted_content: Option<String>,
121 #[serde(skip_serializing_if = "Option::is_none")]
122 pub status: Option<ToolStatus>,
123}
124
125#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
126#[serde(tag = "type", rename_all = "snake_case")]
127pub enum ReasoningSummary {
128 SummaryText { text: String },
129}
130
131impl ReasoningSummary {
132 fn new(input: &str) -> Self {
133 Self::SummaryText {
134 text: input.to_string(),
135 }
136 }
137
138 pub fn text(&self) -> String {
139 let ReasoningSummary::SummaryText { text } = self;
140 text.clone()
141 }
142}
143
144#[derive(Debug, Deserialize, Serialize, Clone)]
146pub struct ToolResult {
147 call_id: String,
149 output: String,
151 status: ToolStatus,
153}
154
155impl From<Message> for InputItem {
156 fn from(value: Message) -> Self {
157 match value {
158 Message::User { .. } => Self {
159 role: Some(Role::User),
160 input: InputContent::Message(value),
161 },
162 Message::Assistant { ref content, .. } => {
163 let role = if content
164 .clone()
165 .iter()
166 .any(|x| matches!(x, AssistantContentType::Reasoning(_)))
167 {
168 None
169 } else {
170 Some(Role::Assistant)
171 };
172 Self {
173 role,
174 input: InputContent::Message(value),
175 }
176 }
177 Message::System { .. } => Self {
178 role: Some(Role::System),
179 input: InputContent::Message(value),
180 },
181 Message::ToolResult {
182 tool_call_id,
183 output,
184 } => Self {
185 role: None,
186 input: InputContent::FunctionCallOutput(ToolResult {
187 call_id: tool_call_id,
188 output,
189 status: ToolStatus::Completed,
190 }),
191 },
192 }
193 }
194}
195
196impl TryFrom<crate::completion::Message> for Vec<InputItem> {
197 type Error = CompletionError;
198
199 fn try_from(value: crate::completion::Message) -> Result<Self, Self::Error> {
200 match value {
201 crate::completion::Message::User { content } => {
202 let mut items = Vec::new();
203
204 for user_content in content {
205 match user_content {
206 crate::message::UserContent::Text(Text { text }) => {
207 items.push(InputItem {
208 role: Some(Role::User),
209 input: InputContent::Message(Message::User {
210 content: OneOrMany::one(UserContent::InputText { text }),
211 name: None,
212 }),
213 });
214 }
215 crate::message::UserContent::ToolResult(
216 crate::completion::message::ToolResult {
217 call_id,
218 content: tool_content,
219 ..
220 },
221 ) => {
222 for tool_result_content in tool_content {
223 let crate::completion::message::ToolResultContent::Text(Text {
224 text,
225 }) = tool_result_content
226 else {
227 return Err(CompletionError::ProviderError(
228 "This thing only supports text!".to_string(),
229 ));
230 };
231 items.push(InputItem {
233 role: None,
234 input: InputContent::FunctionCallOutput(ToolResult {
235 call_id: call_id
236 .clone()
237 .expect("The call ID of this tool should exist!"),
238 output: text,
239 status: ToolStatus::Completed,
240 }),
241 });
242 }
243 }
244 crate::message::UserContent::Document(Document {
245 data,
246 media_type: Some(DocumentMediaType::PDF),
247 ..
248 }) => {
249 let (file_data, file_url) = match data {
250 DocumentSourceKind::Base64(data) => {
251 (Some(format!("data:application/pdf;base64,{data}")), None)
252 }
253 DocumentSourceKind::Url(url) => (None, Some(url)),
254 DocumentSourceKind::Raw(_) => {
255 return Err(CompletionError::RequestError(
256 "Raw file data not supported, encode as base64 first"
257 .into(),
258 ));
259 }
260 doc => {
261 return Err(CompletionError::RequestError(
262 format!("Unsupported document type: {doc}").into(),
263 ));
264 }
265 };
266
267 items.push(InputItem {
268 role: Some(Role::User),
269 input: InputContent::Message(Message::User {
270 content: OneOrMany::one(UserContent::InputFile {
271 file_data,
272 file_url,
273 filename: Some("document.pdf".to_string()),
274 }),
275 name: None,
276 }),
277 })
278 }
279 crate::message::UserContent::Document(Document {
281 data: DocumentSourceKind::Base64(text),
282 ..
283 }) => items.push(InputItem {
284 role: Some(Role::User),
285 input: InputContent::Message(Message::User {
286 content: OneOrMany::one(UserContent::InputText { text }),
287 name: None,
288 }),
289 }),
290 crate::message::UserContent::Document(Document {
291 data: DocumentSourceKind::String(text),
292 ..
293 }) => items.push(InputItem {
294 role: Some(Role::User),
295 input: InputContent::Message(Message::User {
296 content: OneOrMany::one(UserContent::InputText { text }),
297 name: None,
298 }),
299 }),
300 crate::message::UserContent::Image(crate::message::Image {
301 data,
302 media_type,
303 detail,
304 ..
305 }) => {
306 let url = match data {
307 DocumentSourceKind::Base64(data) => {
308 let media_type = if let Some(media_type) = media_type {
309 media_type.to_mime_type().to_string()
310 } else {
311 String::new()
312 };
313 format!("data:{media_type};base64,{data}")
314 }
315 DocumentSourceKind::Url(url) => url,
316 DocumentSourceKind::Raw(_) => {
317 return Err(CompletionError::RequestError(
318 "Raw file data not supported, encode as base64 first"
319 .into(),
320 ));
321 }
322 doc => {
323 return Err(CompletionError::RequestError(
324 format!("Unsupported document type: {doc}").into(),
325 ));
326 }
327 };
328 items.push(InputItem {
329 role: Some(Role::User),
330 input: InputContent::Message(Message::User {
331 content: OneOrMany::one(UserContent::InputImage {
332 image_url: url,
333 detail: detail.unwrap_or_default(),
334 }),
335 name: None,
336 }),
337 });
338 }
339 message => {
340 return Err(CompletionError::ProviderError(format!(
341 "Unsupported message: {message:?}"
342 )));
343 }
344 }
345 }
346
347 Ok(items)
348 }
349 crate::completion::Message::Assistant { id, content } => {
350 let mut items = Vec::new();
351
352 for assistant_content in content {
353 match assistant_content {
354 crate::message::AssistantContent::Text(Text { text }) => {
355 let id = id.as_ref().unwrap_or(&String::default()).clone();
356 items.push(InputItem {
357 role: Some(Role::Assistant),
358 input: InputContent::Message(Message::Assistant {
359 content: OneOrMany::one(AssistantContentType::Text(
360 AssistantContent::OutputText(Text { text }),
361 )),
362 id,
363 name: None,
364 status: ToolStatus::Completed,
365 }),
366 });
367 }
368 crate::message::AssistantContent::ToolCall(crate::message::ToolCall {
369 id: tool_id,
370 call_id,
371 function,
372 }) => {
373 items.push(InputItem {
374 role: None,
375 input: InputContent::FunctionCall(OutputFunctionCall {
376 arguments: function.arguments,
377 call_id: call_id.expect("The tool call ID should exist!"),
378 id: tool_id,
379 name: function.name,
380 status: ToolStatus::Completed,
381 }),
382 });
383 }
384 crate::message::AssistantContent::Reasoning(
385 crate::message::Reasoning { id, reasoning, .. },
386 ) => {
387 items.push(InputItem {
388 role: None,
389 input: InputContent::Reasoning(OpenAIReasoning {
390 id: id
391 .expect("An OpenAI-generated ID is required when using OpenAI reasoning items"),
392 summary: reasoning.into_iter().map(|x| ReasoningSummary::new(&x)).collect(),
393 encrypted_content: None,
394 status: None,
395 }),
396 });
397 }
398 crate::message::AssistantContent::Image(_) => {
399 return Err(CompletionError::ProviderError(
400 "Assistant image content is not supported in OpenAI Responses API"
401 .to_string(),
402 ));
403 }
404 }
405 }
406
407 Ok(items)
408 }
409 }
410 }
411}
412
413impl From<OneOrMany<String>> for Vec<ReasoningSummary> {
414 fn from(value: OneOrMany<String>) -> Self {
415 value.iter().map(|x| ReasoningSummary::new(x)).collect()
416 }
417}
418
419#[derive(Debug, Deserialize, Serialize, Clone)]
421pub struct ResponsesToolDefinition {
422 pub name: String,
424 pub parameters: serde_json::Value,
426 pub strict: bool,
428 #[serde(rename = "type")]
430 pub kind: String,
431 pub description: String,
433}
434
435fn sanitize_schema(schema: &mut serde_json::Value) {
438 if let Value::Object(obj) = schema {
439 let is_object_schema = obj.get("type") == Some(&Value::String("object".to_string()))
440 || obj.contains_key("properties");
441
442 if is_object_schema && !obj.contains_key("additionalProperties") {
445 obj.insert("additionalProperties".to_string(), Value::Bool(false));
446 }
447
448 if let Some(Value::Object(properties)) = obj.get("properties") {
451 let prop_keys = properties.keys().cloned().map(Value::String).collect();
452 obj.insert("required".to_string(), Value::Array(prop_keys));
453 }
454
455 if let Some(defs) = obj.get_mut("$defs")
456 && let Value::Object(defs_obj) = defs
457 {
458 for (_, def_schema) in defs_obj.iter_mut() {
459 sanitize_schema(def_schema);
460 }
461 }
462
463 if let Some(properties) = obj.get_mut("properties")
464 && let Value::Object(props) = properties
465 {
466 for (_, prop_value) in props.iter_mut() {
467 sanitize_schema(prop_value);
468 }
469 }
470
471 if let Some(items) = obj.get_mut("items") {
472 sanitize_schema(items);
473 }
474
475 for key in ["anyOf", "oneOf", "allOf"] {
477 if let Some(variants) = obj.get_mut(key)
478 && let Value::Array(variants_array) = variants
479 {
480 for variant in variants_array.iter_mut() {
481 sanitize_schema(variant);
482 }
483 }
484 }
485 }
486}
487
488impl From<completion::ToolDefinition> for ResponsesToolDefinition {
489 fn from(value: completion::ToolDefinition) -> Self {
490 let completion::ToolDefinition {
491 name,
492 mut parameters,
493 description,
494 } = value;
495
496 sanitize_schema(&mut parameters);
497
498 Self {
499 name,
500 parameters,
501 description,
502 kind: "function".to_string(),
503 strict: true,
504 }
505 }
506}
507
508#[derive(Clone, Debug, Serialize, Deserialize)]
511pub struct ResponsesUsage {
512 pub input_tokens: u64,
514 #[serde(skip_serializing_if = "Option::is_none")]
516 pub input_tokens_details: Option<InputTokensDetails>,
517 pub output_tokens: u64,
519 pub output_tokens_details: OutputTokensDetails,
521 pub total_tokens: u64,
523}
524
525impl ResponsesUsage {
526 pub(crate) fn new() -> Self {
528 Self {
529 input_tokens: 0,
530 input_tokens_details: Some(InputTokensDetails::new()),
531 output_tokens: 0,
532 output_tokens_details: OutputTokensDetails::new(),
533 total_tokens: 0,
534 }
535 }
536}
537
538impl Add for ResponsesUsage {
539 type Output = Self;
540
541 fn add(self, rhs: Self) -> Self::Output {
542 let input_tokens = self.input_tokens + rhs.input_tokens;
543 let input_tokens_details = self.input_tokens_details.map(|lhs| {
544 if let Some(tokens) = rhs.input_tokens_details {
545 lhs + tokens
546 } else {
547 lhs
548 }
549 });
550 let output_tokens = self.output_tokens + rhs.output_tokens;
551 let output_tokens_details = self.output_tokens_details + rhs.output_tokens_details;
552 let total_tokens = self.total_tokens + rhs.total_tokens;
553 Self {
554 input_tokens,
555 input_tokens_details,
556 output_tokens,
557 output_tokens_details,
558 total_tokens,
559 }
560 }
561}
562
563#[derive(Clone, Debug, Serialize, Deserialize)]
565pub struct InputTokensDetails {
566 pub cached_tokens: u64,
568}
569
570impl InputTokensDetails {
571 pub(crate) fn new() -> Self {
572 Self { cached_tokens: 0 }
573 }
574}
575
576impl Add for InputTokensDetails {
577 type Output = Self;
578 fn add(self, rhs: Self) -> Self::Output {
579 Self {
580 cached_tokens: self.cached_tokens + rhs.cached_tokens,
581 }
582 }
583}
584
585#[derive(Clone, Debug, Serialize, Deserialize)]
587pub struct OutputTokensDetails {
588 pub reasoning_tokens: u64,
590}
591
592impl OutputTokensDetails {
593 pub(crate) fn new() -> Self {
594 Self {
595 reasoning_tokens: 0,
596 }
597 }
598}
599
600impl Add for OutputTokensDetails {
601 type Output = Self;
602 fn add(self, rhs: Self) -> Self::Output {
603 Self {
604 reasoning_tokens: self.reasoning_tokens + rhs.reasoning_tokens,
605 }
606 }
607}
608
609#[derive(Clone, Debug, Default, Serialize, Deserialize)]
611pub struct IncompleteDetailsReason {
612 pub reason: String,
614}
615
616#[derive(Clone, Debug, Default, Serialize, Deserialize)]
618pub struct ResponseError {
619 pub code: String,
621 pub message: String,
623}
624
625#[derive(Clone, Debug, Deserialize, Serialize)]
627#[serde(rename_all = "snake_case")]
628pub enum ResponseObject {
629 Response,
630}
631
632#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
634#[serde(rename_all = "snake_case")]
635pub enum ResponseStatus {
636 InProgress,
637 Completed,
638 Failed,
639 Cancelled,
640 Queued,
641 Incomplete,
642}
643
644impl TryFrom<(String, crate::completion::CompletionRequest)> for CompletionRequest {
646 type Error = CompletionError;
647 fn try_from(
648 (model, req): (String, crate::completion::CompletionRequest),
649 ) -> Result<Self, Self::Error> {
650 let input = {
651 let mut partial_history = vec![];
652 if let Some(docs) = req.normalized_documents() {
653 partial_history.push(docs);
654 }
655 partial_history.extend(req.chat_history);
656
657 let mut full_history: Vec<InputItem> = Vec::new();
659
660 full_history.extend(
662 partial_history
663 .into_iter()
664 .map(|x| <Vec<InputItem>>::try_from(x).unwrap())
665 .collect::<Vec<Vec<InputItem>>>()
666 .into_iter()
667 .flatten()
668 .collect::<Vec<InputItem>>(),
669 );
670
671 full_history
672 };
673
674 let input = OneOrMany::many(input)
675 .expect("This should never panic - if it does, please file a bug report");
676
677 let stream = req
678 .additional_params
679 .clone()
680 .unwrap_or(Value::Null)
681 .as_bool();
682
683 let additional_parameters = if let Some(map) = req.additional_params {
684 serde_json::from_value::<AdditionalParameters>(map).expect("Converting additional parameters to AdditionalParameters should never fail as every field is an Option")
685 } else {
686 AdditionalParameters::default()
688 };
689
690 let tool_choice = req.tool_choice.map(ToolChoice::try_from).transpose()?;
691
692 Ok(Self {
693 input,
694 model,
695 instructions: req.preamble,
696 max_output_tokens: req.max_tokens,
697 stream,
698 tool_choice,
699 tools: req
700 .tools
701 .into_iter()
702 .map(ResponsesToolDefinition::from)
703 .collect(),
704 temperature: req.temperature,
705 additional_parameters,
706 })
707 }
708}
709
710#[derive(Clone)]
712pub struct ResponsesCompletionModel<T = reqwest::Client> {
713 pub(crate) client: Client<T>,
715 pub model: String,
717}
718
719impl<T> ResponsesCompletionModel<T>
720where
721 T: HttpClientExt + Clone + Default + std::fmt::Debug + 'static,
722{
723 pub fn new(client: Client<T>, model: impl Into<String>) -> Self {
725 Self {
726 client,
727 model: model.into(),
728 }
729 }
730
731 pub fn with_model(client: Client<T>, model: &str) -> Self {
732 Self {
733 client,
734 model: model.to_string(),
735 }
736 }
737
738 pub fn completions_api(self) -> crate::providers::openai::completion::CompletionModel<T> {
740 super::completion::CompletionModel::with_model(self.client.completions_api(), &self.model)
741 }
742
743 pub(crate) fn create_completion_request(
745 &self,
746 completion_request: crate::completion::CompletionRequest,
747 ) -> Result<CompletionRequest, CompletionError> {
748 let req = CompletionRequest::try_from((self.model.clone(), completion_request))?;
749
750 Ok(req)
751 }
752}
753
754#[derive(Clone, Debug, Serialize, Deserialize)]
756pub struct CompletionResponse {
757 pub id: String,
759 pub object: ResponseObject,
761 pub created_at: u64,
763 pub status: ResponseStatus,
765 pub error: Option<ResponseError>,
767 pub incomplete_details: Option<IncompleteDetailsReason>,
769 pub instructions: Option<String>,
771 pub max_output_tokens: Option<u64>,
773 pub model: String,
775 pub usage: Option<ResponsesUsage>,
777 pub output: Vec<Output>,
779 #[serde(default)]
781 pub tools: Vec<ResponsesToolDefinition>,
782 #[serde(flatten)]
784 pub additional_parameters: AdditionalParameters,
785}
786
787#[derive(Clone, Debug, Deserialize, Serialize, Default)]
790pub struct AdditionalParameters {
791 #[serde(skip_serializing_if = "Option::is_none")]
793 pub background: Option<bool>,
794 #[serde(skip_serializing_if = "Option::is_none")]
796 pub text: Option<TextConfig>,
797 #[serde(skip_serializing_if = "Option::is_none")]
799 pub include: Option<Vec<Include>>,
800 #[serde(skip_serializing_if = "Option::is_none")]
802 pub top_p: Option<f64>,
803 #[serde(skip_serializing_if = "Option::is_none")]
805 pub truncation: Option<TruncationStrategy>,
806 #[serde(skip_serializing_if = "Option::is_none")]
808 pub user: Option<String>,
809 #[serde(skip_serializing_if = "Map::is_empty", default)]
811 pub metadata: serde_json::Map<String, serde_json::Value>,
812 #[serde(skip_serializing_if = "Option::is_none")]
814 pub parallel_tool_calls: Option<bool>,
815 #[serde(skip_serializing_if = "Option::is_none")]
817 pub previous_response_id: Option<String>,
818 #[serde(skip_serializing_if = "Option::is_none")]
820 pub reasoning: Option<Reasoning>,
821 #[serde(skip_serializing_if = "Option::is_none")]
823 pub service_tier: Option<OpenAIServiceTier>,
824 #[serde(skip_serializing_if = "Option::is_none")]
826 pub store: Option<bool>,
827}
828
829impl AdditionalParameters {
830 pub fn to_json(self) -> serde_json::Value {
831 serde_json::to_value(self).expect("this should never fail since a struct that impls Deserialize will always be valid JSON")
832 }
833}
834
835#[derive(Clone, Debug, Default, Serialize, Deserialize)]
839#[serde(rename_all = "snake_case")]
840pub enum TruncationStrategy {
841 Auto,
842 #[default]
843 Disabled,
844}
845
846#[derive(Clone, Debug, Serialize, Deserialize)]
849pub struct TextConfig {
850 pub format: TextFormat,
851}
852
853impl TextConfig {
854 pub(crate) fn structured_output<S>(name: S, schema: serde_json::Value) -> Self
855 where
856 S: Into<String>,
857 {
858 Self {
859 format: TextFormat::JsonSchema(StructuredOutputsInput {
860 name: name.into(),
861 schema,
862 strict: true,
863 }),
864 }
865 }
866}
867
868#[derive(Clone, Debug, Serialize, Deserialize, Default)]
871#[serde(tag = "type")]
872#[serde(rename_all = "snake_case")]
873pub enum TextFormat {
874 JsonSchema(StructuredOutputsInput),
875 #[default]
876 Text,
877}
878
879#[derive(Clone, Debug, Serialize, Deserialize)]
881pub struct StructuredOutputsInput {
882 pub name: String,
884 pub schema: serde_json::Value,
886 pub strict: bool,
888}
889
890#[derive(Clone, Debug, Default, Serialize, Deserialize)]
892pub struct Reasoning {
893 pub effort: Option<ReasoningEffort>,
895 #[serde(skip_serializing_if = "Option::is_none")]
897 pub summary: Option<ReasoningSummaryLevel>,
898}
899
900impl Reasoning {
901 pub fn new() -> Self {
903 Self {
904 effort: None,
905 summary: None,
906 }
907 }
908
909 pub fn with_effort(mut self, reasoning_effort: ReasoningEffort) -> Self {
911 self.effort = Some(reasoning_effort);
912
913 self
914 }
915
916 pub fn with_summary_level(mut self, reasoning_summary_level: ReasoningSummaryLevel) -> Self {
918 self.summary = Some(reasoning_summary_level);
919
920 self
921 }
922}
923
924#[derive(Clone, Debug, Default, Serialize, Deserialize)]
926#[serde(rename_all = "snake_case")]
927pub enum OpenAIServiceTier {
928 #[default]
929 Auto,
930 Default,
931 Flex,
932}
933
934#[derive(Clone, Debug, Default, Serialize, Deserialize)]
936#[serde(rename_all = "snake_case")]
937pub enum ReasoningEffort {
938 Minimal,
939 Low,
940 #[default]
941 Medium,
942 High,
943}
944
945#[derive(Clone, Debug, Default, Serialize, Deserialize)]
947#[serde(rename_all = "snake_case")]
948pub enum ReasoningSummaryLevel {
949 #[default]
950 Auto,
951 Concise,
952 Detailed,
953}
954
955#[derive(Clone, Debug, Deserialize, Serialize)]
958pub enum Include {
959 #[serde(rename = "file_search_call.results")]
960 FileSearchCallResults,
961 #[serde(rename = "message.input_image.image_url")]
962 MessageInputImageImageUrl,
963 #[serde(rename = "computer_call.output.image_url")]
964 ComputerCallOutputOutputImageUrl,
965 #[serde(rename = "reasoning.encrypted_content")]
966 ReasoningEncryptedContent,
967 #[serde(rename = "code_interpreter_call.outputs")]
968 CodeInterpreterCallOutputs,
969}
970
971#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
973#[serde(tag = "type")]
974#[serde(rename_all = "snake_case")]
975pub enum Output {
976 Message(OutputMessage),
977 #[serde(alias = "function_call")]
978 FunctionCall(OutputFunctionCall),
979 Reasoning {
980 id: String,
981 summary: Vec<ReasoningSummary>,
982 },
983}
984
985impl From<Output> for Vec<completion::AssistantContent> {
986 fn from(value: Output) -> Self {
987 let res: Vec<completion::AssistantContent> = match value {
988 Output::Message(OutputMessage { content, .. }) => content
989 .into_iter()
990 .map(completion::AssistantContent::from)
991 .collect(),
992 Output::FunctionCall(OutputFunctionCall {
993 id,
994 arguments,
995 call_id,
996 name,
997 ..
998 }) => vec![completion::AssistantContent::tool_call_with_call_id(
999 id, call_id, name, arguments,
1000 )],
1001 Output::Reasoning { id, summary } => {
1002 let summary: Vec<String> = summary.into_iter().map(|x| x.text()).collect();
1003
1004 vec![completion::AssistantContent::Reasoning(
1005 message::Reasoning::multi(summary).with_id(id),
1006 )]
1007 }
1008 };
1009
1010 res
1011 }
1012}
1013
1014#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1015pub struct OutputReasoning {
1016 id: String,
1017 summary: Vec<ReasoningSummary>,
1018 status: ToolStatus,
1019}
1020
1021#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1023pub struct OutputFunctionCall {
1024 pub id: String,
1025 #[serde(with = "json_utils::stringified_json")]
1026 pub arguments: serde_json::Value,
1027 pub call_id: String,
1028 pub name: String,
1029 pub status: ToolStatus,
1030}
1031
1032#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1034#[serde(rename_all = "snake_case")]
1035pub enum ToolStatus {
1036 InProgress,
1037 Completed,
1038 Incomplete,
1039}
1040
1041#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1043pub struct OutputMessage {
1044 pub id: String,
1046 pub role: OutputRole,
1048 pub status: ResponseStatus,
1050 pub content: Vec<AssistantContent>,
1052}
1053
1054#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1056#[serde(rename_all = "snake_case")]
1057pub enum OutputRole {
1058 Assistant,
1059}
1060
1061impl<T> completion::CompletionModel for ResponsesCompletionModel<T>
1062where
1063 T: HttpClientExt
1064 + Clone
1065 + std::fmt::Debug
1066 + Default
1067 + WasmCompatSend
1068 + WasmCompatSync
1069 + 'static,
1070{
1071 type Response = CompletionResponse;
1072 type StreamingResponse = StreamingCompletionResponse;
1073
1074 type Client = super::Client<T>;
1075
1076 fn make(client: &Self::Client, model: impl Into<String>) -> Self {
1077 Self::new(client.clone(), model)
1078 }
1079
1080 async fn completion(
1081 &self,
1082 completion_request: crate::completion::CompletionRequest,
1083 ) -> Result<completion::CompletionResponse<Self::Response>, CompletionError> {
1084 let span = if tracing::Span::current().is_disabled() {
1085 info_span!(
1086 target: "rig::completions",
1087 "chat",
1088 gen_ai.operation.name = "chat",
1089 gen_ai.provider.name = tracing::field::Empty,
1090 gen_ai.request.model = tracing::field::Empty,
1091 gen_ai.response.id = tracing::field::Empty,
1092 gen_ai.response.model = tracing::field::Empty,
1093 gen_ai.usage.output_tokens = tracing::field::Empty,
1094 gen_ai.usage.input_tokens = tracing::field::Empty,
1095 gen_ai.input.messages = tracing::field::Empty,
1096 gen_ai.output.messages = tracing::field::Empty,
1097 )
1098 } else {
1099 tracing::Span::current()
1100 };
1101
1102 span.record("gen_ai.provider.name", "openai");
1103 span.record("gen_ai.request.model", &self.model);
1104 let request = self.create_completion_request(completion_request)?;
1105 let body = serde_json::to_vec(&request)?;
1106
1107 if enabled!(Level::TRACE) {
1108 tracing::trace!(
1109 target: "rig::completions",
1110 "OpenAI Responses completion request: {request}",
1111 request = serde_json::to_string_pretty(&request)?
1112 );
1113 }
1114
1115 let req = self
1116 .client
1117 .post("/responses")?
1118 .body(body)
1119 .map_err(|e| CompletionError::HttpError(e.into()))?;
1120
1121 async move {
1122 let response = self.client.send(req).await?;
1123
1124 if response.status().is_success() {
1125 let t = http_client::text(response).await?;
1126 let response = serde_json::from_str::<Self::Response>(&t)?;
1127 let span = tracing::Span::current();
1128 span.record("gen_ai.response.id", &response.id);
1129 span.record("gen_ai.response.model", &response.model);
1130 if let Some(ref usage) = response.usage {
1131 span.record("gen_ai.usage.output_tokens", usage.output_tokens);
1132 span.record("gen_ai.usage.input_tokens", usage.input_tokens);
1133 }
1134 if enabled!(Level::TRACE) {
1135 tracing::trace!(
1136 target: "rig::completions",
1137 "OpenAI Responses completion response: {response}",
1138 response = serde_json::to_string_pretty(&response)?
1139 );
1140 }
1141 response.try_into()
1142 } else {
1143 let text = http_client::text(response).await?;
1144 Err(CompletionError::ProviderError(text))
1145 }
1146 }
1147 .instrument(span)
1148 .await
1149 }
1150
1151 #[cfg_attr(feature = "worker", worker::send)]
1152 async fn stream(
1153 &self,
1154 request: crate::completion::CompletionRequest,
1155 ) -> Result<
1156 crate::streaming::StreamingCompletionResponse<Self::StreamingResponse>,
1157 CompletionError,
1158 > {
1159 ResponsesCompletionModel::stream(self, request).await
1160 }
1161}
1162
1163impl TryFrom<CompletionResponse> for completion::CompletionResponse<CompletionResponse> {
1164 type Error = CompletionError;
1165
1166 fn try_from(response: CompletionResponse) -> Result<Self, Self::Error> {
1167 if response.output.is_empty() {
1168 return Err(CompletionError::ResponseError(
1169 "Response contained no parts".to_owned(),
1170 ));
1171 }
1172
1173 let content: Vec<completion::AssistantContent> = response
1174 .output
1175 .iter()
1176 .cloned()
1177 .flat_map(<Vec<completion::AssistantContent>>::from)
1178 .collect();
1179
1180 let choice = OneOrMany::many(content).map_err(|_| {
1181 CompletionError::ResponseError(
1182 "Response contained no message or tool call (empty)".to_owned(),
1183 )
1184 })?;
1185
1186 let usage = response
1187 .usage
1188 .as_ref()
1189 .map(|usage| completion::Usage {
1190 input_tokens: usage.input_tokens,
1191 output_tokens: usage.output_tokens,
1192 total_tokens: usage.total_tokens,
1193 })
1194 .unwrap_or_default();
1195
1196 Ok(completion::CompletionResponse {
1197 choice,
1198 usage,
1199 raw_response: response,
1200 })
1201 }
1202}
1203
1204#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1206#[serde(tag = "role", rename_all = "lowercase")]
1207pub enum Message {
1208 #[serde(alias = "developer")]
1209 System {
1210 #[serde(deserialize_with = "string_or_one_or_many")]
1211 content: OneOrMany<SystemContent>,
1212 #[serde(skip_serializing_if = "Option::is_none")]
1213 name: Option<String>,
1214 },
1215 User {
1216 #[serde(deserialize_with = "string_or_one_or_many")]
1217 content: OneOrMany<UserContent>,
1218 #[serde(skip_serializing_if = "Option::is_none")]
1219 name: Option<String>,
1220 },
1221 Assistant {
1222 content: OneOrMany<AssistantContentType>,
1223 #[serde(skip_serializing_if = "String::is_empty")]
1224 id: String,
1225 #[serde(skip_serializing_if = "Option::is_none")]
1226 name: Option<String>,
1227 status: ToolStatus,
1228 },
1229 #[serde(rename = "tool")]
1230 ToolResult {
1231 tool_call_id: String,
1232 output: String,
1233 },
1234}
1235
1236#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)]
1238#[serde(rename_all = "lowercase")]
1239pub enum ToolResultContentType {
1240 #[default]
1241 Text,
1242}
1243
1244impl Message {
1245 pub fn system(content: &str) -> Self {
1246 Message::System {
1247 content: OneOrMany::one(content.to_owned().into()),
1248 name: None,
1249 }
1250 }
1251}
1252
1253#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1256#[serde(tag = "type", rename_all = "snake_case")]
1257pub enum AssistantContent {
1258 OutputText(Text),
1259 Refusal { refusal: String },
1260}
1261
1262impl From<AssistantContent> for completion::AssistantContent {
1263 fn from(value: AssistantContent) -> Self {
1264 match value {
1265 AssistantContent::Refusal { refusal } => {
1266 completion::AssistantContent::Text(Text { text: refusal })
1267 }
1268 AssistantContent::OutputText(Text { text }) => {
1269 completion::AssistantContent::Text(Text { text })
1270 }
1271 }
1272 }
1273}
1274
1275#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1277#[serde(untagged)]
1278pub enum AssistantContentType {
1279 Text(AssistantContent),
1280 ToolCall(OutputFunctionCall),
1281 Reasoning(OpenAIReasoning),
1282}
1283
1284#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1286#[serde(tag = "type", rename_all = "snake_case")]
1287pub enum UserContent {
1288 InputText {
1289 text: String,
1290 },
1291 InputImage {
1292 image_url: String,
1293 #[serde(default)]
1294 detail: ImageDetail,
1295 },
1296 InputFile {
1297 #[serde(skip_serializing_if = "Option::is_none")]
1298 file_url: Option<String>,
1299 #[serde(skip_serializing_if = "Option::is_none")]
1300 file_data: Option<String>,
1301 #[serde(skip_serializing_if = "Option::is_none")]
1302 filename: Option<String>,
1303 },
1304 Audio {
1305 input_audio: InputAudio,
1306 },
1307 #[serde(rename = "tool")]
1308 ToolResult {
1309 tool_call_id: String,
1310 output: String,
1311 },
1312}
1313
1314impl TryFrom<message::Message> for Vec<Message> {
1315 type Error = message::MessageError;
1316
1317 fn try_from(message: message::Message) -> Result<Self, Self::Error> {
1318 match message {
1319 message::Message::User { content } => {
1320 let (tool_results, other_content): (Vec<_>, Vec<_>) = content
1321 .into_iter()
1322 .partition(|content| matches!(content, message::UserContent::ToolResult(_)));
1323
1324 if !tool_results.is_empty() {
1327 tool_results
1328 .into_iter()
1329 .map(|content| match content {
1330 message::UserContent::ToolResult(message::ToolResult {
1331 call_id,
1332 content,
1333 ..
1334 }) => Ok::<_, message::MessageError>(Message::ToolResult {
1335 tool_call_id: call_id.expect("The tool call ID should exist"),
1336 output: {
1337 let res = content.first();
1338 match res {
1339 completion::message::ToolResultContent::Text(Text {
1340 text,
1341 }) => text,
1342 _ => return Err(MessageError::ConversionError("This API only currently supports text tool results".into()))
1343 }
1344 },
1345 }),
1346 _ => unreachable!(),
1347 })
1348 .collect::<Result<Vec<_>, _>>()
1349 } else {
1350 let other_content = other_content
1351 .into_iter()
1352 .map(|content| match content {
1353 message::UserContent::Text(message::Text { text }) => {
1354 Ok(UserContent::InputText { text })
1355 }
1356 message::UserContent::Image(message::Image {
1357 data,
1358 detail,
1359 media_type,
1360 ..
1361 }) => {
1362 let url = match data {
1363 DocumentSourceKind::Base64(data) => {
1364 let media_type = if let Some(media_type) = media_type {
1365 media_type.to_mime_type().to_string()
1366 } else {
1367 String::new()
1368 };
1369 format!("data:{media_type};base64,{data}")
1370 }
1371 DocumentSourceKind::Url(url) => url,
1372 DocumentSourceKind::Raw(_) => {
1373 return Err(MessageError::ConversionError(
1374 "Raw files not supported, encode as base64 first"
1375 .into(),
1376 ));
1377 }
1378 doc => {
1379 return Err(MessageError::ConversionError(format!(
1380 "Unsupported document type: {doc}"
1381 )));
1382 }
1383 };
1384
1385 Ok(UserContent::InputImage {
1386 image_url: url,
1387 detail: detail.unwrap_or_default(),
1388 })
1389 }
1390 message::UserContent::Document(message::Document {
1391 media_type: Some(DocumentMediaType::PDF),
1392 data,
1393 ..
1394 }) => {
1395 let (file_data, file_url) = match data {
1396 DocumentSourceKind::Base64(data) => {
1397 (Some(format!("data:application/pdf;base64,{data}")), None)
1398 }
1399 DocumentSourceKind::Url(url) => (None, Some(url)),
1400 DocumentSourceKind::Raw(_) => {
1401 return Err(MessageError::ConversionError(
1402 "Raw files not supported, encode as base64 first"
1403 .into(),
1404 ));
1405 }
1406 doc => {
1407 return Err(MessageError::ConversionError(format!(
1408 "Unsupported document type: {doc}"
1409 )));
1410 }
1411 };
1412
1413 Ok(UserContent::InputFile {
1414 file_url,
1415 file_data,
1416 filename: Some("document.pdf".into()),
1417 })
1418 }
1419 message::UserContent::Document(message::Document {
1420 data: DocumentSourceKind::Base64(text),
1421 ..
1422 }) => Ok(UserContent::InputText { text }),
1423 message::UserContent::Audio(message::Audio {
1424 data: DocumentSourceKind::Base64(data),
1425 media_type,
1426 ..
1427 }) => Ok(UserContent::Audio {
1428 input_audio: InputAudio {
1429 data,
1430 format: match media_type {
1431 Some(media_type) => media_type,
1432 None => AudioMediaType::MP3,
1433 },
1434 },
1435 }),
1436 message::UserContent::Audio(_) => Err(MessageError::ConversionError(
1437 "Audio must be base64 encoded data".into(),
1438 )),
1439 _ => unreachable!(),
1440 })
1441 .collect::<Result<Vec<_>, _>>()?;
1442
1443 let other_content = OneOrMany::many(other_content).expect(
1444 "There must be other content here if there were no tool result content",
1445 );
1446
1447 Ok(vec![Message::User {
1448 content: other_content,
1449 name: None,
1450 }])
1451 }
1452 }
1453 message::Message::Assistant { content, id } => {
1454 let assistant_message_id = id;
1455
1456 match content.first() {
1457 crate::message::AssistantContent::Text(Text { text }) => {
1458 Ok(vec![Message::Assistant {
1459 id: assistant_message_id
1460 .expect("The assistant message ID should exist"),
1461 status: ToolStatus::Completed,
1462 content: OneOrMany::one(AssistantContentType::Text(
1463 AssistantContent::OutputText(Text { text }),
1464 )),
1465 name: None,
1466 }])
1467 }
1468 crate::message::AssistantContent::ToolCall(crate::message::ToolCall {
1469 id,
1470 call_id,
1471 function,
1472 }) => Ok(vec![Message::Assistant {
1473 content: OneOrMany::one(AssistantContentType::ToolCall(
1474 OutputFunctionCall {
1475 call_id: call_id.expect("The call ID should exist"),
1476 arguments: function.arguments,
1477 id,
1478 name: function.name,
1479 status: ToolStatus::Completed,
1480 },
1481 )),
1482 id: assistant_message_id.expect("The assistant message ID should exist!"),
1483 name: None,
1484 status: ToolStatus::Completed,
1485 }]),
1486 crate::message::AssistantContent::Reasoning(crate::message::Reasoning {
1487 id,
1488 reasoning,
1489 ..
1490 }) => Ok(vec![Message::Assistant {
1491 content: OneOrMany::one(AssistantContentType::Reasoning(OpenAIReasoning {
1492 id: id.expect("An OpenAI-generated ID is required when using OpenAI reasoning items"),
1493 summary: reasoning.into_iter().map(|x| ReasoningSummary::SummaryText { text: x }).collect(),
1494 encrypted_content: None,
1495 status: Some(ToolStatus::Completed),
1496 })),
1497 id: assistant_message_id.expect("The assistant message ID should exist!"),
1498 name: None,
1499 status: (ToolStatus::Completed),
1500 }]),
1501 crate::message::AssistantContent::Image(_) => {
1502 Err(MessageError::ConversionError(
1503 "Assistant image content is not supported in OpenAI Responses API".into(),
1504 ))
1505 }
1506 }
1507 }
1508 }
1509 }
1510}
1511
1512impl FromStr for UserContent {
1513 type Err = Infallible;
1514
1515 fn from_str(s: &str) -> Result<Self, Self::Err> {
1516 Ok(UserContent::InputText {
1517 text: s.to_string(),
1518 })
1519 }
1520}