1use crate::completion::CompletionRequest as CoreCompletionRequest;
9use crate::error::EncodeError;
10use crate::json_utils::string_or_vec;
11use crate::message::{AudioMediaType, DocumentSourceKind, ImageDetail, MimeType};
12use crate::{completion, json_utils, message};
13use serde::{Deserialize, Serialize, Serializer};
14use std::convert::Infallible;
15use std::fmt;
16use std::str::FromStr;
17
18fn serialize_user_content<S>(content: &[UserContent], serializer: S) -> Result<S::Ok, S::Error>
21where
22 S: Serializer,
23{
24 if content.len() == 1
25 && let Some(UserContent::Text { text, .. }) = content.first()
26 {
27 return serializer.serialize_str(text);
28 }
29 content.serialize(serializer)
30}
31
32pub const GPT_6_ASTRA: &str = "gpt-6-astra";
37
38pub const GPT_6_1_SOL: &str = "gpt-6.1-sol";
43
44pub const GPT_6_SOL: &str = "gpt-6-sol";
49
50pub const GPT_6_LUNA: &str = "gpt-6-luna";
55
56pub const GPT_5_6: &str = "gpt-5.6";
58
59pub const GPT_5_6_SOL: &str = "gpt-5.6-sol";
61
62pub const GPT_5_6_TERRA: &str = "gpt-5.6-terra";
64
65pub const GPT_5_6_LUNA: &str = "gpt-5.6-luna";
67
68pub const GPT_5_5: &str = "gpt-5.5";
70
71pub const GPT_5_4: &str = "gpt-5.4";
73
74pub const GPT_5_4_MINI: &str = "gpt-5.4-mini";
76
77pub const GPT_5_4_NANO: &str = "gpt-5.4-nano";
79
80pub const GPT_5_2: &str = "gpt-5.2";
82
83pub const GPT_5_2_PRO: &str = "gpt-5.2-pro";
86
87pub const GPT_5_1: &str = "gpt-5.1";
89
90pub const GPT_5: &str = "gpt-5";
92pub const GPT_5_MINI: &str = "gpt-5-mini";
94pub const GPT_5_NANO: &str = "gpt-5-nano";
96
97pub const GPT_4_5_PREVIEW: &str = "gpt-4.5-preview";
99pub const GPT_4_5_PREVIEW_2025_02_27: &str = "gpt-4.5-preview-2025-02-27";
101pub const GPT_4O_2024_11_20: &str = "gpt-4o-2024-11-20";
103pub const GPT_4O: &str = "gpt-4o";
105pub const GPT_4O_MINI: &str = "gpt-4o-mini";
107pub const GPT_4O_2024_05_13: &str = "gpt-4o-2024-05-13";
109pub const GPT_4_TURBO: &str = "gpt-4-turbo";
111pub const GPT_4_TURBO_2024_04_09: &str = "gpt-4-turbo-2024-04-09";
113pub const GPT_4_TURBO_PREVIEW: &str = "gpt-4-turbo-preview";
115pub const GPT_4_0125_PREVIEW: &str = "gpt-4-0125-preview";
117pub const GPT_4_1106_PREVIEW: &str = "gpt-4-1106-preview";
119pub const GPT_4_VISION_PREVIEW: &str = "gpt-4-vision-preview";
121pub const GPT_4_1106_VISION_PREVIEW: &str = "gpt-4-1106-vision-preview";
123pub const GPT_4: &str = "gpt-4";
125pub const GPT_4_0613: &str = "gpt-4-0613";
127pub const GPT_4_32K: &str = "gpt-4-32k";
129pub const GPT_4_32K_0613: &str = "gpt-4-32k-0613";
131
132pub const O4_MINI_2025_04_16: &str = "o4-mini-2025-04-16";
134pub const O4_MINI: &str = "o4-mini";
136pub const O3: &str = "o3";
138pub const O3_MINI: &str = "o3-mini";
140pub const O3_MINI_2025_01_31: &str = "o3-mini-2025-01-31";
142pub const O1_PRO: &str = "o1-pro";
144pub const O1: &str = "o1";
146pub const O1_2024_12_17: &str = "o1-2024-12-17";
148pub const O1_PREVIEW: &str = "o1-preview";
150pub const O1_PREVIEW_2024_09_12: &str = "o1-preview-2024-09-12";
152pub const O1_MINI: &str = "o1-mini";
154pub const O1_MINI_2024_09_12: &str = "o1-mini-2024-09-12";
156
157pub const GPT_4_1_MINI: &str = "gpt-4.1-mini";
159pub const GPT_4_1_NANO: &str = "gpt-4.1-nano";
161pub const GPT_4_1_2025_04_14: &str = "gpt-4.1-2025-04-14";
163pub const GPT_4_1: &str = "gpt-4.1";
165
166#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
167#[serde(tag = "role", rename_all = "lowercase")]
168pub enum Message {
169 #[serde(alias = "developer")]
170 System {
171 #[serde(deserialize_with = "string_or_vec")]
172 content: Vec<SystemContent>,
173 #[serde(skip_serializing_if = "Option::is_none")]
174 name: Option<String>,
175 },
176 User {
177 #[serde(
178 deserialize_with = "string_or_vec",
179 serialize_with = "serialize_user_content"
180 )]
181 content: Vec<UserContent>,
182 #[serde(skip_serializing_if = "Option::is_none")]
183 name: Option<String>,
184 },
185 #[serde(alias = "model", deserialize_with = "deserialize_assistant")]
188 Assistant {
189 #[serde(
190 skip_serializing_if = "Vec::is_empty",
191 serialize_with = "serialize_assistant_content_vec"
192 )]
193 content: Vec<AssistantContent>,
194 #[serde(skip_serializing_if = "Option::is_none", rename = "reasoning_content")]
199 reasoning: Option<String>,
200 #[serde(skip_serializing_if = "Option::is_none")]
201 refusal: Option<String>,
202 #[serde(skip_serializing_if = "Option::is_none")]
203 name: Option<String>,
204 #[serde(skip_serializing_if = "Vec::is_empty")]
205 tool_calls: Vec<ToolCall>,
206 #[serde(skip_serializing_if = "Vec::is_empty")]
210 reasoning_details: Vec<ReasoningDetails>,
211 },
212 #[serde(rename = "tool")]
213 ToolResult {
214 tool_call_id: String,
215 content: ToolResultContentValue,
216 },
217}
218
219#[derive(Deserialize)]
223struct AssistantMessageWire {
224 #[serde(default, deserialize_with = "json_utils::string_or_vec")]
225 content: Vec<AssistantContent>,
226 #[serde(default)]
227 reasoning_content: Option<String>,
228 #[serde(default)]
229 reasoning: Option<String>,
230 #[serde(default)]
231 refusal: Option<String>,
232 #[serde(default)]
233 name: Option<String>,
234 #[serde(default, deserialize_with = "json_utils::null_or_default")]
235 tool_calls: Vec<ToolCall>,
236 #[serde(default)]
237 reasoning_details: Vec<ReasoningDetails>,
238}
239
240type AssistantFields = (
242 Vec<AssistantContent>,
243 Option<String>,
244 Option<String>,
245 Option<String>,
246 Vec<ToolCall>,
247 Vec<ReasoningDetails>,
248);
249
250fn deserialize_assistant<'de, D>(deserializer: D) -> Result<AssistantFields, D::Error>
253where
254 D: serde::Deserializer<'de>,
255{
256 let wire = AssistantMessageWire::deserialize(deserializer)?;
257 Ok((
258 wire.content,
259 wire.reasoning_content.or(wire.reasoning),
260 wire.refusal,
261 wire.name,
262 wire.tool_calls,
263 wire.reasoning_details,
264 ))
265}
266
267impl Message {
268 pub fn system(content: &str) -> Self {
269 Message::System {
270 content: vec![content.to_owned().into()],
271 name: None,
272 }
273 }
274}
275
276fn history_contains_tool_result(messages: &[Message]) -> bool {
277 messages
278 .iter()
279 .any(|message| matches!(message, Message::ToolResult { .. }))
280}
281
282#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
288#[serde(tag = "type", rename_all = "snake_case")]
289pub enum ReasoningDetails {
290 #[serde(rename = "reasoning.summary")]
291 Summary {
292 id: Option<String>,
293 format: Option<String>,
294 index: Option<usize>,
295 summary: String,
296 },
297 #[serde(rename = "reasoning.encrypted")]
298 Encrypted {
299 id: Option<String>,
300 format: Option<String>,
301 index: Option<usize>,
302 data: String,
303 },
304 #[serde(rename = "reasoning.text")]
305 Text {
306 id: Option<String>,
307 format: Option<String>,
308 index: Option<usize>,
309 text: Option<String>,
310 signature: Option<String>,
311 },
312}
313
314#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
315pub struct SystemContent {
316 #[serde(default)]
317 pub r#type: SystemContentType,
318 pub text: String,
319}
320
321#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)]
322#[serde(rename_all = "lowercase")]
323pub enum SystemContentType {
324 #[default]
325 Text,
326}
327
328#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
329#[serde(tag = "type", rename_all = "lowercase")]
330pub enum AssistantContent {
331 Text { text: String },
332 Refusal { refusal: String },
333}
334
335impl From<AssistantContent> for completion::AssistantContent {
336 fn from(value: AssistantContent) -> Self {
337 match value {
338 AssistantContent::Text { text, .. } => completion::AssistantContent::text(text),
339 AssistantContent::Refusal { refusal } => completion::AssistantContent::text(refusal),
340 }
341 }
342}
343
344#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
345#[serde(tag = "type", rename_all = "lowercase")]
346pub enum UserContent {
347 Text {
348 text: String,
349 },
350 #[serde(rename = "image_url")]
351 Image {
352 image_url: ImageUrl,
353 },
354 #[serde(rename = "input_audio")]
356 Audio {
357 input_audio: InputAudio,
358 },
359 File {
365 file: FileData,
366 },
367 #[serde(rename = "video_url")]
370 Video {
371 video_url: VideoUrl,
372 },
373}
374
375#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
376pub struct ImageUrl {
377 pub url: String,
378 #[serde(default, skip_serializing_if = "Option::is_none")]
381 pub detail: Option<ImageDetail>,
382}
383
384#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
389pub struct VideoUrl {
390 pub url: String,
391}
392
393#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
394pub struct InputAudio {
395 pub data: String,
396 pub format: AudioMediaType,
397}
398
399#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
405pub struct FileData {
406 #[serde(skip_serializing_if = "Option::is_none")]
409 pub file_data: Option<String>,
410 #[serde(skip_serializing_if = "Option::is_none")]
412 pub file_id: Option<String>,
413 #[serde(skip_serializing_if = "Option::is_none")]
415 pub filename: Option<String>,
416}
417
418#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
422#[serde(tag = "type")]
423pub enum ToolResultContent {
424 #[serde(rename = "text")]
425 Text { text: String },
426 #[serde(rename = "image_url")]
427 Image { image_url: ImageUrl },
428}
429
430impl ToolResultContent {
431 pub fn as_text(&self) -> Option<&str> {
433 match self {
434 Self::Text { text } => Some(text.as_str()),
435 Self::Image { .. } => None,
436 }
437 }
438}
439
440impl FromStr for ToolResultContent {
441 type Err = Infallible;
442
443 fn from_str(s: &str) -> Result<Self, Self::Err> {
444 Ok(s.to_owned().into())
445 }
446}
447
448impl From<String> for ToolResultContent {
449 fn from(s: String) -> Self {
450 ToolResultContent::Text { text: s }
451 }
452}
453
454#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
455#[serde(untagged)]
456pub enum ToolResultContentValue {
457 Array(Vec<ToolResultContent>),
458 String(String),
459}
460
461impl ToolResultContentValue {
462 pub fn as_text(&self) -> String {
464 match self {
465 ToolResultContentValue::Array(arr) => arr
466 .iter()
467 .filter_map(ToolResultContent::as_text)
468 .collect::<Vec<_>>()
469 .join("\n"),
470 ToolResultContentValue::String(s) => s.clone(),
471 }
472 }
473
474 pub fn has_image(&self) -> bool {
476 matches!(self, ToolResultContentValue::Array(arr)
477 if arr.iter().any(|c| matches!(c, ToolResultContent::Image { .. })))
478 }
479
480 pub fn to_array(&self) -> Self {
481 match self {
482 ToolResultContentValue::Array(_) => self.clone(),
483 ToolResultContentValue::String(s) => {
484 ToolResultContentValue::Array(vec![ToolResultContent::from(s.clone())])
485 }
486 }
487 }
488}
489
490#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
491pub struct ToolCall {
492 pub id: String,
493 #[serde(default)]
494 pub r#type: ToolType,
495 pub function: Function,
496}
497
498#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)]
499#[serde(rename_all = "lowercase")]
500pub enum ToolType {
501 #[default]
502 Function,
503}
504
505#[derive(Debug, Deserialize, Serialize, Clone)]
507pub struct FunctionDefinition {
508 pub name: String,
509 pub description: String,
510 pub parameters: serde_json::Value,
511 #[serde(skip_serializing_if = "Option::is_none")]
512 pub strict: Option<bool>,
513}
514
515#[derive(Debug, Deserialize, Serialize, Clone)]
516pub struct ToolDefinition {
517 pub r#type: String,
518 pub function: FunctionDefinition,
519}
520
521impl From<completion::ToolDefinition> for ToolDefinition {
522 fn from(tool: completion::ToolDefinition) -> Self {
523 Self {
524 r#type: "function".into(),
525 function: FunctionDefinition {
526 name: tool.name,
527 description: tool.description,
528 parameters: tool.parameters,
529 strict: None,
530 },
531 }
532 }
533}
534
535impl ToolDefinition {
536 pub fn with_strict(mut self) -> Self {
539 self.function.strict = Some(true);
540 super::sanitize_schema(&mut self.function.parameters);
541 self
542 }
543}
544
545#[derive(Default, Clone, Debug, PartialEq)]
546pub enum ToolChoice {
547 #[default]
548 Auto,
549 None,
550 Required,
551 Function {
554 name: String,
555 },
556}
557
558#[derive(Deserialize, Serialize)]
559struct ToolChoiceFunctionName {
560 name: String,
561}
562
563#[derive(Deserialize, Serialize)]
564#[serde(tag = "type", rename_all = "snake_case")]
565enum ToolChoiceFunctionRepr {
566 Function { function: ToolChoiceFunctionName },
567}
568
569impl Serialize for ToolChoice {
570 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
571 match self {
572 Self::Auto => serializer.serialize_str("auto"),
573 Self::None => serializer.serialize_str("none"),
574 Self::Required => serializer.serialize_str("required"),
575 Self::Function { name } => ToolChoiceFunctionRepr::Function {
576 function: ToolChoiceFunctionName { name: name.clone() },
577 }
578 .serialize(serializer),
579 }
580 }
581}
582
583impl<'de> Deserialize<'de> for ToolChoice {
584 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
585 #[derive(Deserialize)]
586 #[serde(untagged)]
587 enum Repr {
588 Mode(String),
589 Function(ToolChoiceFunctionRepr),
590 }
591
592 match Repr::deserialize(deserializer)? {
593 Repr::Mode(mode) => match mode.as_str() {
594 "auto" => Ok(Self::Auto),
595 "none" => Ok(Self::None),
596 "required" => Ok(Self::Required),
597 other => Err(serde::de::Error::custom(format!(
598 "unknown tool_choice mode {other:?}"
599 ))),
600 },
601 Repr::Function(ToolChoiceFunctionRepr::Function {
602 function: ToolChoiceFunctionName { name },
603 }) => Ok(Self::Function { name }),
604 }
605 }
606}
607
608impl ToolChoice {
609 pub fn function(name: impl Into<String>) -> Self {
611 Self::Function { name: name.into() }
612 }
613}
614
615impl TryFrom<crate::message::ToolChoice> for ToolChoice {
616 type Error = EncodeError;
617 fn try_from(value: crate::message::ToolChoice) -> Result<Self, Self::Error> {
618 let res = match value {
619 message::ToolChoice::Specific { function_names } => {
620 let [name] = function_names.as_slice() else {
621 return Err(EncodeError::request(
622 "Provider only supports forcing exactly one specific tool".to_string(),
623 ));
624 };
625 Self::function(name)
626 }
627 message::ToolChoice::Auto => Self::Auto,
628 message::ToolChoice::None => Self::None,
629 message::ToolChoice::Required => Self::Required,
630 };
631
632 Ok(res)
633 }
634}
635
636#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
637pub struct Function {
638 pub name: String,
639 #[serde(
640 serialize_with = "json_utils::stringified_json::serialize",
641 deserialize_with = "json_utils::stringified_json::deserialize_maybe_stringified"
642 )]
643 pub arguments: serde_json::Value,
644}
645
646impl TryFrom<message::ToolResult> for Message {
647 type Error = message::MessageError;
648
649 fn try_from(value: message::ToolResult) -> Result<Self, Self::Error> {
650 let tool_call_id = value.call.wire().into_owned();
653 let parts = value
654 .content
655 .into_iter()
656 .map(|content| match content {
657 message::ToolResultContent::Text(message::Text { text, .. }) => {
658 Ok(ToolResultContent::from(text))
659 }
660 message::ToolResultContent::Json { value } => {
661 Ok(ToolResultContent::from(value.to_string()))
662 }
663 message::ToolResultContent::Image(message::Image {
665 data,
666 media_type,
667 detail,
668 ..
669 }) => {
670 let url = match data {
671 DocumentSourceKind::Url(url) => url,
672 DocumentSourceKind::Base64(data) => {
673 let media_type = media_type.ok_or_else(|| {
674 message::MessageError::ConversionError(
675 "a base64 image in a tool result needs a media type to build \
676 its data URI"
677 .into(),
678 )
679 })?;
680 format!("data:{};base64,{}", media_type.to_mime_type(), data)
681 }
682 DocumentSourceKind::Raw(_) => {
684 return Err(message::MessageError::ConversionError(
685 "raw image bytes are not supported in a tool result; encode as \
686 base64 first"
687 .into(),
688 ));
689 }
690 DocumentSourceKind::FileId(_) => {
692 return Err(message::MessageError::ConversionError(
693 "a provider-side file id is not supported in a tool result on \
694 this surface; use a URL or base64"
695 .into(),
696 ));
697 }
698 DocumentSourceKind::String(_) | DocumentSourceKind::Unknown => {
699 return Err(message::MessageError::ConversionError(
700 "this image carries no usable source; use a URL or base64".into(),
701 ));
702 }
703 };
704 Ok(ToolResultContent::Image {
705 image_url: ImageUrl { url, detail },
706 })
707 }
708 })
709 .collect::<Result<Vec<_>, _>>()?;
710
711 let content = match parts.as_slice() {
714 [ToolResultContent::Text { text }] => ToolResultContentValue::String(text.clone()),
715 _ => ToolResultContentValue::Array(parts),
716 };
717
718 Ok(Message::ToolResult {
719 tool_call_id,
720 content,
721 })
722 }
723}
724
725impl TryFrom<message::UserContent> for UserContent {
726 type Error = message::MessageError;
727
728 fn try_from(value: message::UserContent) -> Result<Self, Self::Error> {
729 match value {
730 message::UserContent::Text(message::Text { text, .. }) => Ok(UserContent::Text { text }),
731 message::UserContent::Image(message::Image {
732 data,
733 detail,
734 media_type,
735 ..
736 }) => match data {
737 DocumentSourceKind::Url(url) => Ok(UserContent::Image {
738 image_url: ImageUrl {
739 url,
740 detail: Some(detail.unwrap_or_default()),
743 },
744 }),
745 DocumentSourceKind::Base64(data) => {
746 let url = format!(
747 "data:{};base64,{}",
748 media_type.map(|i| i.to_mime_type()).ok_or(
749 message::MessageError::ConversionError(
750 "OpenAI Image URI must have media type".into()
751 )
752 )?,
753 data
754 );
755
756 let detail = Some(detail.unwrap_or_default());
757
758 Ok(UserContent::Image {
759 image_url: ImageUrl { url, detail },
760 })
761 }
762 DocumentSourceKind::Raw(_) => Err(message::MessageError::ConversionError(
763 "Raw files not supported, encode as base64 first".into(),
764 )),
765 DocumentSourceKind::FileId(_) => Err(message::MessageError::ConversionError(
766 "File IDs are not supported for images".into(),
767 )),
768 DocumentSourceKind::Unknown => Err(message::MessageError::ConversionError(
769 "Document has no body".into(),
770 )),
771 doc => Err(message::MessageError::ConversionError(format!(
772 "Unsupported document type: {doc:?}"
773 ))),
774 },
775 message::UserContent::Document(message::Document {
776 data: DocumentSourceKind::FileId(file_id),
777 ..
778 }) => Ok(UserContent::File {
779 file: FileData {
780 file_data: None,
781 file_id: Some(file_id),
782 filename: None,
783 },
784 }),
785 message::UserContent::Document(message::Document {
786 data,
787 media_type: Some(message::DocumentMediaType::PDF),
788 ..
789 }) => match data {
790 DocumentSourceKind::Base64(b64) => Ok(UserContent::File {
791 file: FileData {
792 file_data: Some(format!("data:application/pdf;base64,{b64}")),
793 file_id: None,
794 filename: Some("document.pdf".to_string()),
795 },
796 }),
797 DocumentSourceKind::Url(_) => Err(message::MessageError::ConversionError(
798 "OpenAI chat completions does not accept URL files; use the Responses API or pass base64-encoded bytes".into(),
799 )),
800 DocumentSourceKind::Raw(_) => Err(message::MessageError::ConversionError(
801 "Raw files not supported, encode as base64 first".into(),
802 )),
803 DocumentSourceKind::String(_) => Err(message::MessageError::ConversionError(
804 "PDF documents must be base64-encoded, not raw strings".into(),
805 )),
806 DocumentSourceKind::FileId(_) => Err(message::MessageError::ConversionError(
807 "File ID documents should be converted without media type constraints".into(),
808 )),
809 DocumentSourceKind::Unknown => Err(message::MessageError::ConversionError(
810 "Document has no body".into(),
811 )),
812 },
813 message::UserContent::Document(message::Document { data, .. }) => {
814 if let DocumentSourceKind::Base64(text) | DocumentSourceKind::String(text) = data {
815 Ok(UserContent::Text { text })
816 } else {
817 Err(message::MessageError::ConversionError(
818 "Documents must be base64 or a string".into(),
819 ))
820 }
821 }
822 message::UserContent::Audio(message::Audio {
823 data, media_type, ..
824 }) => match data {
825 DocumentSourceKind::Base64(data) => Ok(UserContent::Audio {
826 input_audio: InputAudio {
827 data,
828 format: media_type.unwrap_or(AudioMediaType::MP3),
829 },
830 }),
831 DocumentSourceKind::Url(_) => Err(message::MessageError::ConversionError(
832 "URLs are not supported for audio".into(),
833 )),
834 DocumentSourceKind::Raw(_) => Err(message::MessageError::ConversionError(
835 "Raw files are not supported for audio".into(),
836 )),
837 DocumentSourceKind::FileId(_) => Err(message::MessageError::ConversionError(
838 "File IDs are not supported for audio".into(),
839 )),
840 DocumentSourceKind::Unknown => Err(message::MessageError::ConversionError(
841 "Audio has no body".into(),
842 )),
843 audio => Err(message::MessageError::ConversionError(format!(
844 "Unsupported audio type: {audio:?}"
845 ))),
846 },
847 message::UserContent::ToolResult(_) => Err(message::MessageError::ConversionError(
848 "Tool result is in unsupported format".into(),
849 )),
850 message::UserContent::Video(message::Video {
851 data, media_type, ..
852 }) => {
853 let url = match data {
854 DocumentSourceKind::Url(url) => url,
855 DocumentSourceKind::Base64(data) => {
856 let mime = media_type
857 .ok_or_else(|| {
858 message::MessageError::ConversionError(
859 "Video media type required for base64 encoding".into(),
860 )
861 })?
862 .to_mime_type();
863 format!("data:{mime};base64,{data}")
864 }
865 DocumentSourceKind::Raw(_) => {
866 return Err(message::MessageError::ConversionError(
867 "Raw bytes not supported for video, encode as base64 first".into(),
868 ));
869 }
870 DocumentSourceKind::FileId(_) => {
871 return Err(message::MessageError::ConversionError(
872 "File IDs are not supported for video".into(),
873 ));
874 }
875 DocumentSourceKind::String(_) => {
876 return Err(message::MessageError::ConversionError(
877 "String source not supported for video".into(),
878 ));
879 }
880 DocumentSourceKind::Unknown => {
881 return Err(message::MessageError::ConversionError(
882 "Video has no data".into(),
883 ));
884 }
885 };
886 Ok(UserContent::Video {
887 video_url: VideoUrl { url },
888 })
889 }
890 }
891 }
892}
893
894pub fn user_content_to_messages(
897 value: impl IntoIterator<Item = message::UserContent>,
898) -> Result<Vec<Message>, message::MessageError> {
899 fn flush_user_content(messages: &mut Vec<Message>, pending: &mut Vec<UserContent>) {
900 if pending.is_empty() {
902 return;
903 }
904
905 messages.push(Message::User {
906 content: std::mem::take(pending),
907 name: None,
908 });
909 }
910
911 let mut messages = Vec::new();
912 let mut pending = Vec::new();
913
914 for content in value {
915 match content {
916 message::UserContent::ToolResult(tool_result) => {
917 flush_user_content(&mut messages, &mut pending);
918 messages.push(tool_result.try_into()?);
919 }
920 content => pending.push(content.try_into()?),
921 }
922 }
923
924 flush_user_content(&mut messages, &mut pending);
925 Ok(messages)
926}
927
928pub fn assistant_content_to_messages(
933 value: impl IntoIterator<Item = message::AssistantContent>,
934 reasoning_details: bool,
935 issuers: &[message::Issuer],
936) -> Result<Vec<Message>, message::MessageError> {
937 let mut text_content = Vec::new();
938 let mut tool_calls = Vec::new();
939 let mut reasoning_parts: Vec<String> = Vec::new();
943 let mut details: Vec<ReasoningDetails> = Vec::new();
944
945 for content in value {
946 match content {
947 message::AssistantContent::Text(text) => text_content.push(text),
948 message::AssistantContent::ToolCall(tool_call) => tool_calls.push(tool_call),
949 message::AssistantContent::Reasoning(sealed) => {
951 let Some(reasoning) = sealed.open_for(issuers) else {
952 continue;
953 };
954 if !reasoning_details || reasoning.content.is_empty() {
955 let display = reasoning.display_text();
956 if !display.is_empty() {
957 reasoning_parts.push(display);
958 }
959 continue;
960 }
961 let id = reasoning.id.clone().filter(|id| !id.is_empty());
966 let base = details.len();
969 let entries = reasoning.content.iter().enumerate().map(|(offset, part)| {
970 let id = id.clone();
971 let index = Some(base + offset);
972 match part {
973 message::ReasoningContent::Text { text, signature } => {
974 ReasoningDetails::Text {
975 id,
976 format: None,
977 index,
978 text: Some(text.clone()),
979 signature: signature.clone(),
980 }
981 }
982 message::ReasoningContent::Summary(summary) => ReasoningDetails::Summary {
983 id,
984 format: None,
985 index,
986 summary: summary.clone(),
987 },
988 message::ReasoningContent::Encrypted(data)
989 | message::ReasoningContent::Redacted { data } => {
990 ReasoningDetails::Encrypted {
991 id,
992 format: None,
993 index,
994 data: data.clone(),
995 }
996 }
997 }
998 });
999 details.extend(entries);
1000 }
1001 message::AssistantContent::Image(_) => {
1002 return Err(message::MessageError::ConversionError(
1003 "OpenAI assistant messages do not support image content in chat completions"
1004 .into(),
1005 ));
1006 }
1007 }
1008 }
1009
1010 if text_content.is_empty() && tool_calls.is_empty() && details.is_empty() {
1014 return Ok(vec![]);
1015 }
1016
1017 Ok(vec![Message::Assistant {
1018 content: text_content
1019 .into_iter()
1020 .map(|content| content.text.into())
1021 .collect::<Vec<_>>(),
1022 reasoning: if reasoning_parts.is_empty() {
1023 None
1024 } else {
1025 Some(reasoning_parts.join("\n"))
1026 },
1027 refusal: None,
1028 name: None,
1029 tool_calls: tool_calls
1030 .into_iter()
1031 .map(std::convert::Into::into)
1032 .collect::<Vec<_>>(),
1033 reasoning_details: details,
1034 }])
1035}
1036
1037impl TryFrom<message::Message> for Vec<Message> {
1038 type Error = message::MessageError;
1039
1040 fn try_from(message: message::Message) -> Result<Self, Self::Error> {
1044 match message {
1045 message::Message::System { content } => Ok(vec![Message::system(&content)]),
1046 message::Message::User { content } => user_content_to_messages(content),
1047 message::Message::Assistant { content, .. } => {
1048 assistant_content_to_messages(content, false, &[])
1049 }
1050 }
1051 }
1052}
1053
1054fn message_with_tool_ids(
1055 source: message::Message,
1056 position: usize,
1057 ids: &crate::providers::internal::wire_ids::WireIds,
1058 reasoning_details: bool,
1059 issuers: &[message::Issuer],
1060) -> Result<Vec<Message>, message::MessageError> {
1061 let content_positions: Vec<_> = match &source {
1062 message::Message::Assistant { content, .. } => content
1063 .iter()
1064 .enumerate()
1065 .filter_map(|(index, part)| {
1066 matches!(part, message::AssistantContent::ToolCall(_)).then_some(index)
1067 })
1068 .collect(),
1069 message::Message::User { content } => content
1070 .iter()
1071 .enumerate()
1072 .filter_map(|(index, part)| {
1073 matches!(part, message::UserContent::ToolResult(_)).then_some(index)
1074 })
1075 .collect(),
1076 message::Message::System { .. } => Vec::new(),
1077 };
1078 let mut converted: Vec<Message> = match source {
1079 message::Message::Assistant { content, .. } => {
1080 assistant_content_to_messages(content, reasoning_details, issuers)?
1081 }
1082 source => source.try_into()?,
1083 };
1084 let slots: Vec<&mut String> = converted
1087 .iter_mut()
1088 .flat_map(|message| match message {
1089 Message::Assistant { tool_calls, .. } => {
1090 tool_calls.iter_mut().map(|call| &mut call.id).collect()
1091 }
1092 Message::ToolResult { tool_call_id, .. } => vec![tool_call_id],
1093 _ => Vec::new(),
1094 })
1095 .collect();
1096 if slots.len() != content_positions.len() {
1097 return Err(message::MessageError::ConversionError(
1098 "tool identity mapping lost a content occurrence during OpenAI conversion".into(),
1099 ));
1100 }
1101 for (slot, content) in slots.into_iter().zip(content_positions) {
1102 *slot = ids
1103 .get(position, content)
1104 .ok_or_else(|| {
1105 message::MessageError::ConversionError(
1106 "missing planned OpenAI tool identity".into(),
1107 )
1108 })?
1109 .to_owned();
1110 }
1111 Ok(converted)
1112}
1113
1114impl From<message::ToolCall> for ToolCall {
1115 fn from(tool_call: message::ToolCall) -> Self {
1116 Self {
1117 id: tool_call.id.wire().into_owned(),
1119 r#type: ToolType::default(),
1120 function: Function {
1121 name: tool_call.function.name.into(),
1122 arguments: tool_call.function.arguments,
1123 },
1124 }
1125 }
1126}
1127
1128impl From<String> for UserContent {
1129 fn from(s: String) -> Self {
1130 UserContent::Text { text: s }
1131 }
1132}
1133
1134impl From<&str> for UserContent {
1135 fn from(s: &str) -> Self {
1136 s.to_owned().into()
1137 }
1138}
1139
1140impl FromStr for UserContent {
1141 type Err = Infallible;
1142
1143 fn from_str(s: &str) -> Result<Self, Self::Err> {
1144 Ok(s.to_owned().into())
1145 }
1146}
1147
1148impl From<String> for AssistantContent {
1149 fn from(s: String) -> Self {
1150 AssistantContent::Text { text: s }
1151 }
1152}
1153
1154impl FromStr for AssistantContent {
1155 type Err = Infallible;
1156
1157 fn from_str(s: &str) -> Result<Self, Self::Err> {
1158 Ok(s.to_owned().into())
1159 }
1160}
1161impl From<String> for SystemContent {
1162 fn from(s: String) -> Self {
1163 SystemContent {
1164 r#type: SystemContentType::default(),
1165 text: s,
1166 }
1167 }
1168}
1169
1170impl FromStr for SystemContent {
1171 type Err = Infallible;
1172
1173 fn from_str(s: &str) -> Result<Self, Self::Err> {
1174 Ok(s.to_owned().into())
1175 }
1176}
1177
1178#[derive(Clone, Debug, Deserialize, Serialize)]
1179pub struct CompletionResponse {
1180 pub id: String,
1181 #[serde(default, deserialize_with = "json_utils::null_or_default")]
1185 pub object: String,
1186 #[serde(default, deserialize_with = "json_utils::null_or_default")]
1187 pub created: u64,
1188 pub model: String,
1189 pub system_fingerprint: Option<String>,
1190 #[serde(default, skip_serializing_if = "Option::is_none")]
1192 pub service_tier: Option<String>,
1193 #[serde(
1194 deserialize_with = "crate::providers::internal::openai_chat_completions_compatible::deserialize_choices_dropping_incomplete_tool_calls"
1195 )]
1196 pub choices: Vec<Choice>,
1197 pub usage: Option<Usage>,
1198}
1199
1200pub(crate) fn assistant_refusal_fallback<'a>(
1203 content: &[AssistantContent],
1204 refusal: Option<&'a str>,
1205) -> Option<&'a str> {
1206 let has_text = content.iter().any(|part| {
1207 !match part {
1208 AssistantContent::Text { text } => text,
1209 AssistantContent::Refusal { refusal } => refusal,
1210 }
1211 .is_empty()
1212 });
1213
1214 refusal.filter(|refusal| !has_text && !refusal.is_empty())
1215}
1216
1217#[cfg(test)]
1225pub(crate) fn assistant_message_text_response(message: &Message) -> Option<String> {
1226 let Message::Assistant {
1227 content, refusal, ..
1228 } = message
1229 else {
1230 return None;
1231 };
1232
1233 let mut segments = content
1234 .iter()
1235 .filter_map(|content| match content {
1236 AssistantContent::Text { text, .. } => (!text.is_empty()).then(|| text.clone()),
1237 AssistantContent::Refusal { refusal } => (!refusal.is_empty()).then(|| refusal.clone()),
1238 })
1239 .collect::<Vec<_>>();
1240
1241 if let Some(refusal) = assistant_refusal_fallback(content, refusal.as_deref()) {
1242 segments.push(refusal.to_owned());
1243 }
1244
1245 if segments.is_empty() {
1246 None
1247 } else {
1248 Some(segments.join("\n"))
1249 }
1250}
1251
1252#[derive(Clone, Debug, Serialize, Deserialize)]
1253pub struct Choice {
1254 #[serde(default, deserialize_with = "json_utils::null_or_default")]
1258 pub index: usize,
1259 pub message: Message,
1260 pub logprobs: Option<serde_json::Value>,
1261 #[serde(default, deserialize_with = "json_utils::null_or_default")]
1262 pub finish_reason: String,
1263}
1264
1265#[derive(Clone, Copy, Debug, Deserialize, Serialize, Default)]
1266pub struct PromptTokensDetails {
1267 #[serde(default)]
1269 pub cached_tokens: usize,
1270 #[serde(
1274 default,
1275 deserialize_with = "json_utils::null_or_default",
1276 skip_serializing_if = "is_zero"
1277 )]
1278 pub audio_tokens: usize,
1279 #[serde(default, skip_serializing_if = "Option::is_none")]
1281 pub cache_write_tokens: Option<usize>,
1282}
1283
1284fn is_zero(value: &usize) -> bool {
1286 *value == 0
1287}
1288
1289#[derive(Clone, Copy, Debug, Deserialize, Serialize, Default)]
1290pub struct CompletionTokensDetails {
1291 #[serde(default)]
1293 pub reasoning_tokens: usize,
1294}
1295
1296#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
1297pub struct Usage {
1298 pub prompt_tokens: usize,
1299 #[serde(default, skip_serializing_if = "Option::is_none")]
1300 pub completion_tokens: Option<usize>,
1301 pub total_tokens: usize,
1302 #[serde(skip_serializing_if = "Option::is_none")]
1306 pub prompt_tokens_details: Option<PromptTokensDetails>,
1307 #[serde(default, skip_serializing_if = "Option::is_none")]
1308 pub completion_tokens_details: Option<CompletionTokensDetails>,
1309 #[serde(default, skip_serializing_if = "Option::is_none")]
1312 pub num_cached_tokens: Option<u64>,
1313 #[serde(default, skip_serializing_if = "Option::is_none")]
1314 pub queue_time: Option<f64>,
1315 #[serde(default, skip_serializing_if = "Option::is_none")]
1316 pub prompt_time: Option<f64>,
1317 #[serde(default, skip_serializing_if = "Option::is_none")]
1318 pub completion_time: Option<f64>,
1319 #[serde(default, skip_serializing_if = "Option::is_none")]
1320 pub total_time: Option<f64>,
1321}
1322
1323impl Usage {
1324 pub fn new() -> Self {
1325 Self {
1326 prompt_tokens: 0,
1327 completion_tokens: None,
1328 total_tokens: 0,
1329 prompt_tokens_details: None,
1330 completion_tokens_details: None,
1331 num_cached_tokens: None,
1332 queue_time: None,
1333 prompt_time: None,
1334 completion_time: None,
1335 total_time: None,
1336 }
1337 }
1338}
1339
1340impl Default for Usage {
1341 fn default() -> Self {
1342 Self::new()
1343 }
1344}
1345
1346impl fmt::Display for Usage {
1347 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1348 let Usage {
1349 prompt_tokens,
1350 total_tokens,
1351 ..
1352 } = self;
1353 write!(
1354 f,
1355 "Prompt tokens: {prompt_tokens} Total tokens: {total_tokens}"
1356 )
1357 }
1358}
1359
1360impl From<&Usage> for crate::completion::Usage {
1361 fn from(value: &Usage) -> crate::completion::Usage {
1362 value.to_normalized()
1363 }
1364}
1365
1366impl From<Usage> for crate::completion::Usage {
1367 fn from(value: Usage) -> crate::completion::Usage {
1368 value.to_normalized()
1369 }
1370}
1371
1372impl Usage {
1373 fn input_tokens(&self) -> usize {
1376 let audio = self
1377 .prompt_tokens_details
1378 .map_or(0, |details| details.audio_tokens);
1379 let beside = self.prompt_tokens.saturating_add(audio);
1380 let accounted = beside.saturating_add(self.completion_tokens.unwrap_or(0));
1381 if audio != 0 && accounted == self.total_tokens {
1382 beside
1383 } else {
1384 self.prompt_tokens
1385 }
1386 }
1387
1388 pub fn to_normalized(&self) -> crate::completion::Usage {
1391 let input_tokens = self.input_tokens();
1392 let details = self.prompt_tokens_details.as_ref();
1393 crate::completion::Usage {
1394 input_tokens: Some(input_tokens as u64),
1395 output_tokens: Some(
1398 self.completion_tokens
1399 .unwrap_or_else(|| self.total_tokens.saturating_sub(input_tokens))
1400 as u64,
1401 ),
1402 total_tokens: Some(self.total_tokens as u64),
1403 cached_input_tokens: details
1404 .map(|d| d.cached_tokens as u64)
1405 .or(self.num_cached_tokens),
1406 cache_creation_input_tokens: details
1407 .and_then(|d| d.cache_write_tokens)
1408 .map(|tokens| tokens as u64),
1409 reasoning_tokens: self
1410 .completion_tokens_details
1411 .as_ref()
1412 .map(|d| d.reasoning_tokens as u64),
1413 ..Default::default()
1414 }
1415 }
1416}
1417
1418pub(crate) fn is_openai_reasoning_model(model: &str) -> bool {
1421 fn is_numbered_gpt_family(model: &str, lowest: u32) -> bool {
1424 model
1425 .strip_prefix("gpt-")
1426 .and_then(|rest| rest.split(['.', '-']).next())
1427 .filter(|major| major.len() == 1)
1428 .and_then(|major| major.parse::<u32>().ok())
1429 .is_some_and(|major| major >= lowest)
1430 }
1431
1432 fn is_o_series(model: &str) -> bool {
1434 let mut chars = model.chars();
1435 chars.next() == Some('o')
1436 && chars.next().is_some_and(|digit| digit.is_ascii_digit())
1437 && chars
1438 .next()
1439 .is_none_or(|next| next == '-' || next.is_ascii_digit())
1440 }
1441
1442 is_numbered_gpt_family(model, 5) || is_o_series(model)
1443}
1444
1445pub(crate) fn request_body(
1452 request: &CompletionRequest,
1453 modern_output_cap: bool,
1454) -> Result<serde_json::Value, EncodeError> {
1455 let mut body = serde_json::to_value(request)?;
1456
1457 if modern_output_cap
1458 && let Some(object) = body.as_object_mut()
1459 && let Some(max_tokens) = object.remove("max_tokens")
1460 {
1461 object.entry("max_completion_tokens").or_insert(max_tokens);
1463 }
1464
1465 Ok(body)
1466}
1467
1468#[derive(Debug, Serialize, Deserialize, Clone)]
1469pub struct CompletionRequest {
1470 pub model: String,
1471 pub messages: Vec<Message>,
1472 #[serde(skip_serializing_if = "Vec::is_empty")]
1473 pub tools: Vec<ToolDefinition>,
1474 #[serde(skip_serializing_if = "Option::is_none")]
1475 pub tool_choice: Option<ToolChoice>,
1476 #[serde(skip_serializing_if = "Option::is_none")]
1477 pub temperature: Option<f64>,
1478 #[serde(skip_serializing_if = "Option::is_none")]
1479 pub max_tokens: Option<u64>,
1480 #[serde(flatten)]
1481 pub additional_params: Option<serde_json::Value>,
1482}
1483
1484pub(crate) fn flatten_text_content_parts(
1490 content: &mut serde_json::Value,
1491 separator: &str,
1492 only_if_all_text: bool,
1493) {
1494 fn part_text(part: &serde_json::Value) -> Option<&str> {
1498 part.get("text")
1499 .and_then(serde_json::Value::as_str)
1500 .or_else(|| part.get("refusal").and_then(serde_json::Value::as_str))
1501 }
1502
1503 let Some(parts) = content.as_array() else {
1504 return;
1505 };
1506 if only_if_all_text && !parts.iter().all(|part| part_text(part).is_some()) {
1507 return;
1508 }
1509 let mut flattened = String::new();
1510 for text in parts.iter().filter_map(part_text) {
1511 if !flattened.is_empty() {
1512 flattened.push_str(separator);
1513 }
1514 flattened.push_str(text);
1515 }
1516 *content = serde_json::Value::String(flattened);
1517}
1518
1519pub(crate) fn joined_text_parts(parts: &[serde_json::Value]) -> String {
1521 parts
1522 .iter()
1523 .filter_map(|part| {
1524 (part.get("type").and_then(serde_json::Value::as_str) == Some("text"))
1525 .then(|| part.get("text").and_then(serde_json::Value::as_str))
1526 .flatten()
1527 })
1528 .collect::<Vec<_>>()
1529 .join("")
1530}
1531
1532pub(crate) fn sanitize_plain_text_history(
1537 messages: &mut Vec<serde_json::Value>,
1538 flatten: Option<(&str, bool)>,
1539 strip_names: bool,
1540 merge_same_role: bool,
1541) {
1542 messages
1543 .retain(|message| message.get("role").and_then(serde_json::Value::as_str) != Some("tool"));
1544
1545 for message in messages.iter_mut() {
1546 let Some(object) = message.as_object_mut() else {
1547 continue;
1548 };
1549 if object.get("role").and_then(serde_json::Value::as_str) == Some("assistant") {
1550 object.remove("tool_calls");
1551 object.remove("reasoning_content");
1552 }
1553 if strip_names {
1554 object.remove("name");
1555 }
1556 if let Some((separator, only_if_all_text)) = flatten
1557 && let Some(content) = object.get_mut("content")
1558 {
1559 flatten_text_content_parts(content, separator, only_if_all_text);
1560 }
1561 }
1562
1563 messages.retain(|message| {
1564 if message.get("role").and_then(serde_json::Value::as_str) != Some("assistant") {
1565 return true;
1566 }
1567 match message.get("content") {
1568 Some(serde_json::Value::String(text)) => !text.is_empty(),
1569 Some(serde_json::Value::Null) | None => false,
1570 Some(_) => true,
1571 }
1572 });
1573
1574 if !merge_same_role {
1575 return;
1576 }
1577
1578 let mut merged: Vec<serde_json::Value> = Vec::with_capacity(messages.len());
1579 for message in std::mem::take(messages) {
1580 let merged_text = if let Some(role) = message
1581 .get("role")
1582 .and_then(serde_json::Value::as_str)
1583 .filter(|role| matches!(*role, "assistant" | "user"))
1584 && let Some(previous) = merged.last()
1585 && previous.get("role").and_then(serde_json::Value::as_str) == Some(role)
1586 && let Some(previous_text) = previous.get("content").and_then(serde_json::Value::as_str)
1587 && let Some(text) = message.get("content").and_then(serde_json::Value::as_str)
1588 {
1589 Some(format!("{previous_text}\n{text}"))
1590 } else {
1591 None
1592 };
1593
1594 if let Some(text) = merged_text
1595 && let Some(previous) = merged.last_mut().and_then(serde_json::Value::as_object_mut)
1596 {
1597 previous.insert("content".to_string(), serde_json::Value::String(text));
1598 continue;
1599 }
1600 merged.push(message);
1601 }
1602 *messages = merged;
1603}
1604
1605pub struct OpenAIRequestParams {
1606 pub model: String,
1607 pub request: CoreCompletionRequest,
1608 pub strict_tools: bool,
1609 pub tool_result_array_content: bool,
1610 pub supports_image_tool_results: bool,
1614 pub supports_response_format: bool,
1617 pub response_format_with_tools: bool,
1621 pub supports_tools: bool,
1624 pub reasoning_details: bool,
1633 pub issuers: Vec<message::Issuer>,
1636}
1637
1638impl TryFrom<OpenAIRequestParams> for CompletionRequest {
1639 type Error = EncodeError;
1640
1641 fn try_from(params: OpenAIRequestParams) -> Result<Self, Self::Error> {
1642 let OpenAIRequestParams {
1643 model,
1644 request: req,
1645 strict_tools,
1646 tool_result_array_content,
1647 supports_image_tool_results,
1648 supports_response_format,
1649 response_format_with_tools,
1650 supports_tools,
1651 reasoning_details,
1652 issuers,
1653 } = params;
1654 let chat_history = req.chat_history_with_documents();
1655
1656 let CoreCompletionRequest {
1657 model: request_model,
1658 chat_history: _,
1659 tools,
1660 temperature,
1661 max_tokens,
1662 additional_params,
1663 tool_choice,
1664 output_schema,
1665 ..
1666 } = req;
1667
1668 let partial_history = chat_history;
1669
1670 let tool_ids = crate::providers::internal::wire_ids::WireIds::new(&partial_history);
1671
1672 let mut full_history: Vec<Message> = Vec::new();
1673 full_history.extend(
1674 partial_history
1675 .into_iter()
1676 .enumerate()
1677 .map(|(position, message)| {
1678 message_with_tool_ids(message, position, &tool_ids, reasoning_details, &issuers)
1679 })
1680 .collect::<Result<Vec<Vec<Message>>, _>>()?
1681 .into_iter()
1682 .flatten(),
1683 );
1684
1685 if full_history.is_empty() {
1686 return Err(EncodeError::request(std::io::Error::new(
1687 std::io::ErrorKind::InvalidInput,
1688 "OpenAI Chat Completions request has no provider-compatible messages after conversion",
1689 )));
1690 }
1691
1692 for msg in &mut full_history {
1694 if let Message::ToolResult { content, .. } = msg {
1695 if content.has_image() {
1696 if !supports_image_tool_results {
1697 return Err(EncodeError::request(concat!(
1699 "this provider does not accept an image in a tool result. ",
1700 "Official OpenAI refuses it on Chat Completions (and the GPT-5 ",
1701 "family accepts the request while ignoring the image); use the ",
1702 "Responses API, which carries images in `function_call_output`, ",
1703 "or a server that sets `SUPPORTS_IMAGE_TOOL_RESULTS` ",
1704 "(llama.cpp does)",
1705 )));
1706 }
1707 *content = content.to_array();
1710 continue;
1711 }
1712
1713 let normalized = if tool_result_array_content {
1714 content.to_array()
1715 } else {
1716 ToolResultContentValue::String(content.as_text())
1717 };
1718
1719 *content = normalized;
1720 }
1721 }
1722
1723 let history_has_tool_result = history_contains_tool_result(&full_history);
1724
1725 let (mut tools, tool_choice) = if supports_tools {
1726 let tool_choice = tool_choice.map(ToolChoice::try_from).transpose()?;
1727 let tools: Vec<ToolDefinition> = tools
1728 .into_iter()
1729 .map(|tool| {
1730 let def = ToolDefinition::from(tool);
1731 if strict_tools { def.with_strict() } else { def }
1732 })
1733 .collect();
1734 (tools, tool_choice)
1735 } else {
1736 if !tools.is_empty() {
1737 tracing::warn!("Tool use is not supported by this provider; tools will be ignored");
1738 }
1739 if tool_choice.is_some() {
1740 tracing::warn!("Tool choice is not supported by this provider and will be ignored");
1741 }
1742 (Vec::new(), None)
1743 };
1744
1745 let mut additional_params = additional_params;
1748 if supports_tools
1749 && let Some(map) = additional_params
1750 .as_mut()
1751 .and_then(serde_json::Value::as_object_mut)
1752 && let Some(raw_tools) = map.remove("tools")
1753 {
1754 let raw_tools =
1755 serde_json::from_value::<Vec<serde_json::Value>>(raw_tools).map_err(|err| {
1756 EncodeError::request(format!(
1757 "Invalid OpenAI Chat Completions `additional_params.tools` payload: {err}"
1758 ))
1759 })?;
1760 let mut remaining = Vec::new();
1761 for raw_tool in raw_tools {
1762 let is_function_tool =
1763 raw_tool.get("type").and_then(serde_json::Value::as_str) == Some("function");
1764 if is_function_tool {
1765 let tool =
1766 serde_json::from_value::<ToolDefinition>(raw_tool).map_err(|err| {
1767 EncodeError::request(format!(
1768 "Invalid function tool in OpenAI Chat Completions \
1769 `additional_params.tools`: {err}"
1770 ))
1771 })?;
1772 tools.push(tool);
1773 } else {
1774 remaining.push(raw_tool);
1775 }
1776 }
1777 if !remaining.is_empty() {
1778 map.insert("tools".to_string(), serde_json::Value::Array(remaining));
1779 }
1780 }
1781
1782 if output_schema.is_some() && !supports_response_format {
1783 tracing::warn!(
1784 "Structured outputs are not supported by this provider; ignoring output_schema"
1785 );
1786 }
1787
1788 let should_apply_response_format = output_schema.is_some()
1790 && supports_response_format
1791 && (response_format_with_tools || tools.is_empty() || history_has_tool_result);
1792
1793 let additional_params = if let Some(schema) = output_schema
1794 && should_apply_response_format
1795 {
1796 let (name, schema_value) = super::structured_output_schema(schema);
1797 let response_format = serde_json::json!({
1798 "response_format": {
1799 "type": "json_schema",
1800 "json_schema": {
1801 "name": name,
1802 "strict": true,
1803 "schema": schema_value
1804 }
1805 }
1806 });
1807 Some(match additional_params {
1808 Some(existing) => json_utils::merge(existing, response_format),
1809 None => response_format,
1810 })
1811 } else {
1812 additional_params
1813 };
1814
1815 let tool_choice = tool_choice.filter(|_| !tools.is_empty());
1817 let res = Self {
1818 model: request_model.unwrap_or(model),
1819 messages: full_history,
1820 tools,
1821 tool_choice,
1822 temperature,
1823 max_tokens,
1824 additional_params,
1825 };
1826
1827 Ok(res)
1828 }
1829}
1830
1831fn serialize_assistant_content_vec<S>(
1832 value: &[AssistantContent],
1833 serializer: S,
1834) -> Result<S::Ok, S::Error>
1835where
1836 S: Serializer,
1837{
1838 if value.is_empty() {
1839 serializer.serialize_str("")
1840 } else {
1841 value.serialize(serializer)
1842 }
1843}
1844
1845#[cfg(test)]
1846mod tests;
1847
1848#[cfg(test)]
1849mod image_tool_result_gate_tests;