use std::collections::{HashMap, HashSet};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use validator::{Validate, ValidationError};
use super::{
common::{
default_true, validate_stop, ChatLogProbs, Function, GenerationRequest,
PromptTokenUsageInfo, StringOrArray, ToolChoice, ToolChoiceValue, ToolReference, UsageInfo,
},
sampling_params::{validate_top_k_value, validate_top_p_value},
};
use crate::{builders::ResponsesResponseBuilder, validated::Normalizable};
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum ResponseTool {
#[serde(rename = "function")]
Function(FunctionTool),
#[serde(rename = "web_search_preview")]
WebSearchPreview(WebSearchPreviewTool),
#[serde(rename = "code_interpreter")]
CodeInterpreter(CodeInterpreterTool),
#[serde(rename = "mcp")]
Mcp(McpTool),
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct FunctionTool {
#[serde(flatten)]
pub function: Function,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct McpTool {
pub server_url: Option<String>,
pub authorization: Option<String>,
pub headers: Option<HashMap<String, String>>,
pub server_label: String,
pub server_description: Option<String>,
pub require_approval: Option<RequireApproval>,
pub allowed_tools: Option<Vec<String>>,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize, Default, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WebSearchPreviewTool {
pub search_context_size: Option<String>,
pub user_location: Option<Value>,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize, Default, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CodeInterpreterTool {
pub container: Option<Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum RequireApproval {
Always,
Never,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
pub struct ResponseReasoningParam {
#[serde(default = "default_reasoning_effort")]
pub effort: Option<ReasoningEffort>,
pub summary: Option<ReasoningSummary>,
}
#[expect(
clippy::unnecessary_wraps,
reason = "serde default function must match field type Option<T>"
)]
fn default_reasoning_effort() -> Option<ReasoningEffort> {
Some(ReasoningEffort::Medium)
}
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ReasoningEffort {
Minimal,
Low,
Medium,
High,
}
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ReasoningSummary {
Auto,
Concise,
Detailed,
}
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
#[serde(untagged)]
pub enum StringOrContentParts {
String(String),
Array(Vec<ResponseContentPart>),
}
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum ResponseInputOutputItem {
#[serde(rename = "message")]
Message {
id: String,
role: String,
content: Vec<ResponseContentPart>,
#[serde(skip_serializing_if = "Option::is_none")]
status: Option<String>,
},
#[serde(rename = "reasoning")]
Reasoning {
id: String,
summary: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
content: Vec<ResponseReasoningContent>,
#[serde(skip_serializing_if = "Option::is_none")]
status: Option<String>,
},
#[serde(rename = "function_call")]
FunctionToolCall {
id: String,
call_id: String,
name: String,
arguments: String,
#[serde(skip_serializing_if = "Option::is_none")]
output: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
status: Option<String>,
},
#[serde(rename = "function_call_output")]
FunctionCallOutput {
id: Option<String>,
call_id: String,
output: String,
#[serde(skip_serializing_if = "Option::is_none")]
status: Option<String>,
},
#[serde(rename = "mcp_approval_request")]
McpApprovalRequest {
id: String,
server_label: String,
name: String,
arguments: String,
},
#[serde(rename = "mcp_approval_response")]
McpApprovalResponse {
#[serde(skip_serializing_if = "Option::is_none")]
id: Option<String>,
approval_request_id: String,
approve: bool,
#[serde(skip_serializing_if = "Option::is_none")]
reason: Option<String>,
},
#[serde(untagged)]
SimpleInputMessage {
content: StringOrContentParts,
role: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "type")]
r#type: Option<String>,
},
}
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum ResponseContentPart {
#[serde(rename = "output_text")]
OutputText {
text: String,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
annotations: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
logprobs: Option<ChatLogProbs>,
},
#[serde(rename = "input_text")]
InputText { text: String },
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum ResponseReasoningContent {
#[serde(rename = "reasoning_text")]
ReasoningText { text: String },
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
pub struct McpToolInfo {
pub name: String,
pub description: Option<String>,
pub input_schema: Value,
pub annotations: Option<Value>,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum ResponseOutputItem {
#[serde(rename = "message")]
Message {
id: String,
role: String,
content: Vec<ResponseContentPart>,
status: String,
},
#[serde(rename = "reasoning")]
Reasoning {
id: String,
summary: Vec<String>,
content: Vec<ResponseReasoningContent>,
status: Option<String>,
},
#[serde(rename = "function_call")]
FunctionToolCall {
id: String,
call_id: String,
name: String,
arguments: String,
output: Option<String>,
status: String,
},
#[serde(rename = "mcp_list_tools")]
McpListTools {
id: String,
server_label: String,
tools: Vec<McpToolInfo>,
},
#[serde(rename = "mcp_call")]
McpCall {
id: String,
status: String,
approval_request_id: Option<String>,
arguments: String,
error: Option<String>,
name: String,
output: String,
server_label: String,
},
#[serde(rename = "mcp_approval_request")]
McpApprovalRequest {
id: String,
server_label: String,
name: String,
arguments: String,
},
#[serde(rename = "web_search_call")]
WebSearchCall {
id: String,
status: WebSearchCallStatus,
action: WebSearchAction,
},
#[serde(rename = "code_interpreter_call")]
CodeInterpreterCall {
id: String,
status: CodeInterpreterCallStatus,
container_id: String,
code: Option<String>,
outputs: Option<Vec<CodeInterpreterOutput>>,
},
#[serde(rename = "file_search_call")]
FileSearchCall {
id: String,
status: FileSearchCallStatus,
queries: Vec<String>,
results: Option<Vec<FileSearchResult>>,
},
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WebSearchCallStatus {
InProgress,
Searching,
Completed,
Failed,
}
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum WebSearchAction {
Search {
#[serde(skip_serializing_if = "Option::is_none")]
query: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
queries: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
sources: Vec<WebSearchSource>,
},
OpenPage {
url: String,
},
Find {
url: String,
pattern: String,
},
}
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
pub struct WebSearchSource {
#[serde(rename = "type")]
pub source_type: String,
pub url: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum CodeInterpreterCallStatus {
InProgress,
Completed,
Incomplete,
Interpreting,
Failed,
}
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum CodeInterpreterOutput {
Logs { logs: String },
Image { url: String },
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum FileSearchCallStatus {
InProgress,
Searching,
Completed,
Incomplete,
Failed,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
pub struct FileSearchResult {
pub file_id: String,
pub filename: String,
pub text: Option<String>,
pub score: Option<f32>,
pub attributes: Option<Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
#[schemars(rename = "ResponsesServiceTier")]
pub enum ServiceTier {
#[default]
Auto,
Default,
Flex,
Scale,
Priority,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Truncation {
Auto,
#[default]
Disabled,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ResponseStatus {
Queued,
InProgress,
Completed,
Failed,
Cancelled,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
pub struct ReasoningInfo {
pub effort: Option<String>,
pub summary: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
pub struct TextConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<TextFormat>,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
#[serde(tag = "type")]
pub enum TextFormat {
#[serde(rename = "text")]
Text,
#[serde(rename = "json_object")]
JsonObject,
#[serde(rename = "json_schema")]
JsonSchema {
name: String,
schema: Value,
description: Option<String>,
strict: Option<bool>,
},
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum IncludeField {
#[serde(rename = "code_interpreter_call.outputs")]
CodeInterpreterCallOutputs,
#[serde(rename = "computer_call_output.output.image_url")]
ComputerCallOutputImageUrl,
#[serde(rename = "file_search_call.results")]
FileSearchCallResults,
#[serde(rename = "message.input_image.image_url")]
MessageInputImageUrl,
#[serde(rename = "message.output_text.logprobs")]
MessageOutputTextLogprobs,
#[serde(rename = "reasoning.encrypted_content")]
ReasoningEncryptedContent,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
pub struct ResponseUsage {
pub input_tokens: u32,
pub output_tokens: u32,
pub total_tokens: u32,
pub input_tokens_details: Option<InputTokensDetails>,
pub output_tokens_details: Option<OutputTokensDetails>,
}
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
#[serde(untagged)]
pub enum ResponsesUsage {
Classic(UsageInfo),
Modern(ResponseUsage),
}
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
pub struct InputTokensDetails {
pub cached_tokens: u32,
}
impl From<&PromptTokenUsageInfo> for InputTokensDetails {
fn from(d: &PromptTokenUsageInfo) -> Self {
Self {
cached_tokens: d.cached_tokens,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
pub struct OutputTokensDetails {
pub reasoning_tokens: u32,
}
impl UsageInfo {
pub fn to_response_usage(&self) -> ResponseUsage {
ResponseUsage {
input_tokens: self.prompt_tokens,
output_tokens: self.completion_tokens,
total_tokens: self.total_tokens,
input_tokens_details: self
.prompt_tokens_details
.as_ref()
.map(InputTokensDetails::from),
output_tokens_details: self.reasoning_tokens.map(|tokens| OutputTokensDetails {
reasoning_tokens: tokens,
}),
}
}
}
impl From<UsageInfo> for ResponseUsage {
fn from(usage: UsageInfo) -> Self {
usage.to_response_usage()
}
}
impl ResponseUsage {
pub fn to_usage_info(&self) -> UsageInfo {
UsageInfo {
prompt_tokens: self.input_tokens,
completion_tokens: self.output_tokens,
total_tokens: self.total_tokens,
reasoning_tokens: self
.output_tokens_details
.as_ref()
.map(|details| details.reasoning_tokens),
prompt_tokens_details: self.input_tokens_details.as_ref().map(|details| {
PromptTokenUsageInfo {
cached_tokens: details.cached_tokens,
}
}),
}
}
}
impl ResponsesUsage {
pub fn to_response_usage(&self) -> ResponseUsage {
match self {
ResponsesUsage::Classic(usage) => usage.to_response_usage(),
ResponsesUsage::Modern(usage) => usage.clone(),
}
}
pub fn to_usage_info(&self) -> UsageInfo {
match self {
ResponsesUsage::Classic(usage) => usage.clone(),
ResponsesUsage::Modern(usage) => usage.to_usage_info(),
}
}
}
fn default_top_k() -> i32 {
-1
}
fn default_repetition_penalty() -> f32 {
1.0
}
#[expect(
clippy::unnecessary_wraps,
reason = "serde default function must match field type Option<T>"
)]
fn default_temperature() -> Option<f32> {
Some(1.0)
}
#[derive(Debug, Clone, Deserialize, Serialize, Validate, schemars::JsonSchema)]
#[validate(schema(function = "validate_responses_cross_parameters"))]
pub struct ResponsesRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include: Option<Vec<IncludeField>>,
#[validate(custom(function = "validate_response_input"))]
pub input: ResponseInput,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(range(min = 1))]
pub max_output_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(range(min = 1))]
pub max_tool_calls: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<HashMap<String, Value>>,
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(custom(function = "validate_conversation_id"))]
pub conversation: Option<String>,
#[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<ResponseReasoningParam>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<ServiceTier>,
#[serde(skip_serializing_if = "Option::is_none")]
pub store: Option<bool>,
#[serde(default)]
pub stream: Option<bool>,
#[serde(
default = "default_temperature",
skip_serializing_if = "Option::is_none"
)]
#[validate(range(min = 0.0, max = 2.0))]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<ToolChoice>,
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(custom(function = "validate_response_tools"))]
pub tools: Option<Vec<ResponseTool>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(range(min = 0, max = 20))]
pub top_logprobs: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(custom(function = "validate_top_p_value"))]
pub top_p: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub truncation: Option<Truncation>,
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(custom(function = "validate_text_format"))]
pub text: Option<TextConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
#[serde(default)]
pub priority: i32,
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(range(min = -2.0, max = 2.0))]
pub frequency_penalty: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(range(min = -2.0, max = 2.0))]
pub presence_penalty: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(custom(function = "validate_stop"))]
pub stop: Option<StringOrArray>,
#[serde(default = "default_top_k")]
#[validate(custom(function = "validate_top_k_value"))]
pub top_k: i32,
#[serde(default)]
#[validate(range(min = 0.0, max = 1.0))]
pub min_p: f32,
#[serde(default = "default_repetition_penalty")]
#[validate(range(min = 0.0, max = 2.0))]
pub repetition_penalty: f32,
}
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
#[serde(untagged)]
pub enum ResponseInput {
Items(Vec<ResponseInputOutputItem>),
Text(String),
}
impl Default for ResponsesRequest {
fn default() -> Self {
Self {
background: None,
include: None,
input: ResponseInput::Text(String::new()),
instructions: None,
max_output_tokens: None,
max_tool_calls: None,
metadata: None,
model: String::new(),
conversation: None,
parallel_tool_calls: None,
previous_response_id: None,
reasoning: None,
service_tier: None,
store: None,
stream: None,
temperature: None,
tool_choice: None,
tools: None,
top_logprobs: None,
top_p: None,
truncation: None,
text: None,
user: None,
request_id: None,
priority: 0,
frequency_penalty: None,
presence_penalty: None,
stop: None,
top_k: default_top_k(),
min_p: 0.0,
repetition_penalty: default_repetition_penalty(),
}
}
}
impl Normalizable for ResponsesRequest {
fn normalize(&mut self) {
if self.tool_choice.is_none() {
if let Some(tools) = &self.tools {
let choice_value = if tools.is_empty() {
ToolChoiceValue::None
} else {
ToolChoiceValue::Auto
};
self.tool_choice = Some(ToolChoice::Value(choice_value));
}
}
if self.parallel_tool_calls.is_none() && self.tools.is_some() {
self.parallel_tool_calls = Some(true);
}
if self.store.is_none() {
self.store = Some(true);
}
}
}
impl GenerationRequest for ResponsesRequest {
fn is_stream(&self) -> bool {
self.stream.unwrap_or(false)
}
fn get_model(&self) -> Option<&str> {
Some(self.model.as_str())
}
fn extract_text_for_routing(&self) -> String {
match &self.input {
ResponseInput::Text(text) => text.clone(),
ResponseInput::Items(items) => {
let mut result = String::with_capacity(256);
let mut has_parts = false;
let mut append_text = |text: &str| {
if has_parts {
result.push(' ');
}
has_parts = true;
result.push_str(text);
};
for item in items {
match item {
ResponseInputOutputItem::Message { content, .. } => {
for part in content {
let text = match part {
ResponseContentPart::OutputText { text, .. } => {
Some(text.as_str())
}
ResponseContentPart::InputText { text } => Some(text.as_str()),
ResponseContentPart::Unknown => None,
};
if let Some(t) = text {
append_text(t);
}
}
}
ResponseInputOutputItem::SimpleInputMessage { content, .. } => {
match content {
StringOrContentParts::String(s) => {
append_text(s.as_str());
}
StringOrContentParts::Array(parts) => {
for part in parts {
let text = match part {
ResponseContentPart::OutputText { text, .. } => {
Some(text.as_str())
}
ResponseContentPart::InputText { text } => {
Some(text.as_str())
}
ResponseContentPart::Unknown => None,
};
if let Some(t) = text {
append_text(t);
}
}
}
}
}
ResponseInputOutputItem::Reasoning { content, .. } => {
for part in content {
match part {
ResponseReasoningContent::ReasoningText { text } => {
append_text(text.as_str());
}
}
}
}
ResponseInputOutputItem::FunctionToolCall { .. }
| ResponseInputOutputItem::FunctionCallOutput { .. }
| ResponseInputOutputItem::McpApprovalRequest { .. }
| ResponseInputOutputItem::McpApprovalResponse { .. } => {}
}
}
result
}
}
}
}
pub fn validate_conversation_id(conv_id: &str) -> Result<(), ValidationError> {
if !conv_id.starts_with("conv_") {
let mut error = ValidationError::new("invalid_conversation_id");
error.message = Some(std::borrow::Cow::Owned(format!(
"Invalid 'conversation': '{conv_id}'. Expected an ID that begins with 'conv_'."
)));
return Err(error);
}
let is_valid = conv_id
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '-');
if !is_valid {
let mut error = ValidationError::new("invalid_conversation_id");
error.message = Some(std::borrow::Cow::Owned(format!(
"Invalid 'conversation': '{conv_id}'. Expected an ID that contains letters, numbers, underscores, or dashes, but this value contained additional characters."
)));
return Err(error);
}
Ok(())
}
fn validate_tool_choice_with_tools(request: &ResponsesRequest) -> Result<(), ValidationError> {
let Some(tool_choice) = &request.tool_choice else {
return Ok(());
};
let has_tools = request.tools.as_ref().is_some_and(|t| !t.is_empty());
let is_some_choice = !matches!(tool_choice, ToolChoice::Value(ToolChoiceValue::None));
if is_some_choice && !has_tools {
let mut e = ValidationError::new("tool_choice_requires_tools");
e.message = Some("Invalid value for 'tool_choice': 'tool_choice' is only allowed when 'tools' are specified.".into());
return Err(e);
}
if !has_tools {
return Ok(());
}
let Some(tools) = request.tools.as_ref() else {
return Ok(());
};
let function_tool_names: Vec<&str> = tools
.iter()
.filter_map(|t| match t {
ResponseTool::Function(ft) => Some(ft.function.name.as_str()),
_ => None,
})
.collect();
match tool_choice {
ToolChoice::Function { function, .. } => {
if !function_tool_names.contains(&function.name.as_str()) {
let mut e = ValidationError::new("tool_choice_function_not_found");
e.message = Some(
format!(
"Invalid value for 'tool_choice': function '{}' not found in 'tools'.",
function.name
)
.into(),
);
return Err(e);
}
}
ToolChoice::AllowedTools {
mode,
tools: allowed_tools,
..
} => {
if mode != "auto" && mode != "required" {
let mut e = ValidationError::new("tool_choice_invalid_mode");
e.message = Some(
format!(
"Invalid value for 'tool_choice.mode': must be 'auto' or 'required', got '{mode}'."
)
.into(),
);
return Err(e);
}
for tool_ref in allowed_tools {
if let ToolReference::Function { name } = tool_ref {
if !function_tool_names.contains(&name.as_str()) {
let mut e = ValidationError::new("tool_choice_tool_not_found");
e.message = Some(
format!(
"Invalid value for 'tool_choice.tools': tool '{name}' not found in 'tools'."
)
.into(),
);
return Err(e);
}
}
}
}
ToolChoice::Value(_) => {}
}
Ok(())
}
fn validate_responses_cross_parameters(request: &ResponsesRequest) -> Result<(), ValidationError> {
validate_tool_choice_with_tools(request)?;
if request.top_logprobs.is_some() {
let has_logprobs_include = request
.include
.as_ref()
.is_some_and(|inc| inc.contains(&IncludeField::MessageOutputTextLogprobs));
if !has_logprobs_include {
let mut e = ValidationError::new("top_logprobs_requires_include");
e.message = Some(
"top_logprobs requires include field with 'message.output_text.logprobs'".into(),
);
return Err(e);
}
}
if request.background == Some(true) && request.stream == Some(true) {
let mut e = ValidationError::new("background_conflicts_with_stream");
e.message = Some("Cannot use background mode with streaming".into());
return Err(e);
}
if request.conversation.is_some() && request.previous_response_id.is_some() {
let mut e = ValidationError::new("mutually_exclusive_parameters");
e.message = Some("Mutually exclusive parameters. Ensure you are only providing one of: 'previous_response_id' or 'conversation'.".into());
return Err(e);
}
if let ResponseInput::Items(items) = &request.input {
let has_valid_input = items.iter().any(|item| {
matches!(
item,
ResponseInputOutputItem::Message { .. }
| ResponseInputOutputItem::SimpleInputMessage { .. }
)
});
if !has_valid_input {
let mut e = ValidationError::new("input_missing_user_message");
e.message = Some("Input items must contain at least one message".into());
return Err(e);
}
}
Ok(())
}
fn validate_response_input(input: &ResponseInput) -> Result<(), ValidationError> {
match input {
ResponseInput::Text(text) => {
if text.is_empty() {
let mut e = ValidationError::new("input_text_empty");
e.message = Some("Input text cannot be empty".into());
return Err(e);
}
}
ResponseInput::Items(items) => {
if items.is_empty() {
let mut e = ValidationError::new("input_items_empty");
e.message = Some("Input items cannot be empty".into());
return Err(e);
}
for item in items {
validate_input_item(item)?;
}
}
}
Ok(())
}
fn validate_input_item(item: &ResponseInputOutputItem) -> Result<(), ValidationError> {
match item {
ResponseInputOutputItem::Message { content, .. } => {
if content.is_empty() {
let mut e = ValidationError::new("message_content_empty");
e.message = Some("Message content cannot be empty".into());
return Err(e);
}
}
ResponseInputOutputItem::SimpleInputMessage { content, .. } => match content {
StringOrContentParts::String(s) if s.is_empty() => {
let mut e = ValidationError::new("message_content_empty");
e.message = Some("Message content cannot be empty".into());
return Err(e);
}
StringOrContentParts::Array(parts) if parts.is_empty() => {
let mut e = ValidationError::new("message_content_empty");
e.message = Some("Message content parts cannot be empty".into());
return Err(e);
}
_ => {}
},
ResponseInputOutputItem::Reasoning { .. } => {
}
ResponseInputOutputItem::FunctionCallOutput { output, .. } => {
if output.is_empty() {
let mut e = ValidationError::new("function_output_empty");
e.message = Some("Function call output cannot be empty".into());
return Err(e);
}
}
ResponseInputOutputItem::FunctionToolCall { .. } => {}
ResponseInputOutputItem::McpApprovalRequest { .. } => {}
ResponseInputOutputItem::McpApprovalResponse { .. } => {}
}
Ok(())
}
fn validate_response_tools(tools: &[ResponseTool]) -> Result<(), ValidationError> {
let mut seen_mcp_labels: HashSet<String> = HashSet::new();
for (idx, tool) in tools.iter().enumerate() {
if let ResponseTool::Mcp(mcp) = tool {
let raw_label = mcp.server_label.as_str();
if raw_label.is_empty() {
let mut e = ValidationError::new("missing_required_parameter");
e.message = Some(
format!("Missing required parameter: 'tools[{idx}].server_label'.").into(),
);
return Err(e);
}
let valid = raw_label.starts_with(|c: char| c.is_ascii_alphabetic())
&& raw_label
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_');
if !valid {
let mut e = ValidationError::new("invalid_server_label");
e.message = Some(
format!(
"Invalid input {raw_label}: 'server_label' must start with a letter and consist of only letters, digits, '-' and '_'"
)
.into(),
);
return Err(e);
}
let normalized = raw_label.to_lowercase();
if !seen_mcp_labels.insert(normalized) {
let mut e = ValidationError::new("mcp_tool_duplicate_server_label");
e.message = Some(
format!("Duplicate MCP server_label '{raw_label}' found in 'tools' parameter.")
.into(),
);
return Err(e);
}
}
}
Ok(())
}
fn validate_text_format(text: &TextConfig) -> Result<(), ValidationError> {
if let Some(TextFormat::JsonSchema { name, .. }) = &text.format {
if name.is_empty() {
let mut e = ValidationError::new("json_schema_name_empty");
e.message = Some("JSON schema name cannot be empty".into());
return Err(e);
}
}
Ok(())
}
pub fn normalize_input_item(item: &ResponseInputOutputItem) -> ResponseInputOutputItem {
match item {
ResponseInputOutputItem::SimpleInputMessage { content, role, .. } => {
let content_vec = match content {
StringOrContentParts::String(s) => {
vec![ResponseContentPart::InputText { text: s.clone() }]
}
StringOrContentParts::Array(parts) => parts.clone(),
};
ResponseInputOutputItem::Message {
id: generate_id("msg"),
role: role.clone(),
content: content_vec,
status: Some("completed".to_string()),
}
}
_ => item.clone(),
}
}
pub fn generate_id(prefix: &str) -> String {
use rand::RngCore;
let mut rng = rand::rng();
let mut bytes = [0u8; 25];
rng.fill_bytes(&mut bytes);
let hex_string: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
format!("{prefix}_{hex_string}")
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
pub struct ResponsesResponse {
pub id: String,
#[serde(default = "default_object_type")]
pub object: String,
pub created_at: i64,
pub status: ResponseStatus,
pub error: Option<Value>,
pub incomplete_details: Option<Value>,
pub instructions: Option<String>,
pub max_output_tokens: Option<u32>,
pub model: String,
#[serde(default)]
pub output: Vec<ResponseOutputItem>,
#[serde(default = "default_true")]
pub parallel_tool_calls: bool,
pub previous_response_id: Option<String>,
pub reasoning: Option<ReasoningInfo>,
#[serde(default = "default_true")]
pub store: bool,
pub temperature: Option<f32>,
pub text: Option<TextConfig>,
#[serde(default = "default_tool_choice")]
pub tool_choice: String,
#[serde(default)]
pub tools: Vec<ResponseTool>,
pub top_p: Option<f32>,
pub truncation: Option<String>,
pub usage: Option<ResponsesUsage>,
pub user: Option<String>,
pub safety_identifier: Option<String>,
#[serde(default)]
pub metadata: HashMap<String, Value>,
}
fn default_object_type() -> String {
"response".to_string()
}
fn default_tool_choice() -> String {
"auto".to_string()
}
impl ResponsesResponse {
pub fn builder(id: impl Into<String>, model: impl Into<String>) -> ResponsesResponseBuilder {
ResponsesResponseBuilder::new(id, model)
}
pub fn is_complete(&self) -> bool {
matches!(self.status, ResponseStatus::Completed)
}
pub fn is_in_progress(&self) -> bool {
matches!(self.status, ResponseStatus::InProgress)
}
pub fn is_failed(&self) -> bool {
matches!(self.status, ResponseStatus::Failed)
}
}
impl ResponseOutputItem {
pub fn new_message(
id: String,
role: String,
content: Vec<ResponseContentPart>,
status: String,
) -> Self {
Self::Message {
id,
role,
content,
status,
}
}
pub fn new_reasoning(
id: String,
summary: Vec<String>,
content: Vec<ResponseReasoningContent>,
status: Option<String>,
) -> Self {
Self::Reasoning {
id,
summary,
content,
status,
}
}
pub fn new_function_tool_call(
id: String,
call_id: String,
name: String,
arguments: String,
output: Option<String>,
status: String,
) -> Self {
Self::FunctionToolCall {
id,
call_id,
name,
arguments,
output,
status,
}
}
}
impl ResponseContentPart {
pub fn new_text(
text: String,
annotations: Vec<String>,
logprobs: Option<ChatLogProbs>,
) -> Self {
Self::OutputText {
text,
annotations,
logprobs,
}
}
}
impl ResponseReasoningContent {
pub fn new_reasoning_text(text: String) -> Self {
Self::ReasoningText { text }
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn test_responses_request_omitted_top_p_deserializes_to_none() {
let request: ResponsesRequest = serde_json::from_value(json!({
"model": "gpt-5.4",
"input": "hello"
}))
.expect("request should deserialize");
assert_eq!(request.top_p, None);
let serialized = serde_json::to_value(&request).expect("request should serialize");
assert!(serialized.get("top_p").is_none());
}
#[test]
fn test_responses_request_null_top_p_deserializes_to_none() {
let request: ResponsesRequest = serde_json::from_value(json!({
"model": "gpt-5.4",
"input": "hello",
"top_p": null
}))
.expect("request should deserialize");
assert_eq!(request.top_p, None);
let serialized = serde_json::to_value(&request).expect("request should serialize");
assert!(serialized.get("top_p").is_none());
}
#[test]
fn test_responses_request_explicit_top_p_preserved() {
let request: ResponsesRequest = serde_json::from_value(json!({
"model": "gpt-5.4",
"input": "hello",
"top_p": 0.9
}))
.expect("request should deserialize");
assert_eq!(request.top_p, Some(0.9));
}
}