Skip to main content

openai_protocol/
common.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use validator;
6
7// ============================================================================
8// Default value helpers
9// ============================================================================
10
11/// Default model for endpoints where model is optional (e.g., /generate).
12/// Uses UNKNOWN_MODEL_ID so routers treat it as "any available worker."
13pub fn default_unknown_model() -> String {
14    super::UNKNOWN_MODEL_ID.to_string()
15}
16
17/// Helper function for serde default value (returns true)
18pub fn default_true() -> bool {
19    true
20}
21
22/// Deserialize a bool that also accepts JSON `null` (mapped to `false`).
23///
24/// Use with `#[serde(default, deserialize_with = "deserialize_null_as_false")]`
25/// on fields that the OpenAI spec defines as `Optional[bool]` defaulting to `false`.
26pub fn deserialize_null_as_false<'de, D>(deserializer: D) -> Result<bool, D::Error>
27where
28    D: serde::Deserializer<'de>,
29{
30    Option::<bool>::deserialize(deserializer).map(|opt| opt.unwrap_or(false))
31}
32
33// ============================================================================
34// GenerationRequest Trait
35// ============================================================================
36
37/// Trait for unified access to generation request properties
38/// Implemented by ChatCompletionRequest, CompletionRequest, GenerateRequest,
39/// EmbeddingRequest, RerankRequest, and ResponsesRequest
40pub trait GenerationRequest: Send + Sync {
41    /// Check if the request is for streaming
42    fn is_stream(&self) -> bool;
43
44    /// Get the model name if specified
45    fn get_model(&self) -> Option<&str>;
46
47    /// Extract text content for routing decisions
48    fn extract_text_for_routing(&self) -> String;
49}
50
51// ============================================================================
52// String/Array Utilities
53// ============================================================================
54
55/// A type that can be either a single string or an array of strings
56#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, schemars::JsonSchema)]
57#[serde(untagged)]
58pub enum StringOrArray {
59    String(String),
60    Array(Vec<String>),
61}
62
63impl StringOrArray {
64    /// Get the number of items in the StringOrArray
65    pub fn len(&self) -> usize {
66        match self {
67            StringOrArray::String(_) => 1,
68            StringOrArray::Array(arr) => arr.len(),
69        }
70    }
71
72    /// Check if the StringOrArray is empty
73    pub fn is_empty(&self) -> bool {
74        match self {
75            StringOrArray::String(s) => s.is_empty(),
76            StringOrArray::Array(arr) => arr.is_empty(),
77        }
78    }
79
80    /// Convert to a vector of strings (clones the data)
81    pub fn to_vec(&self) -> Vec<String> {
82        match self {
83            StringOrArray::String(s) => vec![s.clone()],
84            StringOrArray::Array(arr) => arr.clone(),
85        }
86    }
87
88    /// Returns an iterator over string references without cloning.
89    /// Use this instead of `to_vec()` when you only need to iterate.
90    pub fn iter(&self) -> StringOrArrayIter<'_> {
91        StringOrArrayIter {
92            inner: self,
93            index: 0,
94        }
95    }
96
97    /// Returns the first string, or None if empty
98    pub fn first(&self) -> Option<&str> {
99        match self {
100            StringOrArray::String(s) => {
101                if s.is_empty() {
102                    None
103                } else {
104                    Some(s)
105                }
106            }
107            StringOrArray::Array(arr) => arr.first().map(|s| s.as_str()),
108        }
109    }
110}
111
112/// Iterator over StringOrArray that yields string references without cloning
113pub struct StringOrArrayIter<'a> {
114    inner: &'a StringOrArray,
115    index: usize,
116}
117
118impl<'a> Iterator for StringOrArrayIter<'a> {
119    type Item = &'a str;
120
121    fn next(&mut self) -> Option<Self::Item> {
122        match self.inner {
123            StringOrArray::String(s) => {
124                if self.index == 0 {
125                    self.index = 1;
126                    Some(s.as_str())
127                } else {
128                    None
129                }
130            }
131            StringOrArray::Array(arr) => {
132                if self.index < arr.len() {
133                    let item = &arr[self.index];
134                    self.index += 1;
135                    Some(item.as_str())
136                } else {
137                    None
138                }
139            }
140        }
141    }
142
143    fn size_hint(&self) -> (usize, Option<usize>) {
144        let remaining = match self.inner {
145            StringOrArray::String(_) => 1 - self.index,
146            StringOrArray::Array(arr) => arr.len() - self.index,
147        };
148        (remaining, Some(remaining))
149    }
150}
151
152impl<'a> ExactSizeIterator for StringOrArrayIter<'a> {}
153
154/// Validates stop sequences (max 4, non-empty strings)
155/// Used by both ChatCompletionRequest and ResponsesRequest
156pub fn validate_stop(stop: &StringOrArray) -> Result<(), validator::ValidationError> {
157    match stop {
158        StringOrArray::String(s) => {
159            if s.is_empty() {
160                return Err(validator::ValidationError::new(
161                    "stop sequences cannot be empty",
162                ));
163            }
164        }
165        StringOrArray::Array(arr) => {
166            if arr.len() > 4 {
167                return Err(validator::ValidationError::new(
168                    "maximum 4 stop sequences allowed",
169                ));
170            }
171            for s in arr {
172                if s.is_empty() {
173                    return Err(validator::ValidationError::new(
174                        "stop sequences cannot be empty",
175                    ));
176                }
177            }
178        }
179    }
180    Ok(())
181}
182
183// ============================================================================
184// Content Parts (for multimodal messages)
185// ============================================================================
186
187#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
188#[serde(tag = "type")]
189pub enum ContentPart {
190    #[serde(rename = "text")]
191    Text { text: String },
192    #[serde(rename = "image_url")]
193    ImageUrl { image_url: ImageUrl },
194    #[serde(rename = "video_url")]
195    VideoUrl { video_url: VideoUrl },
196}
197
198#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
199pub struct ImageUrl {
200    pub url: String,
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub detail: Option<String>, // "auto", "low", or "high"
203}
204
205#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
206pub struct VideoUrl {
207    pub url: String,
208}
209
210// ============================================================================
211// Response Format (for structured outputs)
212// ============================================================================
213
214#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
215#[serde(tag = "type")]
216pub enum ResponseFormat {
217    #[serde(rename = "text")]
218    Text,
219    #[serde(rename = "json_object")]
220    JsonObject,
221    #[serde(rename = "json_schema")]
222    JsonSchema { json_schema: JsonSchemaFormat },
223}
224
225#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
226pub struct JsonSchemaFormat {
227    pub name: String,
228    pub schema: Value,
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub strict: Option<bool>,
231}
232
233// ============================================================================
234// Streaming
235// ============================================================================
236
237#[derive(Debug, Clone, Default, Deserialize, Serialize, schemars::JsonSchema)]
238pub struct StreamOptions {
239    /// Chat Completions / Completions: include usage block at end of stream.
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub include_usage: Option<bool>,
242
243    /// Chat Completions / Completions: emit a usage chunk with every streamed
244    /// delta instead of only in the final chunk.
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub continuous_usage_stats: Option<bool>,
247
248    /// Responses API: add random chars on `obfuscation` field of delta events
249    /// to normalize payload sizes. Defaults to `true` upstream when absent.
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub include_obfuscation: Option<bool>,
252}
253
254#[serde_with::skip_serializing_none]
255#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
256pub struct ToolCallDelta {
257    pub index: u32,
258    pub id: Option<String>,
259    #[serde(rename = "type")]
260    pub tool_type: Option<String>,
261    pub function: Option<FunctionCallDelta>,
262}
263
264#[serde_with::skip_serializing_none]
265#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
266pub struct FunctionCallDelta {
267    pub name: Option<String>,
268    pub arguments: Option<String>,
269}
270
271// ============================================================================
272// Tools and Function Calling
273// ============================================================================
274
275/// Tool choice value for simple string options
276#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
277#[serde(rename_all = "snake_case")]
278pub enum ToolChoiceValue {
279    Auto,
280    Required,
281    None,
282}
283
284/// Tool choice for both Chat Completion and Responses APIs
285#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
286#[serde(untagged)]
287pub enum ToolChoice {
288    Value(ToolChoiceValue),
289    Function {
290        #[serde(rename = "type")]
291        tool_type: String, // "function"
292        function: FunctionChoice,
293    },
294    AllowedTools {
295        #[serde(rename = "type")]
296        tool_type: String, // "allowed_tools"
297        mode: String, // "auto" | "required" TODO: need validation
298        tools: Vec<ToolReference>,
299    },
300}
301
302impl Default for ToolChoice {
303    fn default() -> Self {
304        Self::Value(ToolChoiceValue::Auto)
305    }
306}
307
308impl ToolChoice {
309    /// Serialize tool_choice to string for ResponsesResponse
310    ///
311    /// Returns the JSON-serialized tool_choice or "auto" as default
312    pub fn serialize_to_string(tool_choice: Option<&ToolChoice>) -> String {
313        tool_choice
314            .map(|tc| serde_json::to_string(tc).unwrap_or_else(|_| "auto".to_string()))
315            .unwrap_or_else(|| "auto".to_string())
316    }
317}
318
319/// Function choice specification for ToolChoice::Function
320#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
321pub struct FunctionChoice {
322    pub name: String,
323}
324
325/// Tool reference for ToolChoice::AllowedTools
326///
327/// Represents a reference to a specific tool in the allowed_tools array.
328/// Different tool types have different required fields.
329#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
330#[serde(tag = "type")]
331#[serde(rename_all = "snake_case")]
332pub enum ToolReference {
333    /// Reference to a function tool
334    #[serde(rename = "function")]
335    Function { name: String },
336
337    /// Reference to an MCP tool
338    #[serde(rename = "mcp")]
339    Mcp {
340        server_label: String,
341        #[serde(skip_serializing_if = "Option::is_none")]
342        name: Option<String>,
343    },
344
345    /// File search hosted tool
346    #[serde(rename = "file_search")]
347    FileSearch,
348
349    /// Web search preview hosted tool
350    #[serde(rename = "web_search_preview")]
351    WebSearchPreview,
352
353    /// Computer use preview hosted tool
354    #[serde(rename = "computer_use_preview")]
355    ComputerUsePreview,
356
357    /// Code interpreter hosted tool
358    #[serde(rename = "code_interpreter")]
359    CodeInterpreter,
360
361    /// Image generation hosted tool
362    #[serde(rename = "image_generation")]
363    ImageGeneration,
364}
365
366impl ToolReference {
367    /// Get a unique identifier for this tool reference
368    pub fn identifier(&self) -> String {
369        match self {
370            ToolReference::Function { name } => format!("function:{name}"),
371            ToolReference::Mcp { server_label, name } => {
372                if let Some(n) = name {
373                    format!("mcp:{server_label}:{n}")
374                } else {
375                    format!("mcp:{server_label}")
376                }
377            }
378            ToolReference::FileSearch => "file_search".to_string(),
379            ToolReference::WebSearchPreview => "web_search_preview".to_string(),
380            ToolReference::ComputerUsePreview => "computer_use_preview".to_string(),
381            ToolReference::CodeInterpreter => "code_interpreter".to_string(),
382            ToolReference::ImageGeneration => "image_generation".to_string(),
383        }
384    }
385
386    /// Get the tool name if this is a function tool
387    pub fn function_name(&self) -> Option<&str> {
388        match self {
389            ToolReference::Function { name } => Some(name.as_str()),
390            _ => None,
391        }
392    }
393}
394
395#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
396pub struct Tool {
397    #[serde(rename = "type")]
398    pub tool_type: String, // "function"
399    pub function: Function,
400}
401
402#[serde_with::skip_serializing_none]
403#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
404pub struct Function {
405    pub name: String,
406    pub description: Option<String>,
407    pub parameters: Value, // JSON Schema
408    /// Whether to enable strict schema adherence (OpenAI structured outputs)
409    pub strict: Option<bool>,
410}
411
412#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
413pub struct ToolCall {
414    pub id: String,
415    #[serde(rename = "type")]
416    pub tool_type: String, // "function"
417    pub function: FunctionCallResponse,
418}
419
420/// Deprecated `function_call` field from the OpenAI API.
421/// Can be `"none"`, `"auto"`, or `{"name": "function_name"}`.
422#[derive(Debug, Clone)]
423pub enum FunctionCall {
424    None,
425    Auto,
426    Function { name: String },
427}
428
429impl Serialize for FunctionCall {
430    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
431        match self {
432            FunctionCall::None => serializer.serialize_str("none"),
433            FunctionCall::Auto => serializer.serialize_str("auto"),
434            FunctionCall::Function { name } => {
435                use serde::ser::SerializeMap;
436                let mut map = serializer.serialize_map(Some(1))?;
437                map.serialize_entry("name", name)?;
438                map.end()
439            }
440        }
441    }
442}
443
444impl<'de> Deserialize<'de> for FunctionCall {
445    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
446        let value = Value::deserialize(deserializer)?;
447        match &value {
448            Value::String(s) => match s.as_str() {
449                "none" => Ok(FunctionCall::None),
450                "auto" => Ok(FunctionCall::Auto),
451                other => Err(serde::de::Error::custom(format!(
452                    "unknown function_call value: \"{other}\""
453                ))),
454            },
455            Value::Object(map) => {
456                if let Some(Value::String(name)) = map.get("name") {
457                    Ok(FunctionCall::Function { name: name.clone() })
458                } else {
459                    Err(serde::de::Error::custom(
460                        "function_call object must have a \"name\" string field",
461                    ))
462                }
463            }
464            _ => Err(serde::de::Error::custom(
465                "function_call must be a string or object",
466            )),
467        }
468    }
469}
470
471impl schemars::JsonSchema for FunctionCall {
472    fn schema_name() -> std::borrow::Cow<'static, str> {
473        "FunctionCall".into()
474    }
475    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
476        // FunctionCall is either "none", "auto", or {"name": "..."}
477        let name_schema = generator.subschema_for::<String>();
478        schemars::json_schema!({
479            "anyOf": [
480                {
481                    "type": "string",
482                    "enum": ["none", "auto"]
483                },
484                {
485                    "type": "object",
486                    "properties": { "name": name_schema },
487                    "required": ["name"]
488                }
489            ]
490        })
491    }
492}
493
494#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
495pub struct FunctionCallResponse {
496    pub name: String,
497    #[serde(default)]
498    pub arguments: Option<String>, // JSON string
499}
500
501// ============================================================================
502// Usage and Logging
503// ============================================================================
504#[serde_with::skip_serializing_none]
505#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
506pub struct Usage {
507    pub prompt_tokens: u32,
508    pub completion_tokens: u32,
509    pub total_tokens: u32,
510    pub prompt_tokens_details: Option<PromptTokenUsageInfo>,
511    pub completion_tokens_details: Option<CompletionTokensDetails>,
512}
513
514impl Usage {
515    /// Create a Usage from prompt and completion token counts
516    pub fn from_counts(prompt_tokens: u32, completion_tokens: u32) -> Self {
517        Self {
518            prompt_tokens,
519            completion_tokens,
520            total_tokens: prompt_tokens + completion_tokens,
521            prompt_tokens_details: None,
522            completion_tokens_details: None,
523        }
524    }
525
526    /// Add cached token details to this Usage
527    pub fn with_cached_tokens(mut self, cached_tokens: u32) -> Self {
528        if cached_tokens > 0 {
529            self.prompt_tokens_details = Some(PromptTokenUsageInfo { cached_tokens });
530        }
531        self
532    }
533
534    /// Add reasoning token details to this Usage
535    pub fn with_reasoning_tokens(mut self, reasoning_tokens: u32) -> Self {
536        if reasoning_tokens > 0 {
537            self.completion_tokens_details = Some(CompletionTokensDetails {
538                reasoning_tokens: Some(reasoning_tokens),
539                accepted_prediction_tokens: None,
540                rejected_prediction_tokens: None,
541            });
542        }
543        self
544    }
545}
546
547#[serde_with::skip_serializing_none]
548#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
549pub struct CompletionTokensDetails {
550    pub reasoning_tokens: Option<u32>,
551    pub accepted_prediction_tokens: Option<u32>,
552    pub rejected_prediction_tokens: Option<u32>,
553}
554
555/// Usage information (used by rerank and other endpoints)
556#[serde_with::skip_serializing_none]
557#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
558pub struct UsageInfo {
559    pub prompt_tokens: u32,
560    pub completion_tokens: u32,
561    pub total_tokens: u32,
562    pub reasoning_tokens: Option<u32>,
563    pub prompt_tokens_details: Option<PromptTokenUsageInfo>,
564}
565
566#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
567pub struct PromptTokenUsageInfo {
568    pub cached_tokens: u32,
569}
570
571#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
572pub struct LogProbs {
573    pub tokens: Vec<String>,
574    pub token_logprobs: Vec<Option<f32>>,
575    pub top_logprobs: Vec<Option<HashMap<String, f32>>>,
576    pub text_offset: Vec<u32>,
577}
578
579#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
580#[serde(untagged)]
581pub enum ChatLogProbs {
582    Detailed {
583        #[serde(skip_serializing_if = "Option::is_none")]
584        content: Option<Vec<ChatLogProbsContent>>,
585    },
586    Raw(Value),
587}
588
589#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
590pub struct ChatLogProbsContent {
591    pub token: String,
592    pub logprob: f32,
593    pub bytes: Option<Vec<u8>>,
594    pub top_logprobs: Vec<TopLogProb>,
595}
596
597#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
598pub struct TopLogProb {
599    pub token: String,
600    pub logprob: f32,
601    pub bytes: Option<Vec<u8>>,
602}
603
604// ============================================================================
605// Error Types
606// ============================================================================
607
608#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
609pub struct ErrorResponse {
610    pub error: ErrorDetail,
611}
612
613#[serde_with::skip_serializing_none]
614#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
615pub struct ErrorDetail {
616    pub message: String,
617    #[serde(rename = "type")]
618    pub error_type: String,
619    pub param: Option<String>,
620    pub code: Option<String>,
621}
622
623// ============================================================================
624// Input Types
625// ============================================================================
626
627#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
628#[serde(untagged)]
629pub enum InputIds {
630    Single(Vec<i32>),
631    Batch(Vec<Vec<i32>>),
632}
633
634/// LoRA adapter path - can be single path or batch of paths (SGLang extension)
635#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
636#[serde(untagged)]
637pub enum LoRAPath {
638    Single(Option<String>),
639    Batch(Vec<Option<String>>),
640}
641
642// ============================================================================
643// Redacted Types
644// ============================================================================
645#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)]
646pub struct Redacted(pub String);
647
648impl std::fmt::Debug for Redacted {
649    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
650        f.write_str("[REDACTED]")
651    }
652}
653
654// ============================================================================
655// Response Prompt
656// ============================================================================
657
658/// Reference to a prompt template and its variables.
659#[serde_with::skip_serializing_none]
660#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
661pub struct ResponsePrompt {
662    pub id: String,
663    pub variables: Option<HashMap<String, PromptVariable>>,
664    pub version: Option<String>,
665}
666
667/// A prompt variable value: plain string or a typed input (text, image, file).
668///
669/// Variant order matters for `#[serde(untagged)]`: a bare JSON string succeeds
670/// as `String`; a JSON object falls through to `Typed`.
671#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
672#[serde(untagged)]
673pub enum PromptVariable {
674    String(String),
675    Typed(PromptVariableTyped),
676}
677
678/// Typed prompt variable input.
679#[serde_with::skip_serializing_none]
680#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
681#[serde(tag = "type")]
682#[expect(
683    clippy::enum_variant_names,
684    reason = "variant names match OpenAI API spec"
685)]
686pub enum PromptVariableTyped {
687    #[serde(rename = "input_text")]
688    ResponseInputText { text: String },
689    #[serde(rename = "input_image")]
690    ResponseInputImage {
691        detail: Option<Detail>,
692        file_id: Option<String>,
693        image_url: Option<String>,
694    },
695    #[serde(rename = "input_file")]
696    ResponseInputFile {
697        file_data: Option<String>,
698        file_id: Option<String>,
699        file_url: Option<String>,
700        filename: Option<String>,
701    },
702}
703
704/// Image detail level for [`PromptVariableTyped::ResponseInputImage`] and
705/// [`crate::responses::ResponseContentPart::InputImage`]. Spec allows
706/// `"low" | "high" | "auto" | "original"`.
707#[derive(Debug, Clone, Serialize, Deserialize, Default, schemars::JsonSchema)]
708#[serde(rename_all = "snake_case")]
709pub enum Detail {
710    Low,
711    High,
712    #[default]
713    Auto,
714    Original,
715}
716
717// ============================================================================
718// Responses API: prompt-cache retention & context management
719// ============================================================================
720
721/// Retention policy for prompt-cache entries on the Responses API.
722///
723/// Spec: `prompt_cache_retention: "in-memory" | "24h"`.
724#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
725pub enum PromptCacheRetention {
726    #[serde(rename = "in-memory")]
727    InMemory,
728    #[serde(rename = "24h")]
729    Duration24h,
730}
731
732/// A single entry in the Responses API `context_management` array.
733///
734/// Spec: each entry has `type` (currently only `"compaction"`) and an optional
735/// `compact_threshold` token count.
736#[serde_with::skip_serializing_none]
737#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
738pub struct ContextManagementEntry {
739    #[serde(rename = "type")]
740    pub r#type: ContextManagementType,
741    pub compact_threshold: Option<u32>,
742}
743
744/// Type tag for [`ContextManagementEntry`]. Currently only `compaction` is
745/// defined by the spec; the enum is kept small so unknown values serde-fail
746/// (consistent with P5's fail-fast direction).
747#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
748#[serde(rename_all = "snake_case")]
749pub enum ContextManagementType {
750    Compaction,
751}
752
753// ============================================================================
754// Responses API: conversation reference
755// ============================================================================
756
757/// Reference to a conversation the response belongs to.
758///
759/// Spec: `conversation: string | ResponseConversationParam { id: string }`.
760/// Variant order matters for `#[serde(untagged)]`: a bare JSON string succeeds
761/// as `Id`; an object falls through to `Object`.
762#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
763#[serde(untagged)]
764pub enum ConversationRef {
765    Id(String),
766    Object { id: String },
767}
768
769impl ConversationRef {
770    /// Return the underlying conversation id regardless of the wire shape.
771    pub fn as_id(&self) -> &str {
772        match self {
773            Self::Id(id) | Self::Object { id } => id.as_str(),
774        }
775    }
776
777    /// `true` when the underlying conversation id is the empty string.
778    /// Mirrors `String::is_empty` for callers that previously treated
779    /// `Option<String>` empty values as "unset".
780    pub fn is_empty(&self) -> bool {
781        self.as_id().is_empty()
782    }
783}
784
785#[cfg(test)]
786mod tests {
787    use serde::Deserialize;
788    use serde_json::json;
789
790    use super::*;
791
792    #[derive(Deserialize)]
793    struct NullableBoolTest {
794        #[serde(default, deserialize_with = "deserialize_null_as_false")]
795        field: bool,
796    }
797
798    #[test]
799    fn test_deserialize_null_as_false() {
800        let cases = [
801            (json!({"field": true}), true),
802            (json!({"field": false}), false),
803            (json!({"field": null}), false),
804            (json!({}), false),
805        ];
806        for (input, expected) in cases {
807            let t: NullableBoolTest = serde_json::from_value(input).unwrap();
808            assert_eq!(t.field, expected);
809        }
810    }
811
812    #[test]
813    fn test_deserialize_null_as_false_rejects_non_bool() {
814        let result = serde_json::from_value::<NullableBoolTest>(json!({"field": "yes"}));
815        assert!(result.is_err());
816    }
817
818    #[test]
819    fn conversation_ref_deserializes_bare_string() {
820        let v = json!("conv_abc");
821        let r: ConversationRef = serde_json::from_value(v).expect("string form");
822        assert!(matches!(r, ConversationRef::Id(ref s) if s == "conv_abc"));
823        assert_eq!(r.as_id(), "conv_abc");
824        // Bare string round-trips back to a JSON string.
825        assert_eq!(serde_json::to_value(&r).unwrap(), json!("conv_abc"));
826    }
827
828    #[test]
829    fn conversation_ref_deserializes_object() {
830        let v = json!({"id": "conv_xyz"});
831        let r: ConversationRef = serde_json::from_value(v).expect("object form");
832        assert!(matches!(r, ConversationRef::Object { ref id } if id == "conv_xyz"));
833        assert_eq!(r.as_id(), "conv_xyz");
834        // Object round-trips back to an object.
835        assert_eq!(serde_json::to_value(&r).unwrap(), json!({"id": "conv_xyz"}));
836    }
837
838    #[test]
839    fn conversation_ref_is_empty() {
840        assert!(ConversationRef::Id(String::new()).is_empty());
841        assert!(!ConversationRef::Id("conv_1".to_string()).is_empty());
842        assert!(ConversationRef::Object { id: String::new() }.is_empty());
843    }
844}