use crate::error::LingerError;
use crate::stream::{SseEvent, SseStream};
use crate::transport::BodyStream;
use crate::RequestId;
use futures_core::Stream;
use serde::ser::SerializeStruct;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
use std::pin::Pin;
use std::task::{Context, Poll};
#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct CreateResponseRequest {
pub model: String,
pub input: ResponseInput,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub personality: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub previous_response_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub conversation: Option<ResponseConversation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub context_management: Option<Vec<ResponseContextManagement>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub moderation: Option<ResponseModeration>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tools: Vec<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<ResponseToolChoice>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt: Option<ResponsePrompt>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_output_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub store: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tool_calls: Option<u32>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub metadata: BTreeMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parallel_tool_calls: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_logprobs: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<ResponseReasoning>,
#[serde(skip_serializing_if = "Option::is_none")]
pub truncation: Option<ResponseTruncation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_cache_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_cache_retention: Option<ResponsePromptCacheRetention>,
#[serde(skip_serializing_if = "Option::is_none")]
pub safety_identifier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<ResponseServiceTier>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include: Option<Vec<ResponseInclude>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<ResponseTextConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream_options: Option<StreamOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
impl CreateResponseRequest {
pub fn builder() -> CreateResponseRequestBuilder {
CreateResponseRequestBuilder::default()
}
pub(crate) fn into_streaming(mut self) -> Self {
self.stream = Some(true);
self
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CreateResponseRequestBuilder {
model: Option<String>,
input: Option<ResponseInput>,
instructions: Option<String>,
personality: Option<String>,
previous_response_id: Option<String>,
conversation: Option<ResponseConversation>,
context_management: Option<Vec<ResponseContextManagement>>,
moderation: Option<ResponseModeration>,
tools: Vec<Value>,
tool_choice: Option<ResponseToolChoice>,
prompt: Option<ResponsePrompt>,
max_output_tokens: Option<u32>,
store: Option<bool>,
background: Option<bool>,
max_tool_calls: Option<u32>,
metadata: BTreeMap<String, String>,
parallel_tool_calls: Option<bool>,
temperature: Option<f64>,
top_p: Option<f64>,
top_logprobs: Option<u8>,
reasoning: Option<ResponseReasoning>,
truncation: Option<ResponseTruncation>,
prompt_cache_key: Option<String>,
prompt_cache_retention: Option<ResponsePromptCacheRetention>,
safety_identifier: Option<String>,
service_tier: Option<ResponseServiceTier>,
include: Option<Vec<ResponseInclude>>,
text: Option<ResponseTextConfig>,
stream_options: Option<StreamOptions>,
extra: BTreeMap<String, Value>,
}
impl CreateResponseRequestBuilder {
pub fn model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
pub fn input(mut self, input: impl Into<ResponseInput>) -> Self {
self.input = Some(input.into());
self
}
pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
self.instructions = Some(instructions.into());
self
}
pub fn personality(mut self, personality: impl Into<String>) -> Self {
self.personality = Some(personality.into());
self
}
pub fn previous_response_id(mut self, previous_response_id: impl Into<String>) -> Self {
self.previous_response_id = Some(previous_response_id.into());
self
}
pub fn conversation(mut self, conversation: impl Into<ResponseConversation>) -> Self {
self.conversation = Some(conversation.into());
self
}
pub fn context_management<I>(mut self, context_management: I) -> Self
where
I: IntoIterator<Item = ResponseContextManagement>,
{
self.context_management = Some(context_management.into_iter().collect());
self
}
pub fn moderation(mut self, moderation: ResponseModeration) -> Self {
self.moderation = Some(moderation);
self
}
pub fn tools<T>(mut self, tools: impl IntoIterator<Item = T>) -> Self
where
T: Into<ResponseTool>,
{
self.tools = tools
.into_iter()
.map(|tool| tool.into().into_value())
.collect();
self
}
pub fn tool(mut self, tool: impl Into<ResponseTool>) -> Self {
self.tools.push(tool.into().into_value());
self
}
pub fn tool_choice(mut self, tool_choice: ResponseToolChoice) -> Self {
self.tool_choice = Some(tool_choice);
self
}
pub fn prompt(mut self, prompt: ResponsePrompt) -> Self {
self.prompt = Some(prompt);
self
}
pub fn max_output_tokens(mut self, max_output_tokens: u32) -> Self {
self.max_output_tokens = Some(max_output_tokens);
self
}
pub fn store(mut self, store: bool) -> Self {
self.store = Some(store);
self
}
pub fn background(mut self, background: bool) -> Self {
self.background = Some(background);
self
}
pub fn max_tool_calls(mut self, max_tool_calls: u32) -> Self {
self.max_tool_calls = Some(max_tool_calls);
self
}
pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn parallel_tool_calls(mut self, parallel_tool_calls: bool) -> Self {
self.parallel_tool_calls = Some(parallel_tool_calls);
self
}
pub fn temperature(mut self, temperature: f64) -> Self {
self.temperature = Some(temperature);
self
}
pub fn top_p(mut self, top_p: f64) -> Self {
self.top_p = Some(top_p);
self
}
pub fn top_logprobs(mut self, top_logprobs: u8) -> Self {
self.top_logprobs = Some(top_logprobs);
self
}
pub fn reasoning(mut self, reasoning: ResponseReasoning) -> Self {
self.reasoning = Some(reasoning);
self
}
pub fn truncation(mut self, truncation: ResponseTruncation) -> Self {
self.truncation = Some(truncation);
self
}
pub fn prompt_cache_key(mut self, prompt_cache_key: impl Into<String>) -> Self {
self.prompt_cache_key = Some(prompt_cache_key.into());
self
}
pub fn prompt_cache_retention(
mut self,
prompt_cache_retention: ResponsePromptCacheRetention,
) -> Self {
self.prompt_cache_retention = Some(prompt_cache_retention);
self
}
pub fn safety_identifier(mut self, safety_identifier: impl Into<String>) -> Self {
self.safety_identifier = Some(safety_identifier.into());
self
}
pub fn service_tier(mut self, service_tier: ResponseServiceTier) -> Self {
self.service_tier = Some(service_tier);
self
}
pub fn include<I>(mut self, include: I) -> Self
where
I: IntoIterator<Item = ResponseInclude>,
{
self.include = Some(include.into_iter().collect());
self
}
pub fn text(mut self, text: ResponseTextConfig) -> Self {
self.text = Some(text);
self
}
pub fn stream_options(mut self, stream_options: StreamOptions) -> Self {
self.stream_options = Some(stream_options);
self
}
pub fn extra(mut self, name: impl Into<String>, value: Value) -> Self {
self.extra.insert(name.into(), value);
self
}
pub fn build(self) -> Result<CreateResponseRequest, LingerError> {
let model = self
.model
.filter(|value| !value.trim().is_empty())
.ok_or_else(|| LingerError::invalid_config("model is required"))?;
let input = self
.input
.ok_or_else(|| LingerError::invalid_config("input is required"))?;
if self
.max_output_tokens
.is_some_and(|max_output_tokens| max_output_tokens < 16)
{
return Err(LingerError::invalid_config(
"max_output_tokens must be at least 16",
));
}
if self
.temperature
.is_some_and(|temperature| !(0.0..=2.0).contains(&temperature))
{
return Err(LingerError::invalid_config(
"temperature must be between 0.0 and 2.0",
));
}
if self
.top_p
.is_some_and(|top_p| !(0.0..=1.0).contains(&top_p))
{
return Err(LingerError::invalid_config(
"top_p must be between 0.0 and 1.0",
));
}
if self
.top_logprobs
.is_some_and(|top_logprobs| top_logprobs > 20)
{
return Err(LingerError::invalid_config(
"top_logprobs must be between 0 and 20",
));
}
validate_optional_string("prompt_cache_key", self.prompt_cache_key.as_deref())?;
validate_optional_string("safety_identifier", self.safety_identifier.as_deref())?;
validate_optional_string("personality", self.personality.as_deref())?;
if let Some(conversation) = &self.conversation {
validate_optional_string("conversation", Some(conversation.id()))?;
}
if self.previous_response_id.is_some() && self.conversation.is_some() {
return Err(LingerError::invalid_config(
"previous_response_id cannot be used with conversation",
));
}
if let Some(context_management) = &self.context_management {
validate_context_management(context_management)?;
}
if let Some(moderation) = &self.moderation {
validate_moderation(moderation)?;
}
validate_tools(&self.tools)?;
if let Some(tool_choice) = &self.tool_choice {
validate_tool_choice(tool_choice)?;
}
if let Some(prompt) = &self.prompt {
validate_prompt(prompt)?;
}
if let Some(text) = &self.text {
validate_text_config(text)?;
}
if self
.personality
.as_deref()
.is_some_and(|value| value.chars().count() > 64)
{
return Err(LingerError::invalid_config(
"personality must be at most 64 characters",
));
}
validate_metadata(&self.metadata)?;
if self
.safety_identifier
.as_deref()
.is_some_and(|value| value.chars().count() > 64)
{
return Err(LingerError::invalid_config(
"safety_identifier must be at most 64 characters",
));
}
validate_extra_fields(&self.extra)?;
Ok(CreateResponseRequest {
model,
input,
instructions: self.instructions,
personality: self.personality,
previous_response_id: self.previous_response_id,
conversation: self.conversation,
context_management: self.context_management,
moderation: self.moderation,
tools: self.tools,
tool_choice: self.tool_choice,
prompt: self.prompt,
max_output_tokens: self.max_output_tokens,
store: self.store,
background: self.background,
max_tool_calls: self.max_tool_calls,
metadata: self.metadata,
parallel_tool_calls: self.parallel_tool_calls,
temperature: self.temperature,
top_p: self.top_p,
top_logprobs: self.top_logprobs,
reasoning: self.reasoning,
truncation: self.truncation,
prompt_cache_key: self.prompt_cache_key,
prompt_cache_retention: self.prompt_cache_retention,
safety_identifier: self.safety_identifier,
service_tier: self.service_tier,
include: self.include,
text: self.text,
stream_options: self.stream_options,
stream: None,
extra: self.extra,
})
}
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub enum ResponseInclude {
#[serde(rename = "file_search_call.results")]
FileSearchCallResults,
#[serde(rename = "web_search_call.results")]
WebSearchCallResults,
#[serde(rename = "web_search_call.action.sources")]
WebSearchCallActionSources,
#[serde(rename = "message.input_image.image_url")]
MessageInputImageImageUrl,
#[serde(rename = "computer_call_output.output.image_url")]
ComputerCallOutputOutputImageUrl,
#[serde(rename = "code_interpreter_call.outputs")]
CodeInterpreterCallOutputs,
#[serde(rename = "reasoning.encrypted_content")]
ReasoningEncryptedContent,
#[serde(rename = "message.output_text.logprobs")]
MessageOutputTextLogprobs,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ResponseTruncation {
Auto,
Disabled,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(untagged)]
#[non_exhaustive]
pub enum ResponseConversation {
Id(String),
Object {
id: String,
},
}
impl ResponseConversation {
pub fn object(id: impl Into<String>) -> Self {
Self::Object { id: id.into() }
}
fn id(&self) -> &str {
match self {
Self::Id(id) => id,
Self::Object { id } => id,
}
}
}
impl From<String> for ResponseConversation {
fn from(value: String) -> Self {
Self::Id(value)
}
}
impl From<&str> for ResponseConversation {
fn from(value: &str) -> Self {
Self::Id(value.to_string())
}
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ResponsePrompt {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub variables: BTreeMap<String, Value>,
}
impl ResponsePrompt {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
version: None,
variables: BTreeMap::new(),
}
}
pub fn version(mut self, version: impl Into<String>) -> Self {
self.version = Some(version.into());
self
}
pub fn variable(mut self, name: impl Into<String>, value: Value) -> Self {
self.variables.insert(name.into(), value);
self
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ResponseContextManagement {
#[serde(rename = "type")]
pub kind: ResponseContextManagementType,
#[serde(skip_serializing_if = "Option::is_none")]
pub compact_threshold: Option<u32>,
}
impl ResponseContextManagement {
pub fn compaction() -> Self {
Self {
kind: ResponseContextManagementType::Compaction,
compact_threshold: None,
}
}
pub fn compact_threshold(mut self, compact_threshold: u32) -> Self {
self.compact_threshold = Some(compact_threshold);
self
}
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ResponseContextManagementType {
Compaction,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ResponseModeration {
pub model: String,
}
impl ResponseModeration {
pub fn new(model: impl Into<String>) -> Self {
Self {
model: model.into(),
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct ResponseTool {
value: Value,
}
impl ResponseTool {
pub fn raw(value: Value) -> Self {
Self { value }
}
pub fn into_value(self) -> Value {
self.value
}
}
impl From<Value> for ResponseTool {
fn from(value: Value) -> Self {
Self::raw(value)
}
}
impl From<ResponseFunctionTool> for ResponseTool {
fn from(tool: ResponseFunctionTool) -> Self {
Self {
value: serde_json::to_value(tool)
.expect("serializing a response function tool should not fail"),
}
}
}
impl From<ResponseFileSearchTool> for ResponseTool {
fn from(tool: ResponseFileSearchTool) -> Self {
Self {
value: serde_json::to_value(tool)
.expect("serializing a response file search tool should not fail"),
}
}
}
impl From<ResponseComputerTool> for ResponseTool {
fn from(tool: ResponseComputerTool) -> Self {
Self {
value: serde_json::to_value(tool)
.expect("serializing a response computer tool should not fail"),
}
}
}
impl From<ResponseLocalShellTool> for ResponseTool {
fn from(tool: ResponseLocalShellTool) -> Self {
Self {
value: serde_json::to_value(tool)
.expect("serializing a response local shell tool should not fail"),
}
}
}
impl From<ResponseApplyPatchTool> for ResponseTool {
fn from(tool: ResponseApplyPatchTool) -> Self {
Self {
value: serde_json::to_value(tool)
.expect("serializing a response apply patch tool should not fail"),
}
}
}
impl From<ResponseCustomTool> for ResponseTool {
fn from(tool: ResponseCustomTool) -> Self {
Self {
value: serde_json::to_value(tool)
.expect("serializing a response custom tool should not fail"),
}
}
}
impl From<ResponseNamespaceTool> for ResponseTool {
fn from(tool: ResponseNamespaceTool) -> Self {
Self {
value: serde_json::to_value(tool)
.expect("serializing a response namespace tool should not fail"),
}
}
}
impl From<ResponseToolSearchTool> for ResponseTool {
fn from(tool: ResponseToolSearchTool) -> Self {
Self {
value: serde_json::to_value(tool)
.expect("serializing a response tool search tool should not fail"),
}
}
}
impl From<ResponseShellTool> for ResponseTool {
fn from(tool: ResponseShellTool) -> Self {
Self {
value: serde_json::to_value(tool)
.expect("serializing a response shell tool should not fail"),
}
}
}
impl From<ResponseCodeInterpreterTool> for ResponseTool {
fn from(tool: ResponseCodeInterpreterTool) -> Self {
Self {
value: serde_json::to_value(tool)
.expect("serializing a response code interpreter tool should not fail"),
}
}
}
impl From<ResponseImageGenerationTool> for ResponseTool {
fn from(tool: ResponseImageGenerationTool) -> Self {
Self {
value: serde_json::to_value(tool)
.expect("serializing a response image generation tool should not fail"),
}
}
}
impl From<ResponseMcpTool> for ResponseTool {
fn from(tool: ResponseMcpTool) -> Self {
Self {
value: serde_json::to_value(tool)
.expect("serializing a response MCP tool should not fail"),
}
}
}
impl From<ResponseWebSearchTool> for ResponseTool {
fn from(tool: ResponseWebSearchTool) -> Self {
Self {
value: serde_json::to_value(tool)
.expect("serializing a response web search tool should not fail"),
}
}
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ResponseComputerTool {
#[serde(rename = "type")]
kind: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub environment: Option<ResponseComputerEnvironment>,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_width: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_height: Option<u32>,
}
impl ResponseComputerTool {
pub fn computer() -> Self {
Self {
kind: "computer",
environment: None,
display_width: None,
display_height: None,
}
}
pub fn computer_use_preview(
environment: ResponseComputerEnvironment,
display_width: u32,
display_height: u32,
) -> Self {
Self {
kind: "computer_use_preview",
environment: Some(environment),
display_width: Some(display_width),
display_height: Some(display_height),
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub enum ResponseComputerEnvironment {
#[serde(rename = "windows")]
Windows,
#[serde(rename = "mac")]
Mac,
#[serde(rename = "linux")]
Linux,
#[serde(rename = "ubuntu")]
Ubuntu,
#[serde(rename = "browser")]
Browser,
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ResponseLocalShellTool {
#[serde(rename = "type")]
kind: &'static str,
}
impl ResponseLocalShellTool {
pub fn new() -> Self {
Self {
kind: "local_shell",
}
}
}
impl Default for ResponseLocalShellTool {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ResponseApplyPatchTool {
#[serde(rename = "type")]
kind: &'static str,
}
impl ResponseApplyPatchTool {
pub fn new() -> Self {
Self {
kind: "apply_patch",
}
}
}
impl Default for ResponseApplyPatchTool {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ResponseCustomTool {
#[serde(rename = "type")]
kind: &'static str,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<ResponseCustomToolFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub defer_loading: Option<bool>,
}
impl ResponseCustomTool {
pub fn new(name: impl Into<String>) -> Self {
Self {
kind: "custom",
name: name.into(),
description: None,
format: None,
defer_loading: None,
}
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn format(mut self, format: ResponseCustomToolFormat) -> Self {
self.format = Some(format);
self
}
pub fn defer_loading(mut self, defer_loading: bool) -> Self {
self.defer_loading = Some(defer_loading);
self
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(tag = "type")]
#[non_exhaustive]
pub enum ResponseCustomToolFormat {
#[serde(rename = "text")]
Text,
#[serde(rename = "grammar")]
Grammar {
syntax: ResponseCustomToolGrammarSyntax,
definition: String,
},
}
impl ResponseCustomToolFormat {
pub fn text() -> Self {
Self::Text
}
pub fn grammar(syntax: ResponseCustomToolGrammarSyntax, definition: impl Into<String>) -> Self {
Self::Grammar {
syntax,
definition: definition.into(),
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub enum ResponseCustomToolGrammarSyntax {
#[serde(rename = "lark")]
Lark,
#[serde(rename = "regex")]
Regex,
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ResponseNamespaceTool {
#[serde(rename = "type")]
kind: &'static str,
pub name: String,
pub description: String,
pub tools: Vec<Value>,
}
impl ResponseNamespaceTool {
pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
kind: "namespace",
name: name.into(),
description: description.into(),
tools: Vec::new(),
}
}
pub fn tool<T>(mut self, tool: T) -> Self
where
T: Into<ResponseTool>,
{
self.tools.push(tool.into().into_value());
self
}
pub fn tools<I, T>(mut self, tools: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<ResponseTool>,
{
self.tools = tools
.into_iter()
.map(|tool| tool.into().into_value())
.collect();
self
}
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ResponseToolSearchTool {
#[serde(rename = "type")]
kind: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub execution: Option<ResponseToolSearchExecution>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<Value>,
}
impl ResponseToolSearchTool {
pub fn new() -> Self {
Self {
kind: "tool_search",
execution: None,
description: None,
parameters: None,
}
}
pub fn server() -> Self {
Self::new().execution(ResponseToolSearchExecution::Server)
}
pub fn client() -> Self {
Self::new().execution(ResponseToolSearchExecution::Client)
}
pub fn execution(mut self, execution: ResponseToolSearchExecution) -> Self {
self.execution = Some(execution);
self
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn parameters(mut self, parameters: Value) -> Self {
self.parameters = Some(parameters);
self
}
}
impl Default for ResponseToolSearchTool {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub enum ResponseToolSearchExecution {
#[serde(rename = "server")]
Server,
#[serde(rename = "client")]
Client,
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ResponseShellTool {
#[serde(rename = "type")]
kind: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub environment: Option<Value>,
}
impl ResponseShellTool {
pub fn new() -> Self {
Self {
kind: "shell",
environment: None,
}
}
pub fn local() -> Self {
Self::new().environment(shell_environment_type("local"))
}
pub fn container_auto() -> Self {
Self::new().environment(shell_environment_type("container_auto"))
}
pub fn container_reference(container_id: impl Into<String>) -> Self {
let mut environment = serde_json::Map::new();
environment.insert(
"type".to_string(),
Value::String("container_reference".to_string()),
);
environment.insert(
"container_id".to_string(),
Value::String(container_id.into()),
);
Self::new().environment(Value::Object(environment))
}
pub fn environment(mut self, environment: Value) -> Self {
self.environment = Some(environment);
self
}
}
impl Default for ResponseShellTool {
fn default() -> Self {
Self::new()
}
}
fn shell_environment_type(kind: &'static str) -> Value {
let mut environment = serde_json::Map::new();
environment.insert("type".to_string(), Value::String(kind.to_string()));
Value::Object(environment)
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ResponseFunctionTool {
#[serde(rename = "type")]
kind: &'static str,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub parameters: Value,
pub strict: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub defer_loading: Option<bool>,
}
impl ResponseFunctionTool {
pub fn new(name: impl Into<String>, parameters: Value) -> Self {
Self {
kind: "function",
name: name.into(),
description: None,
parameters,
strict: true,
defer_loading: None,
}
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn strict(mut self, strict: bool) -> Self {
self.strict = strict;
self
}
pub fn defer_loading(mut self, defer_loading: bool) -> Self {
self.defer_loading = Some(defer_loading);
self
}
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ResponseFileSearchTool {
#[serde(rename = "type")]
kind: &'static str,
pub vector_store_ids: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_num_results: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ranking_options: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub filters: Option<Value>,
}
impl ResponseFileSearchTool {
pub fn new<I, S>(vector_store_ids: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self {
kind: "file_search",
vector_store_ids: vector_store_ids.into_iter().map(Into::into).collect(),
max_num_results: None,
ranking_options: None,
filters: None,
}
}
pub fn max_num_results(mut self, max_num_results: u8) -> Self {
self.max_num_results = Some(max_num_results);
self
}
pub fn ranking_options(mut self, ranking_options: Value) -> Self {
self.ranking_options = Some(ranking_options);
self
}
pub fn filters(mut self, filters: Value) -> Self {
self.filters = Some(filters);
self
}
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ResponseCodeInterpreterTool {
#[serde(rename = "type")]
kind: &'static str,
pub container: ResponseCodeInterpreterContainer,
}
impl ResponseCodeInterpreterTool {
pub fn container_id(container_id: impl Into<String>) -> Self {
Self {
kind: "code_interpreter",
container: ResponseCodeInterpreterContainer::Id(container_id.into()),
}
}
pub fn auto() -> Self {
Self::auto_container(ResponseCodeInterpreterAutoContainer::new())
}
pub fn auto_container(container: ResponseCodeInterpreterAutoContainer) -> Self {
Self {
kind: "code_interpreter",
container: ResponseCodeInterpreterContainer::Auto(container),
}
}
pub fn file_ids<I, S>(mut self, file_ids: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
if let ResponseCodeInterpreterContainer::Auto(container) = &mut self.container {
container.file_ids = file_ids.into_iter().map(Into::into).collect();
}
self
}
pub fn memory_limit(mut self, memory_limit: ResponseCodeInterpreterMemoryLimit) -> Self {
if let ResponseCodeInterpreterContainer::Auto(container) = &mut self.container {
container.memory_limit = Some(memory_limit);
}
self
}
pub fn network_policy(mut self, network_policy: Value) -> Self {
if let ResponseCodeInterpreterContainer::Auto(container) = &mut self.container {
container.network_policy = Some(network_policy);
}
self
}
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[serde(untagged)]
#[non_exhaustive]
pub enum ResponseCodeInterpreterContainer {
Id(String),
Auto(ResponseCodeInterpreterAutoContainer),
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ResponseCodeInterpreterAutoContainer {
#[serde(rename = "type")]
kind: &'static str,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub file_ids: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub memory_limit: Option<ResponseCodeInterpreterMemoryLimit>,
#[serde(skip_serializing_if = "Option::is_none")]
pub network_policy: Option<Value>,
}
impl ResponseCodeInterpreterAutoContainer {
pub fn new() -> Self {
Self {
kind: "auto",
file_ids: Vec::new(),
memory_limit: None,
network_policy: None,
}
}
pub fn file_ids<I, S>(mut self, file_ids: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.file_ids = file_ids.into_iter().map(Into::into).collect();
self
}
pub fn memory_limit(mut self, memory_limit: ResponseCodeInterpreterMemoryLimit) -> Self {
self.memory_limit = Some(memory_limit);
self
}
pub fn network_policy(mut self, network_policy: Value) -> Self {
self.network_policy = Some(network_policy);
self
}
}
impl Default for ResponseCodeInterpreterAutoContainer {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub enum ResponseCodeInterpreterMemoryLimit {
#[serde(rename = "1g")]
OneGigabyte,
#[serde(rename = "4g")]
FourGigabytes,
#[serde(rename = "16g")]
SixteenGigabytes,
#[serde(rename = "64g")]
SixtyFourGigabytes,
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ResponseImageGenerationTool {
#[serde(rename = "type")]
kind: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quality: Option<ResponseImageGenerationQuality>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_format: Option<ResponseImageGenerationOutputFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_compression: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub moderation: Option<ResponseImageGenerationModeration>,
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<ResponseImageGenerationBackground>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_fidelity: Option<ResponseImageGenerationInputFidelity>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_image_mask: Option<ResponseImageGenerationMask>,
#[serde(skip_serializing_if = "Option::is_none")]
pub partial_images: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub action: Option<ResponseImageGenerationAction>,
}
impl ResponseImageGenerationTool {
pub fn new() -> Self {
Self {
kind: "image_generation",
model: None,
quality: None,
size: None,
output_format: None,
output_compression: None,
moderation: None,
background: None,
input_fidelity: None,
input_image_mask: None,
partial_images: None,
action: None,
}
}
pub fn model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
pub fn quality(mut self, quality: ResponseImageGenerationQuality) -> Self {
self.quality = Some(quality);
self
}
pub fn size(mut self, size: impl Into<String>) -> Self {
self.size = Some(size.into());
self
}
pub fn output_format(mut self, output_format: ResponseImageGenerationOutputFormat) -> Self {
self.output_format = Some(output_format);
self
}
pub fn output_compression(mut self, output_compression: u8) -> Self {
self.output_compression = Some(output_compression);
self
}
pub fn moderation(mut self, moderation: ResponseImageGenerationModeration) -> Self {
self.moderation = Some(moderation);
self
}
pub fn background(mut self, background: ResponseImageGenerationBackground) -> Self {
self.background = Some(background);
self
}
pub fn input_fidelity(mut self, input_fidelity: ResponseImageGenerationInputFidelity) -> Self {
self.input_fidelity = Some(input_fidelity);
self
}
pub fn input_image_mask(mut self, input_image_mask: ResponseImageGenerationMask) -> Self {
self.input_image_mask = Some(input_image_mask);
self
}
pub fn partial_images(mut self, partial_images: u8) -> Self {
self.partial_images = Some(partial_images);
self
}
pub fn action(mut self, action: ResponseImageGenerationAction) -> Self {
self.action = Some(action);
self
}
}
impl Default for ResponseImageGenerationTool {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug, Default, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ResponseImageGenerationMask {
#[serde(skip_serializing_if = "Option::is_none")]
pub image_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_id: Option<String>,
}
impl ResponseImageGenerationMask {
pub fn image_url(image_url: impl Into<String>) -> Self {
Self {
image_url: Some(image_url.into()),
file_id: None,
}
}
pub fn file_id(file_id: impl Into<String>) -> Self {
Self {
image_url: None,
file_id: Some(file_id.into()),
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ResponseImageGenerationQuality {
Low,
Medium,
High,
Auto,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ResponseImageGenerationOutputFormat {
Png,
Webp,
Jpeg,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ResponseImageGenerationModeration {
Auto,
Low,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ResponseImageGenerationBackground {
Transparent,
Opaque,
Auto,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ResponseImageGenerationInputFidelity {
High,
Low,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ResponseImageGenerationAction {
Generate,
Edit,
Auto,
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ResponseMcpTool {
#[serde(rename = "type")]
kind: &'static str,
pub server_label: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub server_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub connector_id: Option<ResponseMcpConnector>,
#[serde(skip_serializing_if = "Option::is_none")]
pub authorization: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub server_description: Option<String>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub headers: BTreeMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_tools: Option<ResponseMcpAllowedTools>,
#[serde(skip_serializing_if = "Option::is_none")]
pub require_approval: Option<ResponseMcpRequireApproval>,
#[serde(skip_serializing_if = "Option::is_none")]
pub defer_loading: Option<bool>,
}
impl ResponseMcpTool {
pub fn server_url(server_label: impl Into<String>, server_url: impl Into<String>) -> Self {
Self {
kind: "mcp",
server_label: server_label.into(),
server_url: Some(server_url.into()),
connector_id: None,
authorization: None,
server_description: None,
headers: BTreeMap::new(),
allowed_tools: None,
require_approval: None,
defer_loading: None,
}
}
pub fn connector(server_label: impl Into<String>, connector: ResponseMcpConnector) -> Self {
Self {
kind: "mcp",
server_label: server_label.into(),
server_url: None,
connector_id: Some(connector),
authorization: None,
server_description: None,
headers: BTreeMap::new(),
allowed_tools: None,
require_approval: None,
defer_loading: None,
}
}
pub fn authorization(mut self, authorization: impl Into<String>) -> Self {
self.authorization = Some(authorization.into());
self
}
pub fn server_description(mut self, server_description: impl Into<String>) -> Self {
self.server_description = Some(server_description.into());
self
}
pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.insert(name.into(), value.into());
self
}
pub fn allowed_tools<I, S>(mut self, tool_names: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.allowed_tools = Some(ResponseMcpAllowedTools::Names(
tool_names.into_iter().map(Into::into).collect(),
));
self
}
pub fn allowed_tool_filter(mut self, filter: ResponseMcpToolFilter) -> Self {
self.allowed_tools = Some(ResponseMcpAllowedTools::Filter(filter));
self
}
pub fn require_approval(mut self, require_approval: ResponseMcpRequireApproval) -> Self {
self.require_approval = Some(require_approval);
self
}
pub fn defer_loading(mut self, defer_loading: bool) -> Self {
self.defer_loading = Some(defer_loading);
self
}
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub enum ResponseMcpConnector {
#[serde(rename = "connector_dropbox")]
Dropbox,
#[serde(rename = "connector_gmail")]
Gmail,
#[serde(rename = "connector_googlecalendar")]
GoogleCalendar,
#[serde(rename = "connector_googledrive")]
GoogleDrive,
#[serde(rename = "connector_microsoftteams")]
MicrosoftTeams,
#[serde(rename = "connector_outlookcalendar")]
OutlookCalendar,
#[serde(rename = "connector_outlookemail")]
OutlookEmail,
#[serde(rename = "connector_sharepoint")]
SharePoint,
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
#[serde(untagged)]
#[non_exhaustive]
pub enum ResponseMcpAllowedTools {
Names(Vec<String>),
Filter(ResponseMcpToolFilter),
}
#[derive(Clone, Debug, Default, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ResponseMcpToolFilter {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tool_names: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub read_only: Option<bool>,
}
impl ResponseMcpToolFilter {
pub fn new() -> Self {
Self::default()
}
pub fn tool_names<I, S>(mut self, tool_names: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.tool_names = tool_names.into_iter().map(Into::into).collect();
self
}
pub fn read_only(mut self, read_only: bool) -> Self {
self.read_only = Some(read_only);
self
}
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
#[serde(untagged)]
#[non_exhaustive]
pub enum ResponseMcpRequireApproval {
Mode(ResponseMcpApprovalMode),
Filter(ResponseMcpApprovalFilter),
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ResponseMcpApprovalMode {
Always,
Never,
}
#[derive(Clone, Debug, Default, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ResponseMcpApprovalFilter {
#[serde(skip_serializing_if = "Option::is_none")]
pub always: Option<ResponseMcpToolFilter>,
#[serde(skip_serializing_if = "Option::is_none")]
pub never: Option<ResponseMcpToolFilter>,
}
impl ResponseMcpApprovalFilter {
pub fn new() -> Self {
Self::default()
}
pub fn always(mut self, filter: ResponseMcpToolFilter) -> Self {
self.always = Some(filter);
self
}
pub fn never(mut self, filter: ResponseMcpToolFilter) -> Self {
self.never = Some(filter);
self
}
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ResponseWebSearchTool {
#[serde(rename = "type")]
kind: ResponseWebSearchToolType,
#[serde(skip_serializing_if = "Option::is_none")]
pub filters: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_location: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub search_context_size: Option<ResponseWebSearchContextSize>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub search_content_types: Vec<String>,
}
impl ResponseWebSearchTool {
pub fn web_search() -> Self {
Self::new(ResponseWebSearchToolType::WebSearch)
}
pub fn web_search_2025_08_26() -> Self {
Self::new(ResponseWebSearchToolType::WebSearch20250826)
}
pub fn web_search_preview() -> Self {
Self::new(ResponseWebSearchToolType::WebSearchPreview)
}
pub fn web_search_preview_2025_03_11() -> Self {
Self::new(ResponseWebSearchToolType::WebSearchPreview20250311)
}
fn new(kind: ResponseWebSearchToolType) -> Self {
Self {
kind,
filters: None,
user_location: None,
search_context_size: None,
search_content_types: Vec::new(),
}
}
pub fn filters(mut self, filters: Value) -> Self {
self.filters = Some(filters);
self
}
pub fn user_location(mut self, user_location: Value) -> Self {
self.user_location = Some(user_location);
self
}
pub fn search_context_size(
mut self,
search_context_size: ResponseWebSearchContextSize,
) -> Self {
self.search_context_size = Some(search_context_size);
self
}
pub fn search_content_types<I, S>(mut self, search_content_types: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.search_content_types = search_content_types.into_iter().map(Into::into).collect();
self
}
}
#[derive(Clone, Copy, Debug, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub enum ResponseWebSearchToolType {
#[serde(rename = "web_search")]
WebSearch,
#[serde(rename = "web_search_2025_08_26")]
WebSearch20250826,
#[serde(rename = "web_search_preview")]
WebSearchPreview,
#[serde(rename = "web_search_preview_2025_03_11")]
WebSearchPreview20250311,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ResponseWebSearchContextSize {
Low,
Medium,
High,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ResponseToolChoice {
None,
Auto,
Required,
HostedTool {
tool_type: ResponseHostedToolChoice,
},
Function {
name: String,
},
Custom {
name: String,
},
Mcp {
server_label: String,
name: Option<String>,
},
ApplyPatch,
Shell,
AllowedTools {
mode: ResponseAllowedToolsMode,
tools: Vec<Value>,
},
}
impl ResponseToolChoice {
pub fn hosted_tool(tool_type: ResponseHostedToolChoice) -> Self {
Self::HostedTool { tool_type }
}
pub fn function(name: impl Into<String>) -> Self {
Self::Function { name: name.into() }
}
pub fn custom(name: impl Into<String>) -> Self {
Self::Custom { name: name.into() }
}
pub fn mcp(server_label: impl Into<String>) -> Self {
Self::Mcp {
server_label: server_label.into(),
name: None,
}
}
pub fn mcp_tool(server_label: impl Into<String>, name: impl Into<String>) -> Self {
Self::Mcp {
server_label: server_label.into(),
name: Some(name.into()),
}
}
pub fn allowed_tools<I>(mode: ResponseAllowedToolsMode, tools: I) -> Self
where
I: IntoIterator<Item = Value>,
{
Self::AllowedTools {
mode,
tools: tools.into_iter().collect(),
}
}
}
impl Serialize for ResponseToolChoice {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::None => serializer.serialize_str("none"),
Self::Auto => serializer.serialize_str("auto"),
Self::Required => serializer.serialize_str("required"),
Self::HostedTool { tool_type } => {
let mut state = serializer.serialize_struct("ResponseToolChoiceHostedTool", 1)?;
state.serialize_field("type", tool_type)?;
state.end()
}
Self::Function { name } => {
let mut state = serializer.serialize_struct("ResponseToolChoiceFunction", 2)?;
state.serialize_field("type", "function")?;
state.serialize_field("name", name)?;
state.end()
}
Self::Custom { name } => {
let mut state = serializer.serialize_struct("ResponseToolChoiceCustom", 2)?;
state.serialize_field("type", "custom")?;
state.serialize_field("name", name)?;
state.end()
}
Self::Mcp { server_label, name } => {
let mut state = serializer
.serialize_struct("ResponseToolChoiceMcp", 2 + usize::from(name.is_some()))?;
state.serialize_field("type", "mcp")?;
state.serialize_field("server_label", server_label)?;
if let Some(name) = name {
state.serialize_field("name", name)?;
}
state.end()
}
Self::ApplyPatch => {
let mut state = serializer.serialize_struct("ResponseToolChoiceApplyPatch", 1)?;
state.serialize_field("type", "apply_patch")?;
state.end()
}
Self::Shell => {
let mut state = serializer.serialize_struct("ResponseToolChoiceShell", 1)?;
state.serialize_field("type", "shell")?;
state.end()
}
Self::AllowedTools { mode, tools } => {
let mut state = serializer.serialize_struct("ResponseToolChoiceAllowedTools", 3)?;
state.serialize_field("type", "allowed_tools")?;
state.serialize_field("mode", mode)?;
state.serialize_field("tools", tools)?;
state.end()
}
}
}
}
impl<'de> Deserialize<'de> for ResponseToolChoice {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct ObjectToolChoice {
#[serde(rename = "type")]
tool_type: String,
name: Option<String>,
server_label: Option<String>,
mode: Option<ResponseAllowedToolsMode>,
tools: Option<Vec<Value>>,
}
let value = Value::deserialize(deserializer)?;
if let Some(value) = value.as_str() {
return match value {
"none" => Ok(Self::None),
"auto" => Ok(Self::Auto),
"required" => Ok(Self::Required),
other => Err(serde::de::Error::unknown_variant(
other,
&["none", "auto", "required"],
)),
};
}
let object = ObjectToolChoice::deserialize(value).map_err(serde::de::Error::custom)?;
if object.tool_type == "function" {
let name = object
.name
.ok_or_else(|| serde::de::Error::missing_field("name"))?;
return Ok(Self::Function { name });
}
if object.tool_type == "custom" {
let name = object
.name
.ok_or_else(|| serde::de::Error::missing_field("name"))?;
return Ok(Self::Custom { name });
}
if object.tool_type == "mcp" {
let server_label = object
.server_label
.ok_or_else(|| serde::de::Error::missing_field("server_label"))?;
return Ok(Self::Mcp {
server_label,
name: object.name,
});
}
if object.tool_type == "apply_patch" {
return Ok(Self::ApplyPatch);
}
if object.tool_type == "shell" {
return Ok(Self::Shell);
}
if object.tool_type == "allowed_tools" {
let mode = object
.mode
.ok_or_else(|| serde::de::Error::missing_field("mode"))?;
let tools = object
.tools
.ok_or_else(|| serde::de::Error::missing_field("tools"))?;
return Ok(Self::AllowedTools { mode, tools });
}
if let Some(tool_type) = ResponseHostedToolChoice::from_wire_type(&object.tool_type) {
return Ok(Self::HostedTool { tool_type });
}
Err(serde::de::Error::unknown_variant(
&object.tool_type,
&[
"none",
"auto",
"required",
"file_search",
"web_search_preview",
"computer",
"computer_use_preview",
"computer_use",
"web_search_preview_2025_03_11",
"image_generation",
"code_interpreter",
"function",
"custom",
"mcp",
"apply_patch",
"shell",
"allowed_tools",
],
))
}
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ResponseAllowedToolsMode {
Auto,
Required,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub enum ResponseHostedToolChoice {
#[serde(rename = "file_search")]
FileSearch,
#[serde(rename = "web_search_preview")]
WebSearchPreview,
#[serde(rename = "computer")]
Computer,
#[serde(rename = "computer_use_preview")]
ComputerUsePreview,
#[serde(rename = "computer_use")]
ComputerUse,
#[serde(rename = "web_search_preview_2025_03_11")]
WebSearchPreview2025_03_11,
#[serde(rename = "image_generation")]
ImageGeneration,
#[serde(rename = "code_interpreter")]
CodeInterpreter,
}
impl ResponseHostedToolChoice {
fn from_wire_type(value: &str) -> Option<Self> {
match value {
"file_search" => Some(Self::FileSearch),
"web_search_preview" => Some(Self::WebSearchPreview),
"computer" => Some(Self::Computer),
"computer_use_preview" => Some(Self::ComputerUsePreview),
"computer_use" => Some(Self::ComputerUse),
"web_search_preview_2025_03_11" => Some(Self::WebSearchPreview2025_03_11),
"image_generation" => Some(Self::ImageGeneration),
"code_interpreter" => Some(Self::CodeInterpreter),
_ => None,
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ResponseReasoning {
#[serde(skip_serializing_if = "Option::is_none")]
pub effort: Option<ResponseReasoningEffort>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<ResponseReasoningSummary>,
}
impl ResponseReasoning {
pub fn builder() -> ResponseReasoningBuilder {
ResponseReasoningBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct ResponseReasoningBuilder {
effort: Option<ResponseReasoningEffort>,
summary: Option<ResponseReasoningSummary>,
}
impl ResponseReasoningBuilder {
pub fn effort(mut self, effort: ResponseReasoningEffort) -> Self {
self.effort = Some(effort);
self
}
pub fn summary(mut self, summary: ResponseReasoningSummary) -> Self {
self.summary = Some(summary);
self
}
pub fn build(self) -> ResponseReasoning {
ResponseReasoning {
effort: self.effort,
summary: self.summary,
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ResponseReasoningEffort {
None,
Minimal,
Low,
Medium,
High,
#[serde(rename = "xhigh")]
XHigh,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ResponseReasoningSummary {
Auto,
Concise,
Detailed,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ResponseServiceTier {
Auto,
Default,
Flex,
Priority,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub enum ResponsePromptCacheRetention {
#[serde(rename = "in_memory")]
InMemory,
#[serde(rename = "24h")]
TwentyFourHours,
}
#[derive(Clone, Debug, Default, Serialize, PartialEq)]
#[non_exhaustive]
pub struct CreateResponseInputTokensRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input: Option<ResponseInput>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub previous_response_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<ResponseTextConfig>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
impl CreateResponseInputTokensRequest {
pub fn builder() -> CreateResponseInputTokensRequestBuilder {
CreateResponseInputTokensRequestBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CreateResponseInputTokensRequestBuilder {
model: Option<String>,
input: Option<ResponseInput>,
instructions: Option<String>,
previous_response_id: Option<String>,
text: Option<ResponseTextConfig>,
extra: BTreeMap<String, Value>,
}
impl CreateResponseInputTokensRequestBuilder {
pub fn model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
pub fn input(mut self, input: impl Into<ResponseInput>) -> Self {
self.input = Some(input.into());
self
}
pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
self.instructions = Some(instructions.into());
self
}
pub fn previous_response_id(mut self, previous_response_id: impl Into<String>) -> Self {
self.previous_response_id = Some(previous_response_id.into());
self
}
pub fn text(mut self, text: ResponseTextConfig) -> Self {
self.text = Some(text);
self
}
pub fn extra(mut self, name: impl Into<String>, value: Value) -> Self {
self.extra.insert(name.into(), value);
self
}
pub fn build(self) -> Result<CreateResponseInputTokensRequest, LingerError> {
validate_optional_string("model", self.model.as_deref())?;
validate_optional_string("instructions", self.instructions.as_deref())?;
validate_optional_string("previous_response_id", self.previous_response_id.as_deref())?;
if self.input.is_none() && self.extra.is_empty() {
return Err(LingerError::invalid_config("input is required"));
}
if let Some(text) = &self.text {
validate_text_config(text)?;
}
validate_extra_fields(&self.extra)?;
Ok(CreateResponseInputTokensRequest {
model: self.model,
input: self.input,
instructions: self.instructions,
previous_response_id: self.previous_response_id,
text: self.text,
extra: self.extra,
})
}
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct CompactResponseRequest {
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub input: Option<ResponseInput>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub previous_response_id: Option<String>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
impl CompactResponseRequest {
pub fn builder() -> CompactResponseRequestBuilder {
CompactResponseRequestBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CompactResponseRequestBuilder {
model: Option<String>,
input: Option<ResponseInput>,
instructions: Option<String>,
previous_response_id: Option<String>,
extra: BTreeMap<String, Value>,
}
impl CompactResponseRequestBuilder {
pub fn model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
pub fn input(mut self, input: impl Into<ResponseInput>) -> Self {
self.input = Some(input.into());
self
}
pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
self.instructions = Some(instructions.into());
self
}
pub fn previous_response_id(mut self, previous_response_id: impl Into<String>) -> Self {
self.previous_response_id = Some(previous_response_id.into());
self
}
pub fn extra(mut self, name: impl Into<String>, value: Value) -> Self {
self.extra.insert(name.into(), value);
self
}
pub fn build(self) -> Result<CompactResponseRequest, LingerError> {
let model = self
.model
.filter(|value| !value.trim().is_empty())
.ok_or_else(|| LingerError::invalid_config("model is required"))?;
validate_optional_string("instructions", self.instructions.as_deref())?;
validate_optional_string("previous_response_id", self.previous_response_id.as_deref())?;
validate_extra_fields(&self.extra)?;
Ok(CompactResponseRequest {
model,
input: self.input,
instructions: self.instructions,
previous_response_id: self.previous_response_id,
extra: self.extra,
})
}
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[serde(untagged)]
#[non_exhaustive]
pub enum ResponseInput {
Text(String),
Messages(Vec<ResponseInputMessage>),
}
impl From<&str> for ResponseInput {
fn from(value: &str) -> Self {
Self::Text(value.to_string())
}
}
impl From<String> for ResponseInput {
fn from(value: String) -> Self {
Self::Text(value)
}
}
impl From<Vec<ResponseInputMessage>> for ResponseInput {
fn from(value: Vec<ResponseInputMessage>) -> Self {
Self::Messages(value)
}
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ResponseInputMessage {
pub role: String,
pub content: Vec<ResponseInputMessageContent>,
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[serde(tag = "type")]
#[non_exhaustive]
pub enum ResponseInputMessageContent {
#[serde(rename = "input_text")]
InputText {
text: String,
},
}
#[derive(Clone, Debug, Default, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ResponseTextConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<ResponseTextFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub verbosity: Option<ResponseTextVerbosity>,
}
impl ResponseTextConfig {
pub fn format(mut self, format: impl Into<ResponseTextFormat>) -> Self {
self.format = Some(format.into());
self
}
pub fn verbosity(mut self, verbosity: ResponseTextVerbosity) -> Self {
self.verbosity = Some(verbosity);
self
}
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[serde(tag = "type")]
#[non_exhaustive]
pub enum ResponseTextFormat {
#[serde(rename = "text")]
Text,
#[serde(rename = "json_object")]
JsonObject,
#[serde(rename = "json_schema")]
JsonSchema {
name: String,
schema: Value,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
strict: Option<bool>,
},
}
impl ResponseTextFormat {
pub fn json_schema(name: impl Into<String>, schema: Value) -> ResponseTextJsonSchemaFormat {
ResponseTextJsonSchemaFormat::new(name, schema)
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct ResponseTextJsonSchemaFormat {
pub name: String,
pub schema: Value,
pub description: Option<String>,
pub strict: Option<bool>,
}
impl ResponseTextJsonSchemaFormat {
pub fn new(name: impl Into<String>, schema: Value) -> Self {
Self {
name: name.into(),
schema,
description: None,
strict: None,
}
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn strict(mut self, strict: bool) -> Self {
self.strict = Some(strict);
self
}
}
impl From<ResponseTextJsonSchemaFormat> for ResponseTextFormat {
fn from(format: ResponseTextJsonSchemaFormat) -> Self {
Self::JsonSchema {
name: format.name,
schema: format.schema,
description: format.description,
strict: format.strict,
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ResponseTextVerbosity {
Low,
Medium,
High,
}
#[derive(Clone, Debug, Default, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct StreamOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub include_obfuscation: Option<bool>,
}
impl StreamOptions {
pub fn builder() -> StreamOptionsBuilder {
StreamOptionsBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct StreamOptionsBuilder {
include_obfuscation: Option<bool>,
}
impl StreamOptionsBuilder {
pub fn include_obfuscation(mut self, include_obfuscation: bool) -> Self {
self.include_obfuscation = Some(include_obfuscation);
self
}
pub fn build(self) -> StreamOptions {
StreamOptions {
include_obfuscation: self.include_obfuscation,
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[non_exhaustive]
pub struct Response {
pub id: String,
pub object: String,
pub model: String,
#[serde(default)]
pub output: Vec<ResponseOutput>,
#[serde(skip)]
request_id: Option<RequestId>,
}
impl Response {
pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
self.request_id = request_id;
self
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
pub fn output_text(&self) -> String {
let mut text = String::new();
for item in &self.output {
if let ResponseOutput::Message(message) = item {
for content in &message.content {
if let ResponseContent::OutputText { text: value, .. } = content {
text.push_str(value);
}
}
}
}
text
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ResponseInputTokens {
pub object: String,
pub input_tokens: u64,
#[serde(skip)]
request_id: Option<RequestId>,
}
impl ResponseInputTokens {
pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
self.request_id = request_id;
self
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ResponseCompaction {
pub id: String,
pub object: String,
pub created_at: u64,
#[serde(default)]
pub output: Vec<Value>,
pub usage: ResponseUsage,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
#[serde(skip)]
request_id: Option<RequestId>,
}
impl ResponseCompaction {
pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
self.request_id = request_id;
self
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ResponseUsage {
pub input_tokens: u64,
pub output_tokens: u64,
pub total_tokens: u64,
#[serde(default)]
pub input_tokens_details: BTreeMap<String, Value>,
#[serde(default)]
pub output_tokens_details: BTreeMap<String, Value>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ResponseDeletion {
pub id: String,
pub object: String,
pub deleted: bool,
#[serde(skip)]
request_id: Option<RequestId>,
}
impl ResponseDeletion {
pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
self.request_id = request_id;
self
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ResponseInputItemsPage {
pub object: String,
#[serde(default)]
pub data: Vec<ResponseInputItem>,
#[serde(default)]
pub first_id: Option<String>,
#[serde(default)]
pub last_id: Option<String>,
pub has_more: bool,
#[serde(skip)]
request_id: Option<RequestId>,
}
impl ResponseInputItemsPage {
pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
self.request_id = request_id;
self
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(tag = "type")]
#[non_exhaustive]
pub enum ResponseInputItem {
#[serde(rename = "message")]
Message(ResponseInputItemMessage),
#[serde(other)]
Unknown,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ResponseInputItemMessage {
pub id: String,
pub role: String,
#[serde(default)]
pub content: Vec<ResponseInputItemContent>,
}
impl ResponseInputItemMessage {
pub fn input_text(&self) -> String {
let mut text = String::new();
for content in &self.content {
if let ResponseInputItemContent::InputText { text: value, .. } = content {
text.push_str(value);
}
}
text
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(tag = "type")]
#[non_exhaustive]
pub enum ResponseInputItemContent {
#[serde(rename = "input_text")]
InputText {
text: String,
#[serde(flatten)]
extra: BTreeMap<String, Value>,
},
#[serde(other)]
Unknown,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(tag = "type")]
#[non_exhaustive]
pub enum ResponseOutput {
#[serde(rename = "message")]
Message(ResponseOutputMessage),
#[serde(other)]
Unknown,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ResponseOutputMessage {
pub id: String,
pub role: String,
#[serde(default)]
pub content: Vec<ResponseContent>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(tag = "type")]
#[non_exhaustive]
pub enum ResponseContent {
#[serde(rename = "output_text")]
OutputText {
text: String,
#[serde(flatten)]
extra: BTreeMap<String, Value>,
},
#[serde(other)]
Unknown,
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum ResponseStreamEvent {
OutputTextDelta {
delta: String,
},
Completed {
response: Response,
},
Unknown {
event_type: String,
data: Value,
},
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct ResponseStreamItem {
pub event: ResponseStreamEvent,
pub raw: SseEvent,
}
impl ResponseStreamItem {
pub fn event_type(&self) -> &str {
self.raw.event_type.as_deref().unwrap_or("")
}
pub fn output_text_delta(&self) -> Option<&str> {
match &self.event {
ResponseStreamEvent::OutputTextDelta { delta } => Some(delta),
_ => None,
}
}
}
pub struct ResponseStream {
inner: SseStream,
}
impl ResponseStream {
pub fn new(body: BodyStream) -> Self {
Self {
inner: SseStream::new(body),
}
}
}
impl Stream for ResponseStream {
type Item = Result<ResponseStreamItem, LingerError>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
match Pin::new(&mut this.inner).poll_next(cx) {
Poll::Ready(Some(Ok(raw))) => Poll::Ready(Some(parse_response_event(raw))),
Poll::Ready(Some(Err(error))) => Poll::Ready(Some(Err(error))),
Poll::Ready(None) => Poll::Ready(None),
Poll::Pending => Poll::Pending,
}
}
}
fn parse_response_event(raw: SseEvent) -> Result<ResponseStreamItem, LingerError> {
let event_type = raw.event_type.clone().unwrap_or_default();
let value: Value = serde_json::from_str(&raw.data).map_err(|error| {
LingerError::streaming(format!("invalid response stream JSON: {error}"))
})?;
let event = match event_type.as_str() {
"response.output_text.delta" => {
let delta = value
.get("delta")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
ResponseStreamEvent::OutputTextDelta { delta }
}
"response.completed" => {
let response = value.get("response").cloned().ok_or_else(|| {
LingerError::streaming("response.completed event is missing response")
})?;
let response = serde_json::from_value(response).map_err(|error| {
LingerError::streaming(format!("invalid completed response: {error}"))
})?;
ResponseStreamEvent::Completed { response }
}
_ => ResponseStreamEvent::Unknown {
event_type,
data: value,
},
};
Ok(ResponseStreamItem { event, raw })
}
fn validate_optional_string(name: &str, value: Option<&str>) -> Result<(), LingerError> {
if value.is_some_and(|value| value.trim().is_empty()) {
return Err(LingerError::invalid_config(format!(
"{name} must not be empty"
)));
}
Ok(())
}
fn validate_metadata(metadata: &BTreeMap<String, String>) -> Result<(), LingerError> {
if metadata.len() > 16 {
return Err(LingerError::invalid_config(
"metadata must contain at most 16 entries",
));
}
for (key, value) in metadata {
if key.trim().is_empty() {
return Err(LingerError::invalid_config(
"metadata keys must not be empty",
));
}
if key.chars().count() > 64 {
return Err(LingerError::invalid_config(
"metadata keys must be at most 64 characters",
));
}
if value.chars().count() > 512 {
return Err(LingerError::invalid_config(
"metadata values must be at most 512 characters",
));
}
}
Ok(())
}
fn validate_json_items(name: &str, values: &[Value]) -> Result<(), LingerError> {
if values.iter().any(Value::is_null) {
return Err(LingerError::invalid_config(format!(
"{name} must not contain null"
)));
}
Ok(())
}
fn validate_tools(tools: &[Value]) -> Result<(), LingerError> {
validate_json_items("tools", tools)?;
for tool in tools {
let Some(object) = tool.as_object() else {
continue;
};
match object.get("type").and_then(Value::as_str) {
Some("function") => validate_function_tool(object)?,
Some("custom") => validate_custom_tool(object)?,
Some("namespace") => validate_namespace_tool(object)?,
Some("tool_search") => validate_tool_search_tool(object)?,
Some("shell") => validate_shell_tool(object)?,
Some("file_search") => validate_file_search_tool(object)?,
Some("computer_use_preview") => validate_computer_use_preview_tool(object)?,
Some("code_interpreter") => validate_code_interpreter_tool(object)?,
Some("image_generation") => validate_image_generation_tool(object)?,
Some("mcp") => validate_mcp_tool(object)?,
Some(
"web_search"
| "web_search_2025_08_26"
| "web_search_preview"
| "web_search_preview_2025_03_11",
) => validate_web_search_tool(object)?,
_ => {}
}
}
Ok(())
}
fn validate_custom_tool(object: &serde_json::Map<String, Value>) -> Result<(), LingerError> {
let name = object
.get("name")
.and_then(Value::as_str)
.unwrap_or_default();
validate_optional_string("custom tool name", Some(name))?;
validate_optional_string(
"custom tool description",
object.get("description").and_then(Value::as_str),
)?;
if let Some(description) = object.get("description") {
if !description.is_string() {
return Err(LingerError::invalid_config(
"custom tool description must be a string",
));
}
}
if let Some(defer_loading) = object.get("defer_loading") {
if !defer_loading.is_boolean() {
return Err(LingerError::invalid_config(
"custom tool defer_loading must be a boolean",
));
}
}
if let Some(format) = object.get("format") {
validate_custom_tool_format(format)?;
}
Ok(())
}
fn validate_custom_tool_format(format: &Value) -> Result<(), LingerError> {
let Some(format) = format.as_object() else {
return Err(LingerError::invalid_config(
"custom tool format must be a JSON object",
));
};
match format.get("type").and_then(Value::as_str) {
Some("text") => Ok(()),
Some("grammar") => {
let syntax = format
.get("syntax")
.and_then(Value::as_str)
.ok_or_else(|| {
LingerError::invalid_config("custom tool grammar format must include syntax")
})?;
if !matches!(syntax, "lark" | "regex") {
return Err(LingerError::invalid_config(
"custom tool grammar syntax has an unsupported value",
));
}
let definition = format
.get("definition")
.and_then(Value::as_str)
.unwrap_or_default();
validate_optional_string("custom tool grammar definition", Some(definition))
}
_ => Err(LingerError::invalid_config(
"custom tool format type has an unsupported value",
)),
}
}
fn validate_tool_search_tool(object: &serde_json::Map<String, Value>) -> Result<(), LingerError> {
if let Some(execution) = object.get("execution") {
let Some(execution) = execution.as_str() else {
return Err(LingerError::invalid_config(
"tool_search execution must be a string",
));
};
if !matches!(execution, "server" | "client") {
return Err(LingerError::invalid_config(
"tool_search execution has an unsupported value",
));
}
}
if let Some(description) = object.get("description") {
if !(description.is_string() || description.is_null()) {
return Err(LingerError::invalid_config(
"tool_search description must be a string or null",
));
}
if let Some(description) = description.as_str() {
validate_optional_string("tool_search description", Some(description))?;
}
}
if let Some(parameters) = object.get("parameters") {
if !(parameters.is_object() || parameters.is_null()) {
return Err(LingerError::invalid_config(
"tool_search parameters must be a JSON object or null",
));
}
}
Ok(())
}
fn validate_shell_tool(object: &serde_json::Map<String, Value>) -> Result<(), LingerError> {
let Some(environment) = object.get("environment") else {
return Ok(());
};
if environment.is_null() {
return Ok(());
}
let Some(environment) = environment.as_object() else {
return Err(LingerError::invalid_config(
"shell environment must be a JSON object or null",
));
};
match environment.get("type").and_then(Value::as_str) {
Some("local" | "container_auto") => Ok(()),
Some("container_reference") => {
let container_id = environment
.get("container_id")
.and_then(Value::as_str)
.unwrap_or_default();
validate_optional_string("shell container_id", Some(container_id))
}
_ => Err(LingerError::invalid_config(
"shell environment type has an unsupported value",
)),
}
}
fn validate_namespace_tool(object: &serde_json::Map<String, Value>) -> Result<(), LingerError> {
let name = object
.get("name")
.and_then(Value::as_str)
.unwrap_or_default();
validate_optional_string("namespace tool name", Some(name))?;
let description = object
.get("description")
.and_then(Value::as_str)
.unwrap_or_default();
validate_optional_string("namespace tool description", Some(description))?;
let tools = object
.get("tools")
.and_then(Value::as_array)
.ok_or_else(|| LingerError::invalid_config("namespace tools must include tools"))?;
if tools.is_empty() {
return Err(LingerError::invalid_config(
"namespace tools must not be empty",
));
}
for tool in tools {
let Some(tool) = tool.as_object() else {
return Err(LingerError::invalid_config(
"namespace tools must contain objects",
));
};
match tool.get("type").and_then(Value::as_str) {
Some("function") => validate_namespace_function_tool(tool)?,
Some("custom") => validate_custom_tool(tool)?,
_ => {
return Err(LingerError::invalid_config(
"namespace tools may only contain function or custom tools",
));
}
}
}
Ok(())
}
fn validate_namespace_function_tool(
object: &serde_json::Map<String, Value>,
) -> Result<(), LingerError> {
let name = object
.get("name")
.and_then(Value::as_str)
.unwrap_or_default();
validate_optional_string("namespace function tool name", Some(name))?;
if let Some(description) = object.get("description") {
if !(description.is_string() || description.is_null()) {
return Err(LingerError::invalid_config(
"namespace function tool description must be a string or null",
));
}
}
if let Some(parameters) = object.get("parameters") {
if !(parameters.is_object() || parameters.is_null()) {
return Err(LingerError::invalid_config(
"namespace function tool parameters must be a JSON object or null",
));
}
}
if let Some(strict) = object.get("strict") {
if !(strict.is_boolean() || strict.is_null()) {
return Err(LingerError::invalid_config(
"namespace function tool strict must be a boolean or null",
));
}
}
if let Some(defer_loading) = object.get("defer_loading") {
if !defer_loading.is_boolean() {
return Err(LingerError::invalid_config(
"namespace function tool defer_loading must be a boolean",
));
}
}
Ok(())
}
fn validate_computer_use_preview_tool(
object: &serde_json::Map<String, Value>,
) -> Result<(), LingerError> {
let environment = object
.get("environment")
.and_then(Value::as_str)
.ok_or_else(|| {
LingerError::invalid_config("computer_use_preview tools must include environment")
})?;
if !matches!(
environment,
"windows" | "mac" | "linux" | "ubuntu" | "browser"
) {
return Err(LingerError::invalid_config(
"computer_use_preview environment has an unsupported value",
));
}
for field in ["display_width", "display_height"] {
let value = object.get(field).and_then(Value::as_u64).ok_or_else(|| {
LingerError::invalid_config(format!(
"computer_use_preview tools must include integer {field}"
))
})?;
if value == 0 {
return Err(LingerError::invalid_config(format!(
"computer_use_preview {field} must be greater than 0"
)));
}
}
Ok(())
}
fn validate_function_tool(object: &serde_json::Map<String, Value>) -> Result<(), LingerError> {
let name = object
.get("name")
.and_then(Value::as_str)
.unwrap_or_default();
validate_optional_string("tools[].name", Some(name))?;
let parameters = object
.get("parameters")
.ok_or_else(|| LingerError::invalid_config("function tools must include parameters"))?;
if parameters.is_null() {
return Err(LingerError::invalid_config(
"function tool parameters must not be null",
));
}
if !parameters.is_object() {
return Err(LingerError::invalid_config(
"function tool parameters must be a JSON object",
));
}
if !object.get("strict").is_some_and(Value::is_boolean) {
return Err(LingerError::invalid_config(
"function tools must include boolean strict",
));
}
Ok(())
}
fn validate_file_search_tool(object: &serde_json::Map<String, Value>) -> Result<(), LingerError> {
let vector_store_ids = object
.get("vector_store_ids")
.and_then(Value::as_array)
.ok_or_else(|| {
LingerError::invalid_config("file_search tools must include vector_store_ids")
})?;
if vector_store_ids.is_empty() {
return Err(LingerError::invalid_config(
"file_search vector_store_ids must not be empty",
));
}
for vector_store_id in vector_store_ids {
let Some(vector_store_id) = vector_store_id.as_str() else {
return Err(LingerError::invalid_config(
"file_search vector_store_ids must contain strings",
));
};
validate_optional_string("file_search vector_store_ids", Some(vector_store_id))?;
}
if let Some(max_num_results) = object.get("max_num_results") {
let max_num_results = max_num_results.as_u64().ok_or_else(|| {
LingerError::invalid_config("file_search max_num_results must be an integer")
})?;
if !(1..=50).contains(&max_num_results) {
return Err(LingerError::invalid_config(
"file_search max_num_results must be between 1 and 50",
));
}
}
for field in ["ranking_options", "filters"] {
if object.get(field).is_some_and(Value::is_null) {
return Err(LingerError::invalid_config(format!(
"file_search {field} must not be null"
)));
}
}
Ok(())
}
fn validate_code_interpreter_tool(
object: &serde_json::Map<String, Value>,
) -> Result<(), LingerError> {
let container = object.get("container").ok_or_else(|| {
LingerError::invalid_config("code_interpreter tools must include container")
})?;
if container.is_null() {
return Err(LingerError::invalid_config(
"code_interpreter container must not be null",
));
}
if let Some(container_id) = container.as_str() {
validate_optional_string("code_interpreter container", Some(container_id))?;
return Ok(());
}
let container = container.as_object().ok_or_else(|| {
LingerError::invalid_config("code_interpreter container must be a string or object")
})?;
let container_type = container
.get("type")
.and_then(Value::as_str)
.unwrap_or_default();
if container_type != "auto" {
return Err(LingerError::invalid_config(
"code_interpreter auto container type must be auto",
));
}
if let Some(file_ids) = container.get("file_ids") {
let file_ids = file_ids.as_array().ok_or_else(|| {
LingerError::invalid_config("code_interpreter file_ids must be an array")
})?;
if file_ids.len() > 50 {
return Err(LingerError::invalid_config(
"code_interpreter file_ids must contain at most 50 entries",
));
}
for file_id in file_ids {
let Some(file_id) = file_id.as_str() else {
return Err(LingerError::invalid_config(
"code_interpreter file_ids must contain strings",
));
};
validate_optional_string("code_interpreter file_ids", Some(file_id))?;
}
}
if let Some(memory_limit) = container.get("memory_limit") {
if !memory_limit.is_null() {
let Some(memory_limit) = memory_limit.as_str() else {
return Err(LingerError::invalid_config(
"code_interpreter memory_limit must be a string",
));
};
if !matches!(memory_limit, "1g" | "4g" | "16g" | "64g") {
return Err(LingerError::invalid_config(
"code_interpreter memory_limit must be one of 1g, 4g, 16g, or 64g",
));
}
}
}
if let Some(network_policy) = container.get("network_policy") {
validate_code_interpreter_network_policy(network_policy)?;
}
Ok(())
}
fn validate_code_interpreter_network_policy(network_policy: &Value) -> Result<(), LingerError> {
if network_policy.is_null() {
return Err(LingerError::invalid_config(
"code_interpreter network_policy must not be null",
));
}
let policy = network_policy.as_object().ok_or_else(|| {
LingerError::invalid_config("code_interpreter network_policy must be a JSON object")
})?;
match policy.get("type").and_then(Value::as_str) {
Some("disabled") => Ok(()),
Some("allowlist") => {
if let Some(domains) = policy.get("allowed_domains") {
let domains = domains.as_array().ok_or_else(|| {
LingerError::invalid_config("code_interpreter allowed_domains must be an array")
})?;
if domains.is_empty() {
return Err(LingerError::invalid_config(
"code_interpreter allowed_domains must not be empty",
));
}
for domain in domains {
let Some(domain) = domain.as_str() else {
return Err(LingerError::invalid_config(
"code_interpreter allowed_domains must contain strings",
));
};
validate_optional_string("code_interpreter allowed_domains", Some(domain))?;
}
}
Ok(())
}
_ => Err(LingerError::invalid_config(
"code_interpreter network_policy type must be disabled or allowlist",
)),
}
}
fn validate_image_generation_tool(
object: &serde_json::Map<String, Value>,
) -> Result<(), LingerError> {
for field in ["model", "size"] {
if let Some(value) = object.get(field) {
let Some(value) = value.as_str() else {
return Err(LingerError::invalid_config(format!(
"image_generation {field} must be a string"
)));
};
validate_optional_string(&format!("image_generation {field}"), Some(value))?;
}
}
validate_image_generation_enum(object, "quality", &["low", "medium", "high", "auto"])?;
validate_image_generation_enum(object, "output_format", &["png", "webp", "jpeg"])?;
validate_image_generation_enum(object, "moderation", &["auto", "low"])?;
validate_image_generation_enum(object, "background", &["transparent", "opaque", "auto"])?;
validate_image_generation_enum(object, "input_fidelity", &["high", "low"])?;
validate_image_generation_enum(object, "action", &["generate", "edit", "auto"])?;
if let Some(output_compression) = object.get("output_compression") {
let output_compression = output_compression.as_u64().ok_or_else(|| {
LingerError::invalid_config("image_generation output_compression must be an integer")
})?;
if output_compression > 100 {
return Err(LingerError::invalid_config(
"image_generation output_compression must be between 0 and 100",
));
}
}
if let Some(partial_images) = object.get("partial_images") {
let partial_images = partial_images.as_u64().ok_or_else(|| {
LingerError::invalid_config("image_generation partial_images must be an integer")
})?;
if partial_images > 3 {
return Err(LingerError::invalid_config(
"image_generation partial_images must be between 0 and 3",
));
}
}
if let Some(input_image_mask) = object.get("input_image_mask") {
validate_image_generation_mask(input_image_mask)?;
}
Ok(())
}
fn validate_image_generation_enum(
object: &serde_json::Map<String, Value>,
field: &str,
allowed: &[&str],
) -> Result<(), LingerError> {
if let Some(value) = object.get(field) {
let Some(value) = value.as_str() else {
return Err(LingerError::invalid_config(format!(
"image_generation {field} must be a string"
)));
};
validate_optional_string(&format!("image_generation {field}"), Some(value))?;
if !allowed.contains(&value) {
return Err(LingerError::invalid_config(format!(
"image_generation {field} has an unsupported value"
)));
}
}
Ok(())
}
fn validate_image_generation_mask(input_image_mask: &Value) -> Result<(), LingerError> {
if input_image_mask.is_null() {
return Err(LingerError::invalid_config(
"image_generation input_image_mask must not be null",
));
}
let mask = input_image_mask.as_object().ok_or_else(|| {
LingerError::invalid_config("image_generation input_image_mask must be a JSON object")
})?;
let image_url = mask.get("image_url").and_then(Value::as_str);
let file_id = mask.get("file_id").and_then(Value::as_str);
match (image_url, file_id) {
(Some(image_url), None) => validate_optional_string(
"image_generation input_image_mask.image_url",
Some(image_url),
),
(None, Some(file_id)) => {
validate_optional_string("image_generation input_image_mask.file_id", Some(file_id))
}
(None, None) => Err(LingerError::invalid_config(
"image_generation input_image_mask must include image_url or file_id",
)),
(Some(_), Some(_)) => Err(LingerError::invalid_config(
"image_generation input_image_mask must not include both image_url and file_id",
)),
}
}
fn validate_mcp_tool(object: &serde_json::Map<String, Value>) -> Result<(), LingerError> {
let server_label = object
.get("server_label")
.and_then(Value::as_str)
.unwrap_or_default();
validate_optional_string("mcp server_label", Some(server_label))?;
let server_url = validate_mcp_optional_string(object, "server_url")?;
let connector_id = validate_mcp_optional_string(object, "connector_id")?;
match (server_url, connector_id) {
(Some(_), Some(_)) => {
return Err(LingerError::invalid_config(
"mcp tools must include only one of server_url or connector_id",
));
}
(None, None) => {
return Err(LingerError::invalid_config(
"mcp tools must include server_url or connector_id",
));
}
(Some(_), None) => {}
(None, Some(connector_id)) => validate_mcp_connector_id(connector_id)?,
}
validate_mcp_optional_string(object, "authorization")?;
validate_mcp_optional_string(object, "server_description")?;
if let Some(headers) = object.get("headers") {
validate_mcp_headers(headers)?;
}
if let Some(allowed_tools) = object.get("allowed_tools") {
validate_mcp_allowed_tools(allowed_tools)?;
}
if let Some(require_approval) = object.get("require_approval") {
validate_mcp_require_approval(require_approval)?;
}
if let Some(defer_loading) = object.get("defer_loading") {
if !defer_loading.is_boolean() {
return Err(LingerError::invalid_config(
"mcp defer_loading must be a boolean",
));
}
}
Ok(())
}
fn validate_mcp_optional_string<'a>(
object: &'a serde_json::Map<String, Value>,
field: &str,
) -> Result<Option<&'a str>, LingerError> {
let Some(value) = object.get(field) else {
return Ok(None);
};
let Some(value) = value.as_str() else {
return Err(LingerError::invalid_config(format!(
"mcp {field} must be a string"
)));
};
validate_optional_string(&format!("mcp {field}"), Some(value))?;
Ok(Some(value))
}
fn validate_mcp_connector_id(connector_id: &str) -> Result<(), LingerError> {
if matches!(
connector_id,
"connector_dropbox"
| "connector_gmail"
| "connector_googlecalendar"
| "connector_googledrive"
| "connector_microsoftteams"
| "connector_outlookcalendar"
| "connector_outlookemail"
| "connector_sharepoint"
) {
Ok(())
} else {
Err(LingerError::invalid_config(
"mcp connector_id has an unsupported value",
))
}
}
fn validate_mcp_headers(headers: &Value) -> Result<(), LingerError> {
if headers.is_null() {
return Err(LingerError::invalid_config("mcp headers must not be null"));
}
let headers = headers
.as_object()
.ok_or_else(|| LingerError::invalid_config("mcp headers must be a JSON object"))?;
for (name, value) in headers {
if name.trim().is_empty() {
return Err(LingerError::invalid_config(
"mcp header names must not be empty",
));
}
if !value.is_string() {
return Err(LingerError::invalid_config(
"mcp header values must be strings",
));
}
}
Ok(())
}
fn validate_mcp_allowed_tools(allowed_tools: &Value) -> Result<(), LingerError> {
if allowed_tools.is_null() {
return Err(LingerError::invalid_config(
"mcp allowed_tools must not be null",
));
}
if let Some(tool_names) = allowed_tools.as_array() {
return validate_mcp_tool_names("mcp allowed_tools", tool_names);
}
validate_mcp_tool_filter("mcp allowed_tools", allowed_tools)
}
fn validate_mcp_require_approval(require_approval: &Value) -> Result<(), LingerError> {
if require_approval.is_null() {
return Err(LingerError::invalid_config(
"mcp require_approval must not be null",
));
}
if let Some(mode) = require_approval.as_str() {
if matches!(mode, "always" | "never") {
return Ok(());
}
return Err(LingerError::invalid_config(
"mcp require_approval must be always, never, or a filter object",
));
}
let approval = require_approval.as_object().ok_or_else(|| {
LingerError::invalid_config("mcp require_approval must be a string or object")
})?;
for field in ["always", "never"] {
if let Some(filter) = approval.get(field) {
validate_mcp_tool_filter(&format!("mcp require_approval.{field}"), filter)?;
}
}
for field in approval.keys() {
if !matches!(field.as_str(), "always" | "never") {
return Err(LingerError::invalid_config(
"mcp require_approval only supports always and never filters",
));
}
}
Ok(())
}
fn validate_mcp_tool_filter(name: &str, filter: &Value) -> Result<(), LingerError> {
if filter.is_null() {
return Err(LingerError::invalid_config(format!(
"{name} must not be null"
)));
}
let filter = filter
.as_object()
.ok_or_else(|| LingerError::invalid_config(format!("{name} must be a JSON object")))?;
if let Some(tool_names) = filter.get("tool_names") {
let tool_names = tool_names.as_array().ok_or_else(|| {
LingerError::invalid_config(format!("{name}.tool_names must be an array"))
})?;
validate_mcp_tool_names(&format!("{name}.tool_names"), tool_names)?;
}
if let Some(read_only) = filter.get("read_only") {
if !read_only.is_boolean() {
return Err(LingerError::invalid_config(format!(
"{name}.read_only must be a boolean"
)));
}
}
for field in filter.keys() {
if !matches!(field.as_str(), "tool_names" | "read_only") {
return Err(LingerError::invalid_config(format!(
"{name} only supports tool_names and read_only"
)));
}
}
Ok(())
}
fn validate_mcp_tool_names(name: &str, tool_names: &[Value]) -> Result<(), LingerError> {
for tool_name in tool_names {
let Some(tool_name) = tool_name.as_str() else {
return Err(LingerError::invalid_config(format!(
"{name} must contain strings"
)));
};
validate_optional_string(name, Some(tool_name))?;
}
Ok(())
}
fn validate_web_search_tool(object: &serde_json::Map<String, Value>) -> Result<(), LingerError> {
for field in ["filters", "user_location"] {
if let Some(value) = object.get(field) {
if value.is_null() {
return Err(LingerError::invalid_config(format!(
"web_search {field} must not be null"
)));
}
if !value.is_object() {
return Err(LingerError::invalid_config(format!(
"web_search {field} must be a JSON object"
)));
}
}
}
if let Some(content_types) = object.get("search_content_types") {
let content_types = content_types.as_array().ok_or_else(|| {
LingerError::invalid_config("web_search search_content_types must be an array")
})?;
for content_type in content_types {
let Some(content_type) = content_type.as_str() else {
return Err(LingerError::invalid_config(
"web_search search_content_types must contain strings",
));
};
validate_optional_string("web_search search_content_types", Some(content_type))?;
}
}
Ok(())
}
fn validate_tool_choice(tool_choice: &ResponseToolChoice) -> Result<(), LingerError> {
if let ResponseToolChoice::AllowedTools { tools, .. } = tool_choice {
validate_json_items("tool_choice.tools", tools)?;
}
Ok(())
}
fn validate_prompt(prompt: &ResponsePrompt) -> Result<(), LingerError> {
validate_optional_string("prompt.id", Some(&prompt.id))?;
validate_optional_string("prompt.version", prompt.version.as_deref())?;
for (name, value) in &prompt.variables {
if name.trim().is_empty() {
return Err(LingerError::invalid_config(
"prompt variable names must not be empty",
));
}
if value.is_null() {
return Err(LingerError::invalid_config(
"prompt variable values must not be null",
));
}
}
Ok(())
}
fn validate_text_config(text: &ResponseTextConfig) -> Result<(), LingerError> {
if let Some(ResponseTextFormat::JsonSchema { name, schema, .. }) = &text.format {
validate_optional_string("text.format.name", Some(name))?;
if name.chars().count() > 64 {
return Err(LingerError::invalid_config(
"text.format.name must be at most 64 characters",
));
}
if !name
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-')
{
return Err(LingerError::invalid_config(
"text.format.name must contain only ASCII letters, digits, underscores, or dashes",
));
}
if schema.is_null() {
return Err(LingerError::invalid_config(
"text.format.schema must not be null",
));
}
}
Ok(())
}
fn validate_context_management(
context_management: &[ResponseContextManagement],
) -> Result<(), LingerError> {
if context_management.is_empty() {
return Err(LingerError::invalid_config(
"context_management must contain at least one entry",
));
}
if context_management.iter().any(|entry| {
entry
.compact_threshold
.is_some_and(|compact_threshold| compact_threshold < 1000)
}) {
return Err(LingerError::invalid_config(
"context_management compact_threshold must be at least 1000",
));
}
Ok(())
}
fn validate_moderation(moderation: &ResponseModeration) -> Result<(), LingerError> {
if moderation.model.trim().is_empty() {
return Err(LingerError::invalid_config(
"moderation model must not be empty",
));
}
Ok(())
}
fn validate_extra_fields(extra: &BTreeMap<String, Value>) -> Result<(), LingerError> {
for (key, value) in extra {
if key.trim().is_empty() {
return Err(LingerError::invalid_config(
"extra field names must not be empty",
));
}
if value.is_null() {
return Err(LingerError::invalid_config(format!(
"extra field {key} must not be null"
)));
}
}
Ok(())
}