1use super::InputAudio;
11use super::completion::ToolChoice;
12use super::{Client, responses_api::streaming::StreamingCompletionResponse};
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, Clone)]
86pub struct InputItem {
87 #[serde(skip_serializing_if = "Option::is_none")]
91 role: Option<Role>,
92 #[serde(flatten)]
94 input: InputContent,
95}
96
97impl Serialize for InputItem {
98 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
99 where
100 S: serde::Serializer,
101 {
102 let mut value = serde_json::to_value(&self.input).map_err(serde::ser::Error::custom)?;
103 let map = value.as_object_mut().ok_or_else(|| {
104 serde::ser::Error::custom("Input content must serialize to an object")
105 })?;
106
107 if let Some(role) = &self.role
108 && !map.contains_key("role")
109 {
110 map.insert(
111 "role".to_string(),
112 serde_json::to_value(role).map_err(serde::ser::Error::custom)?,
113 );
114 }
115
116 value.serialize(serializer)
117 }
118}
119
120impl InputItem {
121 pub fn system_message(content: impl Into<String>) -> Self {
122 Self {
123 role: Some(Role::System),
124 input: InputContent::Message(Message::System {
125 content: OneOrMany::one(SystemContent::InputText {
126 text: content.into(),
127 }),
128 name: None,
129 }),
130 }
131 }
132}
133
134#[derive(Debug, Deserialize, Serialize, Clone)]
136#[serde(rename_all = "lowercase")]
137pub enum Role {
138 User,
139 Assistant,
140 System,
141}
142
143#[derive(Debug, Deserialize, Serialize, Clone)]
145#[serde(tag = "type", rename_all = "snake_case")]
146pub enum InputContent {
147 Message(Message),
148 Reasoning(OpenAIReasoning),
149 FunctionCall(OutputFunctionCall),
150 FunctionCallOutput(ToolResult),
151}
152
153#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
154pub struct OpenAIReasoning {
155 id: String,
156 pub summary: Vec<ReasoningSummary>,
157 pub encrypted_content: Option<String>,
158 #[serde(skip_serializing_if = "Option::is_none")]
159 pub status: Option<ToolStatus>,
160}
161
162#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
163#[serde(tag = "type", rename_all = "snake_case")]
164pub enum ReasoningSummary {
165 SummaryText { text: String },
166}
167
168impl ReasoningSummary {
169 fn new(input: &str) -> Self {
170 Self::SummaryText {
171 text: input.to_string(),
172 }
173 }
174
175 pub fn text(&self) -> String {
176 let ReasoningSummary::SummaryText { text } = self;
177 text.clone()
178 }
179}
180
181#[derive(Debug, Deserialize, Serialize, Clone)]
183pub struct ToolResult {
184 call_id: String,
186 output: String,
188 status: ToolStatus,
190}
191
192impl From<Message> for InputItem {
193 fn from(value: Message) -> Self {
194 match value {
195 Message::User { .. } => Self {
196 role: Some(Role::User),
197 input: InputContent::Message(value),
198 },
199 Message::Assistant { ref content, .. } => {
200 let role = if content
201 .iter()
202 .any(|x| matches!(x, AssistantContentType::Reasoning(_)))
203 {
204 None
205 } else {
206 Some(Role::Assistant)
207 };
208 Self {
209 role,
210 input: InputContent::Message(value),
211 }
212 }
213 Message::System { .. } => Self {
214 role: Some(Role::System),
215 input: InputContent::Message(value),
216 },
217 Message::ToolResult {
218 tool_call_id,
219 output,
220 } => Self {
221 role: None,
222 input: InputContent::FunctionCallOutput(ToolResult {
223 call_id: tool_call_id,
224 output,
225 status: ToolStatus::Completed,
226 }),
227 },
228 }
229 }
230}
231
232impl TryFrom<crate::completion::Message> for Vec<InputItem> {
233 type Error = CompletionError;
234
235 fn try_from(value: crate::completion::Message) -> Result<Self, Self::Error> {
236 match value {
237 crate::completion::Message::User { content } => {
238 let mut items = Vec::new();
239
240 for user_content in content {
241 match user_content {
242 crate::message::UserContent::Text(Text { text }) => {
243 items.push(InputItem {
244 role: Some(Role::User),
245 input: InputContent::Message(Message::User {
246 content: OneOrMany::one(UserContent::InputText { text }),
247 name: None,
248 }),
249 });
250 }
251 crate::message::UserContent::ToolResult(
252 crate::completion::message::ToolResult {
253 call_id,
254 content: tool_content,
255 ..
256 },
257 ) => {
258 for tool_result_content in tool_content {
259 let crate::completion::message::ToolResultContent::Text(Text {
260 text,
261 }) = tool_result_content
262 else {
263 return Err(CompletionError::ProviderError(
264 "This thing only supports text!".to_string(),
265 ));
266 };
267 items.push(InputItem {
269 role: None,
270 input: InputContent::FunctionCallOutput(ToolResult {
271 call_id: require_call_id(call_id.clone(), "Tool result")?,
272 output: text,
273 status: ToolStatus::Completed,
274 }),
275 });
276 }
277 }
278 crate::message::UserContent::Document(Document {
279 data,
280 media_type: Some(DocumentMediaType::PDF),
281 ..
282 }) => {
283 let (file_data, file_url) = match data {
284 DocumentSourceKind::Base64(data) => {
285 (Some(format!("data:application/pdf;base64,{data}")), None)
286 }
287 DocumentSourceKind::Url(url) => (None, Some(url)),
288 DocumentSourceKind::Raw(_) => {
289 return Err(CompletionError::RequestError(
290 "Raw file data not supported, encode as base64 first"
291 .into(),
292 ));
293 }
294 doc => {
295 return Err(CompletionError::RequestError(
296 format!("Unsupported document type: {doc}").into(),
297 ));
298 }
299 };
300
301 items.push(InputItem {
302 role: Some(Role::User),
303 input: InputContent::Message(Message::User {
304 content: OneOrMany::one(UserContent::InputFile {
305 file_data,
306 file_url,
307 filename: Some("document.pdf".to_string()),
308 }),
309 name: None,
310 }),
311 })
312 }
313 crate::message::UserContent::Document(Document {
314 data:
315 DocumentSourceKind::Base64(text) | DocumentSourceKind::String(text),
316 ..
317 }) => items.push(InputItem {
318 role: Some(Role::User),
319 input: InputContent::Message(Message::User {
320 content: OneOrMany::one(UserContent::InputText { text }),
321 name: None,
322 }),
323 }),
324 crate::message::UserContent::Image(crate::message::Image {
325 data,
326 media_type,
327 detail,
328 ..
329 }) => {
330 let url = match data {
331 DocumentSourceKind::Base64(data) => {
332 let media_type = if let Some(media_type) = media_type {
333 media_type.to_mime_type().to_string()
334 } else {
335 String::new()
336 };
337 format!("data:{media_type};base64,{data}")
338 }
339 DocumentSourceKind::Url(url) => url,
340 DocumentSourceKind::Raw(_) => {
341 return Err(CompletionError::RequestError(
342 "Raw file data not supported, encode as base64 first"
343 .into(),
344 ));
345 }
346 doc => {
347 return Err(CompletionError::RequestError(
348 format!("Unsupported document type: {doc}").into(),
349 ));
350 }
351 };
352 items.push(InputItem {
353 role: Some(Role::User),
354 input: InputContent::Message(Message::User {
355 content: OneOrMany::one(UserContent::InputImage {
356 image_url: url,
357 detail: detail.unwrap_or_default(),
358 }),
359 name: None,
360 }),
361 });
362 }
363 message => {
364 return Err(CompletionError::ProviderError(format!(
365 "Unsupported message: {message:?}"
366 )));
367 }
368 }
369 }
370
371 Ok(items)
372 }
373 crate::completion::Message::Assistant { id, content } => {
374 let mut reasoning_items = Vec::new();
375 let mut other_items = Vec::new();
376
377 for assistant_content in content {
378 match assistant_content {
379 crate::message::AssistantContent::Text(Text { text }) => {
380 let id = id.as_ref().unwrap_or(&String::default()).clone();
381 other_items.push(InputItem {
382 role: Some(Role::Assistant),
383 input: InputContent::Message(Message::Assistant {
384 content: OneOrMany::one(AssistantContentType::Text(
385 AssistantContent::OutputText(Text { text }),
386 )),
387 id,
388 name: None,
389 status: ToolStatus::Completed,
390 }),
391 });
392 }
393 crate::message::AssistantContent::ToolCall(crate::message::ToolCall {
394 id: tool_id,
395 call_id,
396 function,
397 ..
398 }) => {
399 other_items.push(InputItem {
400 role: None,
401 input: InputContent::FunctionCall(OutputFunctionCall {
402 arguments: function.arguments,
403 call_id: require_call_id(call_id, "Assistant tool call")?,
404 id: tool_id,
405 name: function.name,
406 status: ToolStatus::Completed,
407 }),
408 });
409 }
410 crate::message::AssistantContent::Reasoning(reasoning) => {
411 let openai_reasoning = openai_reasoning_from_core(&reasoning)
412 .map_err(|err| CompletionError::ProviderError(err.to_string()))?;
413 reasoning_items.push(InputItem {
414 role: None,
415 input: InputContent::Reasoning(openai_reasoning),
416 });
417 }
418 crate::message::AssistantContent::Image(_) => {
419 return Err(CompletionError::ProviderError(
420 "Assistant image content is not supported in OpenAI Responses API"
421 .to_string(),
422 ));
423 }
424 }
425 }
426
427 let mut items = reasoning_items;
428 items.extend(other_items);
429 Ok(items)
430 }
431 }
432 }
433}
434
435impl From<OneOrMany<String>> for Vec<ReasoningSummary> {
436 fn from(value: OneOrMany<String>) -> Self {
437 value.iter().map(|x| ReasoningSummary::new(x)).collect()
438 }
439}
440
441fn require_call_id(call_id: Option<String>, context: &str) -> Result<String, CompletionError> {
442 call_id.ok_or_else(|| {
443 CompletionError::RequestError(
444 format!("{context} `call_id` is required for OpenAI Responses API").into(),
445 )
446 })
447}
448
449fn openai_reasoning_from_core(
450 reasoning: &crate::message::Reasoning,
451) -> Result<OpenAIReasoning, MessageError> {
452 let id = reasoning.id.clone().ok_or_else(|| {
453 MessageError::ConversionError(
454 "An OpenAI-generated ID is required when using OpenAI reasoning items".to_string(),
455 )
456 })?;
457 let mut summary = Vec::new();
458 let mut encrypted_content = None;
459 for content in &reasoning.content {
460 match content {
461 crate::message::ReasoningContent::Text { text, .. }
462 | crate::message::ReasoningContent::Summary(text) => {
463 summary.push(ReasoningSummary::new(text));
464 }
465 crate::message::ReasoningContent::Encrypted(data)
468 | crate::message::ReasoningContent::Redacted { data } => {
469 encrypted_content.get_or_insert_with(|| data.clone());
470 }
471 }
472 }
473
474 Ok(OpenAIReasoning {
475 id,
476 summary,
477 encrypted_content,
478 status: None,
479 })
480}
481
482#[derive(Debug, Deserialize, Serialize, Clone)]
484pub struct ResponsesToolDefinition {
485 pub name: String,
487 pub parameters: serde_json::Value,
489 pub strict: bool,
491 #[serde(rename = "type")]
493 pub kind: String,
494 pub description: String,
496}
497
498impl From<completion::ToolDefinition> for ResponsesToolDefinition {
499 fn from(value: completion::ToolDefinition) -> Self {
500 let completion::ToolDefinition {
501 name,
502 mut parameters,
503 description,
504 } = value;
505
506 super::sanitize_schema(&mut parameters);
507
508 Self {
509 name,
510 parameters,
511 description,
512 kind: "function".to_string(),
513 strict: true,
514 }
515 }
516}
517
518#[derive(Clone, Debug, Serialize, Deserialize)]
521pub struct ResponsesUsage {
522 pub input_tokens: u64,
524 #[serde(skip_serializing_if = "Option::is_none")]
526 pub input_tokens_details: Option<InputTokensDetails>,
527 pub output_tokens: u64,
529 pub output_tokens_details: OutputTokensDetails,
531 pub total_tokens: u64,
533}
534
535impl ResponsesUsage {
536 pub(crate) fn new() -> Self {
538 Self {
539 input_tokens: 0,
540 input_tokens_details: Some(InputTokensDetails::new()),
541 output_tokens: 0,
542 output_tokens_details: OutputTokensDetails::new(),
543 total_tokens: 0,
544 }
545 }
546}
547
548impl Add for ResponsesUsage {
549 type Output = Self;
550
551 fn add(self, rhs: Self) -> Self::Output {
552 let input_tokens = self.input_tokens + rhs.input_tokens;
553 let input_tokens_details = self.input_tokens_details.map(|lhs| {
554 if let Some(tokens) = rhs.input_tokens_details {
555 lhs + tokens
556 } else {
557 lhs
558 }
559 });
560 let output_tokens = self.output_tokens + rhs.output_tokens;
561 let output_tokens_details = self.output_tokens_details + rhs.output_tokens_details;
562 let total_tokens = self.total_tokens + rhs.total_tokens;
563 Self {
564 input_tokens,
565 input_tokens_details,
566 output_tokens,
567 output_tokens_details,
568 total_tokens,
569 }
570 }
571}
572
573#[derive(Clone, Debug, Serialize, Deserialize)]
575pub struct InputTokensDetails {
576 pub cached_tokens: u64,
578}
579
580impl InputTokensDetails {
581 pub(crate) fn new() -> Self {
582 Self { cached_tokens: 0 }
583 }
584}
585
586impl Add for InputTokensDetails {
587 type Output = Self;
588 fn add(self, rhs: Self) -> Self::Output {
589 Self {
590 cached_tokens: self.cached_tokens + rhs.cached_tokens,
591 }
592 }
593}
594
595#[derive(Clone, Debug, Serialize, Deserialize)]
597pub struct OutputTokensDetails {
598 pub reasoning_tokens: u64,
600}
601
602impl OutputTokensDetails {
603 pub(crate) fn new() -> Self {
604 Self {
605 reasoning_tokens: 0,
606 }
607 }
608}
609
610impl Add for OutputTokensDetails {
611 type Output = Self;
612 fn add(self, rhs: Self) -> Self::Output {
613 Self {
614 reasoning_tokens: self.reasoning_tokens + rhs.reasoning_tokens,
615 }
616 }
617}
618
619#[derive(Clone, Debug, Default, Serialize, Deserialize)]
621pub struct IncompleteDetailsReason {
622 pub reason: String,
624}
625
626#[derive(Clone, Debug, Default, Serialize, Deserialize)]
628pub struct ResponseError {
629 pub code: String,
631 pub message: String,
633}
634
635#[derive(Clone, Debug, Deserialize, Serialize)]
637#[serde(rename_all = "snake_case")]
638pub enum ResponseObject {
639 Response,
640}
641
642#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
644#[serde(rename_all = "snake_case")]
645pub enum ResponseStatus {
646 InProgress,
647 Completed,
648 Failed,
649 Cancelled,
650 Queued,
651 Incomplete,
652}
653
654impl TryFrom<(String, crate::completion::CompletionRequest)> for CompletionRequest {
656 type Error = CompletionError;
657 fn try_from(
658 (model, req): (String, crate::completion::CompletionRequest),
659 ) -> Result<Self, Self::Error> {
660 let model = req.model.clone().unwrap_or(model);
661 let input = {
662 let mut partial_history = vec![];
663 if let Some(docs) = req.normalized_documents() {
664 partial_history.push(docs);
665 }
666 partial_history.extend(req.chat_history);
667
668 let mut full_history: Vec<InputItem> = if let Some(content) = req.preamble {
672 vec![InputItem::system_message(content)]
673 } else {
674 Vec::new()
675 };
676
677 for history_item in partial_history {
678 full_history.extend(<Vec<InputItem>>::try_from(history_item)?);
679 }
680
681 full_history
682 };
683
684 let input = OneOrMany::many(input).map_err(|_| {
685 CompletionError::RequestError(
686 "OpenAI Responses request input must contain at least one item".into(),
687 )
688 })?;
689
690 let stream = req
691 .additional_params
692 .clone()
693 .unwrap_or(Value::Null)
694 .as_bool();
695
696 let mut additional_parameters = if let Some(map) = req.additional_params {
697 serde_json::from_value::<AdditionalParameters>(map).map_err(|err| {
698 CompletionError::RequestError(
699 format!("Invalid OpenAI Responses additional_params payload: {err}").into(),
700 )
701 })?
702 } else {
703 AdditionalParameters::default()
705 };
706 if additional_parameters.reasoning.is_some() {
707 let include = additional_parameters.include.get_or_insert_with(Vec::new);
708 if !include
709 .iter()
710 .any(|item| matches!(item, Include::ReasoningEncryptedContent))
711 {
712 include.push(Include::ReasoningEncryptedContent);
713 }
714 }
715
716 if additional_parameters.text.is_none()
718 && let Some(schema) = req.output_schema
719 {
720 let name = schema
721 .as_object()
722 .and_then(|o| o.get("title"))
723 .and_then(|v| v.as_str())
724 .unwrap_or("response_schema")
725 .to_string();
726 let mut schema_value = schema.to_value();
727 super::sanitize_schema(&mut schema_value);
728 additional_parameters.text = Some(TextConfig::structured_output(name, schema_value));
729 }
730
731 let tool_choice = req.tool_choice.map(ToolChoice::try_from).transpose()?;
732
733 Ok(Self {
734 input,
735 model,
736 instructions: None, max_output_tokens: req.max_tokens,
738 stream,
739 tool_choice,
740 tools: req
741 .tools
742 .into_iter()
743 .map(ResponsesToolDefinition::from)
744 .collect(),
745 temperature: req.temperature,
746 additional_parameters,
747 })
748 }
749}
750
751#[derive(Clone)]
753pub struct ResponsesCompletionModel<T = reqwest::Client> {
754 pub(crate) client: Client<T>,
756 pub model: String,
758}
759
760impl<T> ResponsesCompletionModel<T>
761where
762 T: HttpClientExt + Clone + Default + std::fmt::Debug + 'static,
763{
764 pub fn new(client: Client<T>, model: impl Into<String>) -> Self {
766 Self {
767 client,
768 model: model.into(),
769 }
770 }
771
772 pub fn with_model(client: Client<T>, model: &str) -> Self {
773 Self {
774 client,
775 model: model.to_string(),
776 }
777 }
778
779 pub fn completions_api(self) -> crate::providers::openai::completion::CompletionModel<T> {
781 super::completion::CompletionModel::with_model(self.client.completions_api(), &self.model)
782 }
783
784 pub(crate) fn create_completion_request(
786 &self,
787 completion_request: crate::completion::CompletionRequest,
788 ) -> Result<CompletionRequest, CompletionError> {
789 let req = CompletionRequest::try_from((self.model.clone(), completion_request))?;
790
791 Ok(req)
792 }
793}
794
795#[derive(Clone, Debug, Serialize, Deserialize)]
797pub struct CompletionResponse {
798 pub id: String,
800 pub object: ResponseObject,
802 pub created_at: u64,
804 pub status: ResponseStatus,
806 pub error: Option<ResponseError>,
808 pub incomplete_details: Option<IncompleteDetailsReason>,
810 pub instructions: Option<String>,
812 pub max_output_tokens: Option<u64>,
814 pub model: String,
816 pub usage: Option<ResponsesUsage>,
818 pub output: Vec<Output>,
820 #[serde(default)]
822 pub tools: Vec<ResponsesToolDefinition>,
823 #[serde(flatten)]
825 pub additional_parameters: AdditionalParameters,
826}
827
828#[derive(Clone, Debug, Deserialize, Serialize, Default)]
831pub struct AdditionalParameters {
832 #[serde(skip_serializing_if = "Option::is_none")]
834 pub background: Option<bool>,
835 #[serde(skip_serializing_if = "Option::is_none")]
837 pub text: Option<TextConfig>,
838 #[serde(skip_serializing_if = "Option::is_none")]
840 pub include: Option<Vec<Include>>,
841 #[serde(skip_serializing_if = "Option::is_none")]
843 pub top_p: Option<f64>,
844 #[serde(skip_serializing_if = "Option::is_none")]
846 pub truncation: Option<TruncationStrategy>,
847 #[serde(skip_serializing_if = "Option::is_none")]
849 pub user: Option<String>,
850 #[serde(skip_serializing_if = "Map::is_empty", default)]
852 pub metadata: serde_json::Map<String, serde_json::Value>,
853 #[serde(skip_serializing_if = "Option::is_none")]
855 pub parallel_tool_calls: Option<bool>,
856 #[serde(skip_serializing_if = "Option::is_none")]
858 pub previous_response_id: Option<String>,
859 #[serde(skip_serializing_if = "Option::is_none")]
861 pub reasoning: Option<Reasoning>,
862 #[serde(skip_serializing_if = "Option::is_none")]
864 pub service_tier: Option<OpenAIServiceTier>,
865 #[serde(skip_serializing_if = "Option::is_none")]
867 pub store: Option<bool>,
868}
869
870impl AdditionalParameters {
871 pub fn to_json(self) -> serde_json::Value {
872 serde_json::to_value(self).unwrap_or_else(|_| serde_json::Value::Object(Map::new()))
873 }
874}
875
876#[derive(Clone, Debug, Default, Serialize, Deserialize)]
880#[serde(rename_all = "snake_case")]
881pub enum TruncationStrategy {
882 Auto,
883 #[default]
884 Disabled,
885}
886
887#[derive(Clone, Debug, Serialize, Deserialize)]
890pub struct TextConfig {
891 pub format: TextFormat,
892}
893
894impl TextConfig {
895 pub(crate) fn structured_output<S>(name: S, schema: serde_json::Value) -> Self
896 where
897 S: Into<String>,
898 {
899 Self {
900 format: TextFormat::JsonSchema(StructuredOutputsInput {
901 name: name.into(),
902 schema,
903 strict: true,
904 }),
905 }
906 }
907}
908
909#[derive(Clone, Debug, Serialize, Deserialize, Default)]
912#[serde(tag = "type")]
913#[serde(rename_all = "snake_case")]
914pub enum TextFormat {
915 JsonSchema(StructuredOutputsInput),
916 #[default]
917 Text,
918}
919
920#[derive(Clone, Debug, Serialize, Deserialize)]
922pub struct StructuredOutputsInput {
923 pub name: String,
925 pub schema: serde_json::Value,
927 pub strict: bool,
929}
930
931#[derive(Clone, Debug, Default, Serialize, Deserialize)]
933pub struct Reasoning {
934 pub effort: Option<ReasoningEffort>,
936 #[serde(skip_serializing_if = "Option::is_none")]
938 pub summary: Option<ReasoningSummaryLevel>,
939}
940
941impl Reasoning {
942 pub fn new() -> Self {
944 Self {
945 effort: None,
946 summary: None,
947 }
948 }
949
950 pub fn with_effort(mut self, reasoning_effort: ReasoningEffort) -> Self {
952 self.effort = Some(reasoning_effort);
953
954 self
955 }
956
957 pub fn with_summary_level(mut self, reasoning_summary_level: ReasoningSummaryLevel) -> Self {
959 self.summary = Some(reasoning_summary_level);
960
961 self
962 }
963}
964
965#[derive(Clone, Debug, Default, Serialize, Deserialize)]
967#[serde(rename_all = "snake_case")]
968pub enum OpenAIServiceTier {
969 #[default]
970 Auto,
971 Default,
972 Flex,
973}
974
975#[derive(Clone, Debug, Default, Serialize, Deserialize)]
977#[serde(rename_all = "snake_case")]
978pub enum ReasoningEffort {
979 None,
980 Minimal,
981 Low,
982 #[default]
983 Medium,
984 High,
985 Xhigh,
986}
987
988#[derive(Clone, Debug, Default, Serialize, Deserialize)]
990#[serde(rename_all = "snake_case")]
991pub enum ReasoningSummaryLevel {
992 #[default]
993 Auto,
994 Concise,
995 Detailed,
996}
997
998#[derive(Clone, Debug, Deserialize, Serialize)]
1001pub enum Include {
1002 #[serde(rename = "file_search_call.results")]
1003 FileSearchCallResults,
1004 #[serde(rename = "message.input_image.image_url")]
1005 MessageInputImageImageUrl,
1006 #[serde(rename = "computer_call.output.image_url")]
1007 ComputerCallOutputOutputImageUrl,
1008 #[serde(rename = "reasoning.encrypted_content")]
1009 ReasoningEncryptedContent,
1010 #[serde(rename = "code_interpreter_call.outputs")]
1011 CodeInterpreterCallOutputs,
1012}
1013
1014#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1016#[serde(tag = "type")]
1017#[serde(rename_all = "snake_case")]
1018pub enum Output {
1019 Message(OutputMessage),
1020 #[serde(alias = "function_call")]
1021 FunctionCall(OutputFunctionCall),
1022 Reasoning {
1023 id: String,
1024 summary: Vec<ReasoningSummary>,
1025 #[serde(default)]
1026 encrypted_content: Option<String>,
1027 #[serde(default)]
1028 status: Option<ToolStatus>,
1029 },
1030}
1031
1032impl From<Output> for Vec<completion::AssistantContent> {
1033 fn from(value: Output) -> Self {
1034 let res: Vec<completion::AssistantContent> = match value {
1035 Output::Message(OutputMessage { content, .. }) => content
1036 .into_iter()
1037 .map(completion::AssistantContent::from)
1038 .collect(),
1039 Output::FunctionCall(OutputFunctionCall {
1040 id,
1041 arguments,
1042 call_id,
1043 name,
1044 ..
1045 }) => vec![completion::AssistantContent::tool_call_with_call_id(
1046 id, call_id, name, arguments,
1047 )],
1048 Output::Reasoning {
1049 id,
1050 summary,
1051 encrypted_content,
1052 ..
1053 } => {
1054 let mut content = summary
1055 .into_iter()
1056 .map(|summary| match summary {
1057 ReasoningSummary::SummaryText { text } => {
1058 message::ReasoningContent::Summary(text)
1059 }
1060 })
1061 .collect::<Vec<_>>();
1062 if let Some(encrypted_content) = encrypted_content {
1063 content.push(message::ReasoningContent::Encrypted(encrypted_content));
1064 }
1065 vec![completion::AssistantContent::Reasoning(
1066 message::Reasoning {
1067 id: Some(id),
1068 content,
1069 },
1070 )]
1071 }
1072 };
1073
1074 res
1075 }
1076}
1077
1078#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1079pub struct OutputReasoning {
1080 id: String,
1081 summary: Vec<ReasoningSummary>,
1082 status: ToolStatus,
1083}
1084
1085#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1087pub struct OutputFunctionCall {
1088 pub id: String,
1089 #[serde(with = "json_utils::stringified_json")]
1090 pub arguments: serde_json::Value,
1091 pub call_id: String,
1092 pub name: String,
1093 pub status: ToolStatus,
1094}
1095
1096#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1098#[serde(rename_all = "snake_case")]
1099pub enum ToolStatus {
1100 InProgress,
1101 Completed,
1102 Incomplete,
1103}
1104
1105#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1107pub struct OutputMessage {
1108 pub id: String,
1110 pub role: OutputRole,
1112 pub status: ResponseStatus,
1114 pub content: Vec<AssistantContent>,
1116}
1117
1118#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1120#[serde(rename_all = "snake_case")]
1121pub enum OutputRole {
1122 Assistant,
1123}
1124
1125impl<T> completion::CompletionModel for ResponsesCompletionModel<T>
1126where
1127 T: HttpClientExt
1128 + Clone
1129 + std::fmt::Debug
1130 + Default
1131 + WasmCompatSend
1132 + WasmCompatSync
1133 + 'static,
1134{
1135 type Response = CompletionResponse;
1136 type StreamingResponse = StreamingCompletionResponse;
1137
1138 type Client = super::Client<T>;
1139
1140 fn make(client: &Self::Client, model: impl Into<String>) -> Self {
1141 Self::new(client.clone(), model)
1142 }
1143
1144 async fn completion(
1145 &self,
1146 completion_request: crate::completion::CompletionRequest,
1147 ) -> Result<completion::CompletionResponse<Self::Response>, CompletionError> {
1148 let span = if tracing::Span::current().is_disabled() {
1149 info_span!(
1150 target: "rig::completions",
1151 "chat",
1152 gen_ai.operation.name = "chat",
1153 gen_ai.provider.name = tracing::field::Empty,
1154 gen_ai.request.model = tracing::field::Empty,
1155 gen_ai.response.id = tracing::field::Empty,
1156 gen_ai.response.model = tracing::field::Empty,
1157 gen_ai.usage.output_tokens = tracing::field::Empty,
1158 gen_ai.usage.input_tokens = tracing::field::Empty,
1159 gen_ai.input.messages = tracing::field::Empty,
1160 gen_ai.output.messages = tracing::field::Empty,
1161 )
1162 } else {
1163 tracing::Span::current()
1164 };
1165
1166 span.record("gen_ai.provider.name", "openai");
1167 span.record("gen_ai.request.model", &self.model);
1168 let request = self.create_completion_request(completion_request)?;
1169 let body = serde_json::to_vec(&request)?;
1170
1171 if enabled!(Level::TRACE) {
1172 tracing::trace!(
1173 target: "rig::completions",
1174 "OpenAI Responses completion request: {request}",
1175 request = serde_json::to_string_pretty(&request)?
1176 );
1177 }
1178
1179 let req = self
1180 .client
1181 .post("/responses")?
1182 .body(body)
1183 .map_err(|e| CompletionError::HttpError(e.into()))?;
1184
1185 async move {
1186 let response = self.client.send(req).await?;
1187
1188 if response.status().is_success() {
1189 let t = http_client::text(response).await?;
1190 let response = serde_json::from_str::<Self::Response>(&t)?;
1191 let span = tracing::Span::current();
1192 span.record("gen_ai.response.id", &response.id);
1193 span.record("gen_ai.response.model", &response.model);
1194 if let Some(ref usage) = response.usage {
1195 span.record("gen_ai.usage.output_tokens", usage.output_tokens);
1196 span.record("gen_ai.usage.input_tokens", usage.input_tokens);
1197 }
1198 if enabled!(Level::TRACE) {
1199 tracing::trace!(
1200 target: "rig::completions",
1201 "OpenAI Responses completion response: {response}",
1202 response = serde_json::to_string_pretty(&response)?
1203 );
1204 }
1205 response.try_into()
1206 } else {
1207 let text = http_client::text(response).await?;
1208 Err(CompletionError::ProviderError(text))
1209 }
1210 }
1211 .instrument(span)
1212 .await
1213 }
1214
1215 async fn stream(
1216 &self,
1217 request: crate::completion::CompletionRequest,
1218 ) -> Result<
1219 crate::streaming::StreamingCompletionResponse<Self::StreamingResponse>,
1220 CompletionError,
1221 > {
1222 ResponsesCompletionModel::stream(self, request).await
1223 }
1224}
1225
1226impl TryFrom<CompletionResponse> for completion::CompletionResponse<CompletionResponse> {
1227 type Error = CompletionError;
1228
1229 fn try_from(response: CompletionResponse) -> Result<Self, Self::Error> {
1230 if response.output.is_empty() {
1231 return Err(CompletionError::ResponseError(
1232 "Response contained no parts".to_owned(),
1233 ));
1234 }
1235
1236 let message_id = response.output.iter().find_map(|item| match item {
1238 Output::Message(msg) => Some(msg.id.clone()),
1239 _ => None,
1240 });
1241
1242 let content: Vec<completion::AssistantContent> = response
1243 .output
1244 .iter()
1245 .cloned()
1246 .flat_map(<Vec<completion::AssistantContent>>::from)
1247 .collect();
1248
1249 let choice = OneOrMany::many(content).map_err(|_| {
1250 CompletionError::ResponseError(
1251 "Response contained no message or tool call (empty)".to_owned(),
1252 )
1253 })?;
1254
1255 let usage = response
1256 .usage
1257 .as_ref()
1258 .map(|usage| completion::Usage {
1259 input_tokens: usage.input_tokens,
1260 output_tokens: usage.output_tokens,
1261 total_tokens: usage.total_tokens,
1262 cached_input_tokens: usage
1263 .input_tokens_details
1264 .as_ref()
1265 .map(|d| d.cached_tokens)
1266 .unwrap_or(0),
1267 })
1268 .unwrap_or_default();
1269
1270 Ok(completion::CompletionResponse {
1271 choice,
1272 usage,
1273 raw_response: response,
1274 message_id,
1275 })
1276 }
1277}
1278
1279#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1281#[serde(tag = "role", rename_all = "lowercase")]
1282pub enum Message {
1283 #[serde(alias = "developer")]
1284 System {
1285 #[serde(deserialize_with = "string_or_one_or_many")]
1286 content: OneOrMany<SystemContent>,
1287 #[serde(skip_serializing_if = "Option::is_none")]
1288 name: Option<String>,
1289 },
1290 User {
1291 #[serde(deserialize_with = "string_or_one_or_many")]
1292 content: OneOrMany<UserContent>,
1293 #[serde(skip_serializing_if = "Option::is_none")]
1294 name: Option<String>,
1295 },
1296 Assistant {
1297 content: OneOrMany<AssistantContentType>,
1298 #[serde(skip_serializing_if = "String::is_empty")]
1299 id: String,
1300 #[serde(skip_serializing_if = "Option::is_none")]
1301 name: Option<String>,
1302 status: ToolStatus,
1303 },
1304 #[serde(rename = "tool")]
1305 ToolResult {
1306 tool_call_id: String,
1307 output: String,
1308 },
1309}
1310
1311#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)]
1313#[serde(rename_all = "lowercase")]
1314pub enum ToolResultContentType {
1315 #[default]
1316 Text,
1317}
1318
1319impl Message {
1320 pub fn system(content: &str) -> Self {
1321 Message::System {
1322 content: OneOrMany::one(content.to_owned().into()),
1323 name: None,
1324 }
1325 }
1326}
1327
1328#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1331#[serde(tag = "type", rename_all = "snake_case")]
1332pub enum AssistantContent {
1333 OutputText(Text),
1334 Refusal { refusal: String },
1335}
1336
1337impl From<AssistantContent> for completion::AssistantContent {
1338 fn from(value: AssistantContent) -> Self {
1339 match value {
1340 AssistantContent::Refusal { refusal } => {
1341 completion::AssistantContent::Text(Text { text: refusal })
1342 }
1343 AssistantContent::OutputText(Text { text }) => {
1344 completion::AssistantContent::Text(Text { text })
1345 }
1346 }
1347 }
1348}
1349
1350#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1352#[serde(untagged)]
1353pub enum AssistantContentType {
1354 Text(AssistantContent),
1355 ToolCall(OutputFunctionCall),
1356 Reasoning(OpenAIReasoning),
1357}
1358
1359#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1362#[serde(tag = "type", rename_all = "snake_case")]
1363pub enum SystemContent {
1364 InputText { text: String },
1365}
1366
1367impl From<String> for SystemContent {
1368 fn from(s: String) -> Self {
1369 SystemContent::InputText { text: s }
1370 }
1371}
1372
1373impl std::str::FromStr for SystemContent {
1374 type Err = std::convert::Infallible;
1375
1376 fn from_str(s: &str) -> Result<Self, Self::Err> {
1377 Ok(SystemContent::InputText {
1378 text: s.to_string(),
1379 })
1380 }
1381}
1382
1383#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1385#[serde(tag = "type", rename_all = "snake_case")]
1386pub enum UserContent {
1387 InputText {
1388 text: String,
1389 },
1390 InputImage {
1391 image_url: String,
1392 #[serde(default)]
1393 detail: ImageDetail,
1394 },
1395 InputFile {
1396 #[serde(skip_serializing_if = "Option::is_none")]
1397 file_url: Option<String>,
1398 #[serde(skip_serializing_if = "Option::is_none")]
1399 file_data: Option<String>,
1400 #[serde(skip_serializing_if = "Option::is_none")]
1401 filename: Option<String>,
1402 },
1403 Audio {
1404 input_audio: InputAudio,
1405 },
1406 #[serde(rename = "tool")]
1407 ToolResult {
1408 tool_call_id: String,
1409 output: String,
1410 },
1411}
1412
1413impl TryFrom<message::Message> for Vec<Message> {
1414 type Error = message::MessageError;
1415
1416 fn try_from(message: message::Message) -> Result<Self, Self::Error> {
1417 match message {
1418 message::Message::User { content } => {
1419 let (tool_results, other_content): (Vec<_>, Vec<_>) = content
1420 .into_iter()
1421 .partition(|content| matches!(content, message::UserContent::ToolResult(_)));
1422
1423 if !tool_results.is_empty() {
1426 tool_results
1427 .into_iter()
1428 .map(|content| match content {
1429 message::UserContent::ToolResult(message::ToolResult {
1430 call_id,
1431 content,
1432 ..
1433 }) => Ok::<_, message::MessageError>(Message::ToolResult {
1434 tool_call_id: call_id.ok_or_else(|| {
1435 MessageError::ConversionError(
1436 "Tool result `call_id` is required for OpenAI Responses API"
1437 .into(),
1438 )
1439 })?,
1440 output: {
1441 let res = content.first();
1442 match res {
1443 completion::message::ToolResultContent::Text(Text {
1444 text,
1445 }) => text,
1446 _ => return Err(MessageError::ConversionError("This API only currently supports text tool results".into()))
1447 }
1448 },
1449 }),
1450 _ => unreachable!(),
1451 })
1452 .collect::<Result<Vec<_>, _>>()
1453 } else {
1454 let other_content = other_content
1455 .into_iter()
1456 .map(|content| match content {
1457 message::UserContent::Text(message::Text { text }) => {
1458 Ok(UserContent::InputText { text })
1459 }
1460 message::UserContent::Image(message::Image {
1461 data,
1462 detail,
1463 media_type,
1464 ..
1465 }) => {
1466 let url = match data {
1467 DocumentSourceKind::Base64(data) => {
1468 let media_type = if let Some(media_type) = media_type {
1469 media_type.to_mime_type().to_string()
1470 } else {
1471 String::new()
1472 };
1473 format!("data:{media_type};base64,{data}")
1474 }
1475 DocumentSourceKind::Url(url) => url,
1476 DocumentSourceKind::Raw(_) => {
1477 return Err(MessageError::ConversionError(
1478 "Raw files not supported, encode as base64 first"
1479 .into(),
1480 ));
1481 }
1482 doc => {
1483 return Err(MessageError::ConversionError(format!(
1484 "Unsupported document type: {doc}"
1485 )));
1486 }
1487 };
1488
1489 Ok(UserContent::InputImage {
1490 image_url: url,
1491 detail: detail.unwrap_or_default(),
1492 })
1493 }
1494 message::UserContent::Document(message::Document {
1495 media_type: Some(DocumentMediaType::PDF),
1496 data,
1497 ..
1498 }) => {
1499 let (file_data, file_url) = match data {
1500 DocumentSourceKind::Base64(data) => {
1501 (Some(format!("data:application/pdf;base64,{data}")), None)
1502 }
1503 DocumentSourceKind::Url(url) => (None, Some(url)),
1504 DocumentSourceKind::Raw(_) => {
1505 return Err(MessageError::ConversionError(
1506 "Raw files not supported, encode as base64 first"
1507 .into(),
1508 ));
1509 }
1510 doc => {
1511 return Err(MessageError::ConversionError(format!(
1512 "Unsupported document type: {doc}"
1513 )));
1514 }
1515 };
1516
1517 Ok(UserContent::InputFile {
1518 file_url,
1519 file_data,
1520 filename: Some("document.pdf".into()),
1521 })
1522 }
1523 message::UserContent::Document(message::Document {
1524 data: DocumentSourceKind::Base64(text),
1525 ..
1526 }) => Ok(UserContent::InputText { text }),
1527 message::UserContent::Audio(message::Audio {
1528 data: DocumentSourceKind::Base64(data),
1529 media_type,
1530 ..
1531 }) => Ok(UserContent::Audio {
1532 input_audio: InputAudio {
1533 data,
1534 format: match media_type {
1535 Some(media_type) => media_type,
1536 None => AudioMediaType::MP3,
1537 },
1538 },
1539 }),
1540 message::UserContent::Audio(_) => Err(MessageError::ConversionError(
1541 "Audio must be base64 encoded data".into(),
1542 )),
1543 _ => unreachable!(),
1544 })
1545 .collect::<Result<Vec<_>, _>>()?;
1546
1547 let other_content = OneOrMany::many(other_content).map_err(|_| {
1548 MessageError::ConversionError(
1549 "User message did not contain OpenAI Responses-compatible content"
1550 .to_string(),
1551 )
1552 })?;
1553
1554 Ok(vec![Message::User {
1555 content: other_content,
1556 name: None,
1557 }])
1558 }
1559 }
1560 message::Message::Assistant { content, id } => {
1561 let assistant_message_id = id.ok_or_else(|| {
1562 MessageError::ConversionError(
1563 "Assistant message ID is required for OpenAI Responses API".into(),
1564 )
1565 })?;
1566
1567 match content.first() {
1568 crate::message::AssistantContent::Text(Text { text }) => {
1569 Ok(vec![Message::Assistant {
1570 id: assistant_message_id.clone(),
1571 status: ToolStatus::Completed,
1572 content: OneOrMany::one(AssistantContentType::Text(
1573 AssistantContent::OutputText(Text { text }),
1574 )),
1575 name: None,
1576 }])
1577 }
1578 crate::message::AssistantContent::ToolCall(crate::message::ToolCall {
1579 id,
1580 call_id,
1581 function,
1582 ..
1583 }) => Ok(vec![Message::Assistant {
1584 content: OneOrMany::one(AssistantContentType::ToolCall(
1585 OutputFunctionCall {
1586 call_id: call_id.ok_or_else(|| {
1587 MessageError::ConversionError(
1588 "Tool call `call_id` is required for OpenAI Responses API"
1589 .into(),
1590 )
1591 })?,
1592 arguments: function.arguments,
1593 id,
1594 name: function.name,
1595 status: ToolStatus::Completed,
1596 },
1597 )),
1598 id: assistant_message_id.clone(),
1599 name: None,
1600 status: ToolStatus::Completed,
1601 }]),
1602 crate::message::AssistantContent::Reasoning(reasoning) => {
1603 let openai_reasoning = openai_reasoning_from_core(&reasoning)?;
1604 Ok(vec![Message::Assistant {
1605 content: OneOrMany::one(AssistantContentType::Reasoning(
1606 openai_reasoning,
1607 )),
1608 id: assistant_message_id,
1609 name: None,
1610 status: ToolStatus::Completed,
1611 }])
1612 }
1613 crate::message::AssistantContent::Image(_) => {
1614 Err(MessageError::ConversionError(
1615 "Assistant image content is not supported in OpenAI Responses API"
1616 .into(),
1617 ))
1618 }
1619 }
1620 }
1621 }
1622 }
1623}
1624
1625impl FromStr for UserContent {
1626 type Err = Infallible;
1627
1628 fn from_str(s: &str) -> Result<Self, Self::Err> {
1629 Ok(UserContent::InputText {
1630 text: s.to_string(),
1631 })
1632 }
1633}