use super::completion::ToolChoice;
use super::{Client, responses_api::streaming::StreamingCompletionResponse};
use super::{InputAudio, SystemContent};
use crate::completion::CompletionError;
use crate::http_client;
use crate::http_client::HttpClientExt;
use crate::json_utils;
use crate::message::{
AudioMediaType, Document, DocumentMediaType, DocumentSourceKind, ImageDetail, MessageError,
MimeType, Text,
};
use crate::one_or_many::string_or_one_or_many;
use crate::wasm_compat::{WasmCompatSend, WasmCompatSync};
use crate::{OneOrMany, completion, message};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use tracing::{Instrument, Level, enabled, info_span};
use std::convert::Infallible;
use std::ops::Add;
use std::str::FromStr;
pub mod streaming;
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct CompletionRequest {
pub input: OneOrMany<InputItem>,
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_output_tokens: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
tool_choice: Option<ToolChoice>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tools: Vec<ResponsesToolDefinition>,
#[serde(flatten)]
pub additional_parameters: AdditionalParameters,
}
impl CompletionRequest {
pub fn with_structured_outputs<S>(mut self, schema_name: S, schema: serde_json::Value) -> Self
where
S: Into<String>,
{
self.additional_parameters.text = Some(TextConfig::structured_output(schema_name, schema));
self
}
pub fn with_reasoning(mut self, reasoning: Reasoning) -> Self {
self.additional_parameters.reasoning = Some(reasoning);
self
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct InputItem {
#[serde(skip_serializing_if = "Option::is_none")]
role: Option<Role>,
#[serde(flatten)]
input: InputContent,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(rename_all = "lowercase")]
pub enum Role {
User,
Assistant,
System,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum InputContent {
Message(Message),
Reasoning(OpenAIReasoning),
FunctionCall(OutputFunctionCall),
FunctionCallOutput(ToolResult),
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub struct OpenAIReasoning {
id: String,
pub summary: Vec<ReasoningSummary>,
pub encrypted_content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<ToolStatus>,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ReasoningSummary {
SummaryText { text: String },
}
impl ReasoningSummary {
fn new(input: &str) -> Self {
Self::SummaryText {
text: input.to_string(),
}
}
pub fn text(&self) -> String {
let ReasoningSummary::SummaryText { text } = self;
text.clone()
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ToolResult {
call_id: String,
output: String,
status: ToolStatus,
}
impl From<Message> for InputItem {
fn from(value: Message) -> Self {
match value {
Message::User { .. } => Self {
role: Some(Role::User),
input: InputContent::Message(value),
},
Message::Assistant { ref content, .. } => {
let role = if content
.clone()
.iter()
.any(|x| matches!(x, AssistantContentType::Reasoning(_)))
{
None
} else {
Some(Role::Assistant)
};
Self {
role,
input: InputContent::Message(value),
}
}
Message::System { .. } => Self {
role: Some(Role::System),
input: InputContent::Message(value),
},
Message::ToolResult {
tool_call_id,
output,
} => Self {
role: None,
input: InputContent::FunctionCallOutput(ToolResult {
call_id: tool_call_id,
output,
status: ToolStatus::Completed,
}),
},
}
}
}
impl TryFrom<crate::completion::Message> for Vec<InputItem> {
type Error = CompletionError;
fn try_from(value: crate::completion::Message) -> Result<Self, Self::Error> {
match value {
crate::completion::Message::User { content } => {
let mut items = Vec::new();
for user_content in content {
match user_content {
crate::message::UserContent::Text(Text { text }) => {
items.push(InputItem {
role: Some(Role::User),
input: InputContent::Message(Message::User {
content: OneOrMany::one(UserContent::InputText { text }),
name: None,
}),
});
}
crate::message::UserContent::ToolResult(
crate::completion::message::ToolResult {
call_id,
content: tool_content,
..
},
) => {
for tool_result_content in tool_content {
let crate::completion::message::ToolResultContent::Text(Text {
text,
}) = tool_result_content
else {
return Err(CompletionError::ProviderError(
"This thing only supports text!".to_string(),
));
};
items.push(InputItem {
role: None,
input: InputContent::FunctionCallOutput(ToolResult {
call_id: call_id
.clone()
.expect("The call ID of this tool should exist!"),
output: text,
status: ToolStatus::Completed,
}),
});
}
}
crate::message::UserContent::Document(Document {
data,
media_type: Some(DocumentMediaType::PDF),
..
}) => {
let (file_data, file_url) = match data {
DocumentSourceKind::Base64(data) => {
(Some(format!("data:application/pdf;base64,{data}")), None)
}
DocumentSourceKind::Url(url) => (None, Some(url)),
DocumentSourceKind::Raw(_) => {
return Err(CompletionError::RequestError(
"Raw file data not supported, encode as base64 first"
.into(),
));
}
doc => {
return Err(CompletionError::RequestError(
format!("Unsupported document type: {doc}").into(),
));
}
};
items.push(InputItem {
role: Some(Role::User),
input: InputContent::Message(Message::User {
content: OneOrMany::one(UserContent::InputFile {
file_data,
file_url,
filename: Some("document.pdf".to_string()),
}),
name: None,
}),
})
}
crate::message::UserContent::Document(Document {
data: DocumentSourceKind::Base64(text),
..
}) => items.push(InputItem {
role: Some(Role::User),
input: InputContent::Message(Message::User {
content: OneOrMany::one(UserContent::InputText { text }),
name: None,
}),
}),
crate::message::UserContent::Document(Document {
data: DocumentSourceKind::String(text),
..
}) => items.push(InputItem {
role: Some(Role::User),
input: InputContent::Message(Message::User {
content: OneOrMany::one(UserContent::InputText { text }),
name: None,
}),
}),
crate::message::UserContent::Image(crate::message::Image {
data,
media_type,
detail,
..
}) => {
let url = match data {
DocumentSourceKind::Base64(data) => {
let media_type = if let Some(media_type) = media_type {
media_type.to_mime_type().to_string()
} else {
String::new()
};
format!("data:{media_type};base64,{data}")
}
DocumentSourceKind::Url(url) => url,
DocumentSourceKind::Raw(_) => {
return Err(CompletionError::RequestError(
"Raw file data not supported, encode as base64 first"
.into(),
));
}
doc => {
return Err(CompletionError::RequestError(
format!("Unsupported document type: {doc}").into(),
));
}
};
items.push(InputItem {
role: Some(Role::User),
input: InputContent::Message(Message::User {
content: OneOrMany::one(UserContent::InputImage {
image_url: url,
detail: detail.unwrap_or_default(),
}),
name: None,
}),
});
}
message => {
return Err(CompletionError::ProviderError(format!(
"Unsupported message: {message:?}"
)));
}
}
}
Ok(items)
}
crate::completion::Message::Assistant { id, content } => {
let mut items = Vec::new();
for assistant_content in content {
match assistant_content {
crate::message::AssistantContent::Text(Text { text }) => {
let id = id.as_ref().unwrap_or(&String::default()).clone();
items.push(InputItem {
role: Some(Role::Assistant),
input: InputContent::Message(Message::Assistant {
content: OneOrMany::one(AssistantContentType::Text(
AssistantContent::OutputText(Text { text }),
)),
id,
name: None,
status: ToolStatus::Completed,
}),
});
}
crate::message::AssistantContent::ToolCall(crate::message::ToolCall {
id: tool_id,
call_id,
function,
..
}) => {
items.push(InputItem {
role: None,
input: InputContent::FunctionCall(OutputFunctionCall {
arguments: function.arguments,
call_id: call_id.expect("The tool call ID should exist!"),
id: tool_id,
name: function.name,
status: ToolStatus::Completed,
}),
});
}
crate::message::AssistantContent::Reasoning(
crate::message::Reasoning { id, reasoning, .. },
) => {
items.push(InputItem {
role: None,
input: InputContent::Reasoning(OpenAIReasoning {
id: id
.expect("An OpenAI-generated ID is required when using OpenAI reasoning items"),
summary: reasoning.into_iter().map(|x| ReasoningSummary::new(&x)).collect(),
encrypted_content: None,
status: None,
}),
});
}
crate::message::AssistantContent::Image(_) => {
return Err(CompletionError::ProviderError(
"Assistant image content is not supported in OpenAI Responses API"
.to_string(),
));
}
}
}
Ok(items)
}
}
}
}
impl From<OneOrMany<String>> for Vec<ReasoningSummary> {
fn from(value: OneOrMany<String>) -> Self {
value.iter().map(|x| ReasoningSummary::new(x)).collect()
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ResponsesToolDefinition {
pub name: String,
pub parameters: serde_json::Value,
pub strict: bool,
#[serde(rename = "type")]
pub kind: String,
pub description: String,
}
impl From<completion::ToolDefinition> for ResponsesToolDefinition {
fn from(value: completion::ToolDefinition) -> Self {
let completion::ToolDefinition {
name,
mut parameters,
description,
} = value;
super::sanitize_schema(&mut parameters);
Self {
name,
parameters,
description,
kind: "function".to_string(),
strict: true,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ResponsesUsage {
pub input_tokens: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_tokens_details: Option<InputTokensDetails>,
pub output_tokens: u64,
pub output_tokens_details: OutputTokensDetails,
pub total_tokens: u64,
}
impl ResponsesUsage {
pub(crate) fn new() -> Self {
Self {
input_tokens: 0,
input_tokens_details: Some(InputTokensDetails::new()),
output_tokens: 0,
output_tokens_details: OutputTokensDetails::new(),
total_tokens: 0,
}
}
}
impl Add for ResponsesUsage {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
let input_tokens = self.input_tokens + rhs.input_tokens;
let input_tokens_details = self.input_tokens_details.map(|lhs| {
if let Some(tokens) = rhs.input_tokens_details {
lhs + tokens
} else {
lhs
}
});
let output_tokens = self.output_tokens + rhs.output_tokens;
let output_tokens_details = self.output_tokens_details + rhs.output_tokens_details;
let total_tokens = self.total_tokens + rhs.total_tokens;
Self {
input_tokens,
input_tokens_details,
output_tokens,
output_tokens_details,
total_tokens,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct InputTokensDetails {
pub cached_tokens: u64,
}
impl InputTokensDetails {
pub(crate) fn new() -> Self {
Self { cached_tokens: 0 }
}
}
impl Add for InputTokensDetails {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self {
cached_tokens: self.cached_tokens + rhs.cached_tokens,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OutputTokensDetails {
pub reasoning_tokens: u64,
}
impl OutputTokensDetails {
pub(crate) fn new() -> Self {
Self {
reasoning_tokens: 0,
}
}
}
impl Add for OutputTokensDetails {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self {
reasoning_tokens: self.reasoning_tokens + rhs.reasoning_tokens,
}
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct IncompleteDetailsReason {
pub reason: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ResponseError {
pub code: String,
pub message: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ResponseObject {
Response,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ResponseStatus {
InProgress,
Completed,
Failed,
Cancelled,
Queued,
Incomplete,
}
impl TryFrom<(String, crate::completion::CompletionRequest)> for CompletionRequest {
type Error = CompletionError;
fn try_from(
(model, req): (String, crate::completion::CompletionRequest),
) -> Result<Self, Self::Error> {
let input = {
let mut partial_history = vec![];
if let Some(docs) = req.normalized_documents() {
partial_history.push(docs);
}
partial_history.extend(req.chat_history);
let mut full_history: Vec<InputItem> = Vec::new();
full_history.extend(
partial_history
.into_iter()
.map(|x| <Vec<InputItem>>::try_from(x).unwrap())
.collect::<Vec<Vec<InputItem>>>()
.into_iter()
.flatten()
.collect::<Vec<InputItem>>(),
);
full_history
};
let input = OneOrMany::many(input)
.expect("This should never panic - if it does, please file a bug report");
let stream = req
.additional_params
.clone()
.unwrap_or(Value::Null)
.as_bool();
let additional_parameters = if let Some(map) = req.additional_params {
serde_json::from_value::<AdditionalParameters>(map).expect("Converting additional parameters to AdditionalParameters should never fail as every field is an Option")
} else {
AdditionalParameters::default()
};
let tool_choice = req.tool_choice.map(ToolChoice::try_from).transpose()?;
Ok(Self {
input,
model,
instructions: req.preamble,
max_output_tokens: req.max_tokens,
stream,
tool_choice,
tools: req
.tools
.into_iter()
.map(ResponsesToolDefinition::from)
.collect(),
temperature: req.temperature,
additional_parameters,
})
}
}
#[derive(Clone)]
pub struct ResponsesCompletionModel<T = reqwest::Client> {
pub(crate) client: Client<T>,
pub model: String,
}
impl<T> ResponsesCompletionModel<T>
where
T: HttpClientExt + Clone + Default + std::fmt::Debug + 'static,
{
pub fn new(client: Client<T>, model: impl Into<String>) -> Self {
Self {
client,
model: model.into(),
}
}
pub fn with_model(client: Client<T>, model: &str) -> Self {
Self {
client,
model: model.to_string(),
}
}
pub fn completions_api(self) -> crate::providers::openai::completion::CompletionModel<T> {
super::completion::CompletionModel::with_model(self.client.completions_api(), &self.model)
}
pub(crate) fn create_completion_request(
&self,
completion_request: crate::completion::CompletionRequest,
) -> Result<CompletionRequest, CompletionError> {
let req = CompletionRequest::try_from((self.model.clone(), completion_request))?;
Ok(req)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CompletionResponse {
pub id: String,
pub object: ResponseObject,
pub created_at: u64,
pub status: ResponseStatus,
pub error: Option<ResponseError>,
pub incomplete_details: Option<IncompleteDetailsReason>,
pub instructions: Option<String>,
pub max_output_tokens: Option<u64>,
pub model: String,
pub usage: Option<ResponsesUsage>,
pub output: Vec<Output>,
#[serde(default)]
pub tools: Vec<ResponsesToolDefinition>,
#[serde(flatten)]
pub additional_parameters: AdditionalParameters,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default)]
pub struct AdditionalParameters {
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<TextConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include: Option<Vec<Include>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub truncation: Option<TruncationStrategy>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
#[serde(skip_serializing_if = "Map::is_empty", default)]
pub metadata: serde_json::Map<String, serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parallel_tool_calls: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub previous_response_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<Reasoning>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<OpenAIServiceTier>,
#[serde(skip_serializing_if = "Option::is_none")]
pub store: Option<bool>,
}
impl AdditionalParameters {
pub fn to_json(self) -> serde_json::Value {
serde_json::to_value(self).expect("this should never fail since a struct that impls Deserialize will always be valid JSON")
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TruncationStrategy {
Auto,
#[default]
Disabled,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TextConfig {
pub format: TextFormat,
}
impl TextConfig {
pub(crate) fn structured_output<S>(name: S, schema: serde_json::Value) -> Self
where
S: Into<String>,
{
Self {
format: TextFormat::JsonSchema(StructuredOutputsInput {
name: name.into(),
schema,
strict: true,
}),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum TextFormat {
JsonSchema(StructuredOutputsInput),
#[default]
Text,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct StructuredOutputsInput {
pub name: String,
pub schema: serde_json::Value,
pub strict: bool,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Reasoning {
pub effort: Option<ReasoningEffort>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<ReasoningSummaryLevel>,
}
impl Reasoning {
pub fn new() -> Self {
Self {
effort: None,
summary: None,
}
}
pub fn with_effort(mut self, reasoning_effort: ReasoningEffort) -> Self {
self.effort = Some(reasoning_effort);
self
}
pub fn with_summary_level(mut self, reasoning_summary_level: ReasoningSummaryLevel) -> Self {
self.summary = Some(reasoning_summary_level);
self
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OpenAIServiceTier {
#[default]
Auto,
Default,
Flex,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReasoningEffort {
None,
Minimal,
Low,
#[default]
Medium,
High,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReasoningSummaryLevel {
#[default]
Auto,
Concise,
Detailed,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum Include {
#[serde(rename = "file_search_call.results")]
FileSearchCallResults,
#[serde(rename = "message.input_image.image_url")]
MessageInputImageImageUrl,
#[serde(rename = "computer_call.output.image_url")]
ComputerCallOutputOutputImageUrl,
#[serde(rename = "reasoning.encrypted_content")]
ReasoningEncryptedContent,
#[serde(rename = "code_interpreter_call.outputs")]
CodeInterpreterCallOutputs,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum Output {
Message(OutputMessage),
#[serde(alias = "function_call")]
FunctionCall(OutputFunctionCall),
Reasoning {
id: String,
summary: Vec<ReasoningSummary>,
},
}
impl From<Output> for Vec<completion::AssistantContent> {
fn from(value: Output) -> Self {
let res: Vec<completion::AssistantContent> = match value {
Output::Message(OutputMessage { content, .. }) => content
.into_iter()
.map(completion::AssistantContent::from)
.collect(),
Output::FunctionCall(OutputFunctionCall {
id,
arguments,
call_id,
name,
..
}) => vec![completion::AssistantContent::tool_call_with_call_id(
id, call_id, name, arguments,
)],
Output::Reasoning { id, summary } => {
let summary: Vec<String> = summary.into_iter().map(|x| x.text()).collect();
vec![completion::AssistantContent::Reasoning(
message::Reasoning::multi(summary).with_id(id),
)]
}
};
res
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct OutputReasoning {
id: String,
summary: Vec<ReasoningSummary>,
status: ToolStatus,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct OutputFunctionCall {
pub id: String,
#[serde(with = "json_utils::stringified_json")]
pub arguments: serde_json::Value,
pub call_id: String,
pub name: String,
pub status: ToolStatus,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ToolStatus {
InProgress,
Completed,
Incomplete,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct OutputMessage {
pub id: String,
pub role: OutputRole,
pub status: ResponseStatus,
pub content: Vec<AssistantContent>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum OutputRole {
Assistant,
}
impl<T> completion::CompletionModel for ResponsesCompletionModel<T>
where
T: HttpClientExt
+ Clone
+ std::fmt::Debug
+ Default
+ WasmCompatSend
+ WasmCompatSync
+ 'static,
{
type Response = CompletionResponse;
type StreamingResponse = StreamingCompletionResponse;
type Client = super::Client<T>;
fn make(client: &Self::Client, model: impl Into<String>) -> Self {
Self::new(client.clone(), model)
}
async fn completion(
&self,
completion_request: crate::completion::CompletionRequest,
) -> Result<completion::CompletionResponse<Self::Response>, CompletionError> {
let span = if tracing::Span::current().is_disabled() {
info_span!(
target: "rig::completions",
"chat",
gen_ai.operation.name = "chat",
gen_ai.provider.name = tracing::field::Empty,
gen_ai.request.model = tracing::field::Empty,
gen_ai.response.id = tracing::field::Empty,
gen_ai.response.model = tracing::field::Empty,
gen_ai.usage.output_tokens = tracing::field::Empty,
gen_ai.usage.input_tokens = tracing::field::Empty,
gen_ai.input.messages = tracing::field::Empty,
gen_ai.output.messages = tracing::field::Empty,
)
} else {
tracing::Span::current()
};
span.record("gen_ai.provider.name", "openai");
span.record("gen_ai.request.model", &self.model);
let request = self.create_completion_request(completion_request)?;
let body = serde_json::to_vec(&request)?;
if enabled!(Level::TRACE) {
tracing::trace!(
target: "rig::completions",
"OpenAI Responses completion request: {request}",
request = serde_json::to_string_pretty(&request)?
);
}
let req = self
.client
.post("/responses")?
.body(body)
.map_err(|e| CompletionError::HttpError(e.into()))?;
async move {
let response = self.client.send(req).await?;
if response.status().is_success() {
let t = http_client::text(response).await?;
let response = serde_json::from_str::<Self::Response>(&t)?;
let span = tracing::Span::current();
span.record("gen_ai.response.id", &response.id);
span.record("gen_ai.response.model", &response.model);
if let Some(ref usage) = response.usage {
span.record("gen_ai.usage.output_tokens", usage.output_tokens);
span.record("gen_ai.usage.input_tokens", usage.input_tokens);
}
if enabled!(Level::TRACE) {
tracing::trace!(
target: "rig::completions",
"OpenAI Responses completion response: {response}",
response = serde_json::to_string_pretty(&response)?
);
}
response.try_into()
} else {
let text = http_client::text(response).await?;
Err(CompletionError::ProviderError(text))
}
}
.instrument(span)
.await
}
async fn stream(
&self,
request: crate::completion::CompletionRequest,
) -> Result<
crate::streaming::StreamingCompletionResponse<Self::StreamingResponse>,
CompletionError,
> {
ResponsesCompletionModel::stream(self, request).await
}
}
impl TryFrom<CompletionResponse> for completion::CompletionResponse<CompletionResponse> {
type Error = CompletionError;
fn try_from(response: CompletionResponse) -> Result<Self, Self::Error> {
if response.output.is_empty() {
return Err(CompletionError::ResponseError(
"Response contained no parts".to_owned(),
));
}
let content: Vec<completion::AssistantContent> = response
.output
.iter()
.cloned()
.flat_map(<Vec<completion::AssistantContent>>::from)
.collect();
let choice = OneOrMany::many(content).map_err(|_| {
CompletionError::ResponseError(
"Response contained no message or tool call (empty)".to_owned(),
)
})?;
let usage = response
.usage
.as_ref()
.map(|usage| completion::Usage {
input_tokens: usage.input_tokens,
output_tokens: usage.output_tokens,
total_tokens: usage.total_tokens,
})
.unwrap_or_default();
Ok(completion::CompletionResponse {
choice,
usage,
raw_response: response,
})
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[serde(tag = "role", rename_all = "lowercase")]
pub enum Message {
#[serde(alias = "developer")]
System {
#[serde(deserialize_with = "string_or_one_or_many")]
content: OneOrMany<SystemContent>,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
},
User {
#[serde(deserialize_with = "string_or_one_or_many")]
content: OneOrMany<UserContent>,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
},
Assistant {
content: OneOrMany<AssistantContentType>,
#[serde(skip_serializing_if = "String::is_empty")]
id: String,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
status: ToolStatus,
},
#[serde(rename = "tool")]
ToolResult {
tool_call_id: String,
output: String,
},
}
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "lowercase")]
pub enum ToolResultContentType {
#[default]
Text,
}
impl Message {
pub fn system(content: &str) -> Self {
Message::System {
content: OneOrMany::one(content.to_owned().into()),
name: None,
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AssistantContent {
OutputText(Text),
Refusal { refusal: String },
}
impl From<AssistantContent> for completion::AssistantContent {
fn from(value: AssistantContent) -> Self {
match value {
AssistantContent::Refusal { refusal } => {
completion::AssistantContent::Text(Text { text: refusal })
}
AssistantContent::OutputText(Text { text }) => {
completion::AssistantContent::Text(Text { text })
}
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[serde(untagged)]
pub enum AssistantContentType {
Text(AssistantContent),
ToolCall(OutputFunctionCall),
Reasoning(OpenAIReasoning),
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum UserContent {
InputText {
text: String,
},
InputImage {
image_url: String,
#[serde(default)]
detail: ImageDetail,
},
InputFile {
#[serde(skip_serializing_if = "Option::is_none")]
file_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
file_data: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
filename: Option<String>,
},
Audio {
input_audio: InputAudio,
},
#[serde(rename = "tool")]
ToolResult {
tool_call_id: String,
output: String,
},
}
impl TryFrom<message::Message> for Vec<Message> {
type Error = message::MessageError;
fn try_from(message: message::Message) -> Result<Self, Self::Error> {
match message {
message::Message::User { content } => {
let (tool_results, other_content): (Vec<_>, Vec<_>) = content
.into_iter()
.partition(|content| matches!(content, message::UserContent::ToolResult(_)));
if !tool_results.is_empty() {
tool_results
.into_iter()
.map(|content| match content {
message::UserContent::ToolResult(message::ToolResult {
call_id,
content,
..
}) => Ok::<_, message::MessageError>(Message::ToolResult {
tool_call_id: call_id.expect("The tool call ID should exist"),
output: {
let res = content.first();
match res {
completion::message::ToolResultContent::Text(Text {
text,
}) => text,
_ => return Err(MessageError::ConversionError("This API only currently supports text tool results".into()))
}
},
}),
_ => unreachable!(),
})
.collect::<Result<Vec<_>, _>>()
} else {
let other_content = other_content
.into_iter()
.map(|content| match content {
message::UserContent::Text(message::Text { text }) => {
Ok(UserContent::InputText { text })
}
message::UserContent::Image(message::Image {
data,
detail,
media_type,
..
}) => {
let url = match data {
DocumentSourceKind::Base64(data) => {
let media_type = if let Some(media_type) = media_type {
media_type.to_mime_type().to_string()
} else {
String::new()
};
format!("data:{media_type};base64,{data}")
}
DocumentSourceKind::Url(url) => url,
DocumentSourceKind::Raw(_) => {
return Err(MessageError::ConversionError(
"Raw files not supported, encode as base64 first"
.into(),
));
}
doc => {
return Err(MessageError::ConversionError(format!(
"Unsupported document type: {doc}"
)));
}
};
Ok(UserContent::InputImage {
image_url: url,
detail: detail.unwrap_or_default(),
})
}
message::UserContent::Document(message::Document {
media_type: Some(DocumentMediaType::PDF),
data,
..
}) => {
let (file_data, file_url) = match data {
DocumentSourceKind::Base64(data) => {
(Some(format!("data:application/pdf;base64,{data}")), None)
}
DocumentSourceKind::Url(url) => (None, Some(url)),
DocumentSourceKind::Raw(_) => {
return Err(MessageError::ConversionError(
"Raw files not supported, encode as base64 first"
.into(),
));
}
doc => {
return Err(MessageError::ConversionError(format!(
"Unsupported document type: {doc}"
)));
}
};
Ok(UserContent::InputFile {
file_url,
file_data,
filename: Some("document.pdf".into()),
})
}
message::UserContent::Document(message::Document {
data: DocumentSourceKind::Base64(text),
..
}) => Ok(UserContent::InputText { text }),
message::UserContent::Audio(message::Audio {
data: DocumentSourceKind::Base64(data),
media_type,
..
}) => Ok(UserContent::Audio {
input_audio: InputAudio {
data,
format: match media_type {
Some(media_type) => media_type,
None => AudioMediaType::MP3,
},
},
}),
message::UserContent::Audio(_) => Err(MessageError::ConversionError(
"Audio must be base64 encoded data".into(),
)),
_ => unreachable!(),
})
.collect::<Result<Vec<_>, _>>()?;
let other_content = OneOrMany::many(other_content).expect(
"There must be other content here if there were no tool result content",
);
Ok(vec![Message::User {
content: other_content,
name: None,
}])
}
}
message::Message::Assistant { content, id } => {
let assistant_message_id = id;
match content.first() {
crate::message::AssistantContent::Text(Text { text }) => {
Ok(vec![Message::Assistant {
id: assistant_message_id
.expect("The assistant message ID should exist"),
status: ToolStatus::Completed,
content: OneOrMany::one(AssistantContentType::Text(
AssistantContent::OutputText(Text { text }),
)),
name: None,
}])
}
crate::message::AssistantContent::ToolCall(crate::message::ToolCall {
id,
call_id,
function,
..
}) => Ok(vec![Message::Assistant {
content: OneOrMany::one(AssistantContentType::ToolCall(
OutputFunctionCall {
call_id: call_id.expect("The call ID should exist"),
arguments: function.arguments,
id,
name: function.name,
status: ToolStatus::Completed,
},
)),
id: assistant_message_id.expect("The assistant message ID should exist!"),
name: None,
status: ToolStatus::Completed,
}]),
crate::message::AssistantContent::Reasoning(crate::message::Reasoning {
id,
reasoning,
..
}) => Ok(vec![Message::Assistant {
content: OneOrMany::one(AssistantContentType::Reasoning(OpenAIReasoning {
id: id.expect("An OpenAI-generated ID is required when using OpenAI reasoning items"),
summary: reasoning.into_iter().map(|x| ReasoningSummary::SummaryText { text: x }).collect(),
encrypted_content: None,
status: Some(ToolStatus::Completed),
})),
id: assistant_message_id.expect("The assistant message ID should exist!"),
name: None,
status: (ToolStatus::Completed),
}]),
crate::message::AssistantContent::Image(_) => {
Err(MessageError::ConversionError(
"Assistant image content is not supported in OpenAI Responses API".into(),
))
}
}
}
}
}
}
impl FromStr for UserContent {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(UserContent::InputText {
text: s.to_string(),
})
}
}