use crate::openai::types::{FunctionCall, ToolCall};
use serde::Serialize;
use serde_json::{Map, Value};
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct ModelList {
pub object: &'static str,
pub data: Vec<ModelObject>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct ModelObject {
pub id: String,
pub object: &'static str,
pub owned_by: &'static str,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct ChatCompletionResponse {
pub id: String,
pub object: &'static str,
pub created: i64,
pub model: String,
pub choices: Vec<ChatChoice>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<Usage>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct ChatChoice {
pub index: u32,
pub message: AssistantMessage,
pub finish_reason: String,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct AssistantMessage {
pub role: &'static str,
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub images: Option<Vec<GeneratedImage>>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct Usage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
}
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct ResponseObject {
pub id: String,
pub object: &'static str,
pub created_at: i64,
pub status: String,
pub error: Option<Value>,
pub incomplete_details: Option<Value>,
pub instructions: Option<String>,
pub max_output_tokens: Option<u32>,
pub model: String,
pub output: Vec<ResponseOutputItem>,
pub parallel_tool_calls: bool,
pub store: bool,
pub temperature: Option<f64>,
pub tool_choice: Option<Value>,
pub tools: Vec<Value>,
pub usage: Option<Usage>,
pub metadata: Option<Map<String, Value>>,
pub previous_response_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct ResponseOutputItem {
pub id: String,
#[serde(rename = "type")]
pub kind: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<&'static str>,
pub status: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub content: Vec<ResponseOutputContent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub call_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub revised_prompt: Option<String>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct ResponseOutputContent {
#[serde(rename = "type")]
pub kind: &'static str,
pub text: String,
pub annotations: Vec<Value>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct GeneratedImage {
pub b64_json: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub revised_prompt: Option<String>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct ImageGenerationResponse {
pub created: i64,
pub data: Vec<GeneratedImage>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct ResponseInputTokens {
pub input_tokens: u32,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct ResponseCompaction {
pub output: Vec<Value>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct ChatCompletionChunk {
pub id: String,
pub object: &'static str,
pub created: i64,
pub model: String,
pub choices: Vec<ChunkChoice>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct ChunkChoice {
pub index: u32,
pub delta: DeltaMessage,
#[serde(skip_serializing_if = "Option::is_none")]
pub finish_reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct DeltaMessage {
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCallDelta>>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct ToolCallDelta {
pub index: u32,
pub id: String,
#[serde(rename = "type")]
pub kind: &'static str,
pub function: FunctionCall,
}
impl ModelList {
#[must_use]
pub fn from_ids(ids: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
Self {
object: "list",
data: ids
.into_iter()
.map(|id| ModelObject {
id: id.as_ref().to_owned(),
object: "model",
owned_by: "openai-codex",
})
.collect(),
}
}
}
#[must_use]
pub fn response_message_item(id: String, text: Option<String>) -> ResponseOutputItem {
let content = text
.filter(|value| !value.is_empty())
.map(|value| {
vec![ResponseOutputContent {
kind: "output_text",
text: value,
annotations: Vec::new(),
}]
})
.unwrap_or_default();
ResponseOutputItem {
id,
kind: "message",
role: Some("assistant"),
status: "completed".to_owned(),
content,
call_id: None,
name: None,
arguments: None,
result: None,
revised_prompt: None,
}
}
#[must_use]
pub fn response_function_call_item(id: String, tool_call: ToolCall) -> ResponseOutputItem {
ResponseOutputItem {
id,
kind: "function_call",
role: None,
status: "completed".to_owned(),
content: Vec::new(),
call_id: Some(tool_call.id),
name: Some(tool_call.function.name),
arguments: Some(tool_call.function.arguments),
result: None,
revised_prompt: None,
}
}
#[must_use]
pub fn response_image_generation_item(
id: String,
result: String,
revised_prompt: Option<String>,
) -> ResponseOutputItem {
ResponseOutputItem {
id,
kind: "image_generation_call",
role: None,
status: "completed".to_owned(),
content: Vec::new(),
call_id: None,
name: None,
arguments: None,
result: Some(result),
revised_prompt,
}
}
#[must_use]
pub const fn image_generation_response(
created: i64,
data: Vec<GeneratedImage>,
) -> ImageGenerationResponse {
ImageGenerationResponse { created, data }
}
#[must_use]
pub fn generated_image_from_item(item: &Value) -> Option<GeneratedImage> {
let raw = item
.get("result")
.or_else(|| item.get("b64_json"))
.and_then(Value::as_str)?;
let (media_type, b64_json) = parse_image_payload(raw, item);
Some(GeneratedImage {
b64_json,
media_type,
revised_prompt: item
.get("revised_prompt")
.and_then(Value::as_str)
.map(str::to_owned),
})
}
fn parse_image_payload(raw: &str, item: &Value) -> (Option<String>, String) {
if let Some(rest) = raw.strip_prefix("data:") {
if let Some((header, data)) = rest.split_once(',') {
let media_type = header
.split(';')
.next()
.filter(|value| !value.is_empty())
.map(str::to_owned);
return (media_type, data.to_owned());
}
}
let media_type = item
.get("media_type")
.or_else(|| item.get("mime_type"))
.and_then(Value::as_str)
.map(str::to_owned)
.or_else(|| {
item.get("output_format")
.and_then(Value::as_str)
.map(|format| format!("image/{format}"))
});
(media_type, raw.to_owned())
}
#[must_use]
pub fn generated_images_from_output(items: &[Value]) -> Vec<GeneratedImage> {
items.iter().filter_map(generated_image_from_item).collect()
}
#[must_use]
pub fn generated_images_from_response_items(items: &[ResponseOutputItem]) -> Vec<GeneratedImage> {
items
.iter()
.filter_map(|item| {
let raw = item.result.as_deref()?;
Some(GeneratedImage {
b64_json: raw.to_owned(),
media_type: None,
revised_prompt: item.revised_prompt.clone(),
})
})
.collect()
}
#[must_use]
pub fn generated_image_data_url(image: &GeneratedImage) -> String {
let media_type = image.media_type.as_deref().unwrap_or("image/png");
format!("data:{media_type};base64,{}", image.b64_json)
}
#[must_use]
pub fn chunk_with_role(id: &str, created: i64, model: &str) -> ChatCompletionChunk {
ChatCompletionChunk {
id: id.to_owned(),
object: "chat.completion.chunk",
created,
model: model.to_owned(),
choices: vec![ChunkChoice {
index: 0,
delta: DeltaMessage {
role: Some("assistant"),
content: None,
tool_calls: None,
},
finish_reason: None,
}],
}
}
#[must_use]
pub fn chunk_with_content(
id: &str,
created: i64,
model: &str,
content: String,
) -> ChatCompletionChunk {
ChatCompletionChunk {
id: id.to_owned(),
object: "chat.completion.chunk",
created,
model: model.to_owned(),
choices: vec![ChunkChoice {
index: 0,
delta: DeltaMessage {
role: None,
content: Some(content),
tool_calls: None,
},
finish_reason: None,
}],
}
}
#[must_use]
pub fn chunk_with_tool_call(
id: &str,
created: i64,
model: &str,
index: u32,
tool_call: ToolCall,
) -> ChatCompletionChunk {
ChatCompletionChunk {
id: id.to_owned(),
object: "chat.completion.chunk",
created,
model: model.to_owned(),
choices: vec![ChunkChoice {
index: 0,
delta: DeltaMessage {
role: None,
content: None,
tool_calls: Some(vec![ToolCallDelta {
index,
id: tool_call.id,
kind: "function",
function: tool_call.function,
}]),
},
finish_reason: None,
}],
}
}
#[must_use]
pub fn chunk_finished(id: &str, created: i64, model: &str, reason: &str) -> ChatCompletionChunk {
ChatCompletionChunk {
id: id.to_owned(),
object: "chat.completion.chunk",
created,
model: model.to_owned(),
choices: vec![ChunkChoice {
index: 0,
delta: DeltaMessage {
role: None,
content: None,
tool_calls: None,
},
finish_reason: Some(reason.to_owned()),
}],
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn extracts_generated_image_from_plain_base64_result() {
let image = generated_image_from_item(&json!({
"type": "image_generation_call",
"result": "YWJj",
"output_format": "png",
"revised_prompt": "refined"
}))
.unwrap();
assert_eq!(image.b64_json, "YWJj");
assert_eq!(image.media_type.as_deref(), Some("image/png"));
assert_eq!(image.revised_prompt.as_deref(), Some("refined"));
}
#[test]
fn extracts_generated_image_from_data_url_result() {
let image = generated_image_from_item(&json!({
"type": "image_generation_call",
"result": "data:image/webp;base64,AAAA"
}))
.unwrap();
assert_eq!(image.b64_json, "AAAA");
assert_eq!(image.media_type.as_deref(), Some("image/webp"));
}
}