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() -> String {
473        "FunctionCall".to_string()
474    }
475    fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
476        use schemars::schema::*;
477        // FunctionCall is either "none", "auto", or {"name": "..."}
478        let string_schema = SchemaObject {
479            instance_type: Some(InstanceType::String.into()),
480            enum_values: Some(vec!["none".into(), "auto".into()]),
481            ..Default::default()
482        };
483        let object_schema = SchemaObject {
484            instance_type: Some(InstanceType::Object.into()),
485            object: Some(Box::new(ObjectValidation {
486                properties: {
487                    let mut map = schemars::Map::new();
488                    map.insert("name".to_string(), gen.subschema_for::<String>());
489                    map
490                },
491                required: {
492                    let mut set = std::collections::BTreeSet::new();
493                    set.insert("name".to_string());
494                    set
495                },
496                ..Default::default()
497            })),
498            ..Default::default()
499        };
500        SchemaObject {
501            subschemas: Some(Box::new(SubschemaValidation {
502                any_of: Some(vec![string_schema.into(), object_schema.into()]),
503                ..Default::default()
504            })),
505            ..Default::default()
506        }
507        .into()
508    }
509}
510
511#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
512pub struct FunctionCallResponse {
513    pub name: String,
514    #[serde(default)]
515    pub arguments: Option<String>, // JSON string
516}
517
518// ============================================================================
519// Usage and Logging
520// ============================================================================
521#[serde_with::skip_serializing_none]
522#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
523pub struct Usage {
524    pub prompt_tokens: u32,
525    pub completion_tokens: u32,
526    pub total_tokens: u32,
527    pub prompt_tokens_details: Option<PromptTokenUsageInfo>,
528    pub completion_tokens_details: Option<CompletionTokensDetails>,
529}
530
531impl Usage {
532    /// Create a Usage from prompt and completion token counts
533    pub fn from_counts(prompt_tokens: u32, completion_tokens: u32) -> Self {
534        Self {
535            prompt_tokens,
536            completion_tokens,
537            total_tokens: prompt_tokens + completion_tokens,
538            prompt_tokens_details: None,
539            completion_tokens_details: None,
540        }
541    }
542
543    /// Add cached token details to this Usage
544    pub fn with_cached_tokens(mut self, cached_tokens: u32) -> Self {
545        if cached_tokens > 0 {
546            self.prompt_tokens_details = Some(PromptTokenUsageInfo { cached_tokens });
547        }
548        self
549    }
550
551    /// Add reasoning token details to this Usage
552    pub fn with_reasoning_tokens(mut self, reasoning_tokens: u32) -> Self {
553        if reasoning_tokens > 0 {
554            self.completion_tokens_details = Some(CompletionTokensDetails {
555                reasoning_tokens: Some(reasoning_tokens),
556                accepted_prediction_tokens: None,
557                rejected_prediction_tokens: None,
558            });
559        }
560        self
561    }
562}
563
564#[serde_with::skip_serializing_none]
565#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
566pub struct CompletionTokensDetails {
567    pub reasoning_tokens: Option<u32>,
568    pub accepted_prediction_tokens: Option<u32>,
569    pub rejected_prediction_tokens: Option<u32>,
570}
571
572/// Usage information (used by rerank and other endpoints)
573#[serde_with::skip_serializing_none]
574#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
575pub struct UsageInfo {
576    pub prompt_tokens: u32,
577    pub completion_tokens: u32,
578    pub total_tokens: u32,
579    pub reasoning_tokens: Option<u32>,
580    pub prompt_tokens_details: Option<PromptTokenUsageInfo>,
581}
582
583#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
584pub struct PromptTokenUsageInfo {
585    pub cached_tokens: u32,
586}
587
588#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
589pub struct LogProbs {
590    pub tokens: Vec<String>,
591    pub token_logprobs: Vec<Option<f32>>,
592    pub top_logprobs: Vec<Option<HashMap<String, f32>>>,
593    pub text_offset: Vec<u32>,
594}
595
596#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
597#[serde(untagged)]
598pub enum ChatLogProbs {
599    Detailed {
600        #[serde(skip_serializing_if = "Option::is_none")]
601        content: Option<Vec<ChatLogProbsContent>>,
602    },
603    Raw(Value),
604}
605
606#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
607pub struct ChatLogProbsContent {
608    pub token: String,
609    pub logprob: f32,
610    pub bytes: Option<Vec<u8>>,
611    pub top_logprobs: Vec<TopLogProb>,
612}
613
614#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
615pub struct TopLogProb {
616    pub token: String,
617    pub logprob: f32,
618    pub bytes: Option<Vec<u8>>,
619}
620
621// ============================================================================
622// Error Types
623// ============================================================================
624
625#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
626pub struct ErrorResponse {
627    pub error: ErrorDetail,
628}
629
630#[serde_with::skip_serializing_none]
631#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
632pub struct ErrorDetail {
633    pub message: String,
634    #[serde(rename = "type")]
635    pub error_type: String,
636    pub param: Option<String>,
637    pub code: Option<String>,
638}
639
640// ============================================================================
641// Input Types
642// ============================================================================
643
644#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
645#[serde(untagged)]
646pub enum InputIds {
647    Single(Vec<i32>),
648    Batch(Vec<Vec<i32>>),
649}
650
651/// LoRA adapter path - can be single path or batch of paths (SGLang extension)
652#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
653#[serde(untagged)]
654pub enum LoRAPath {
655    Single(Option<String>),
656    Batch(Vec<Option<String>>),
657}
658
659// ============================================================================
660// Redacted Types
661// ============================================================================
662#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)]
663pub struct Redacted(pub String);
664
665impl std::fmt::Debug for Redacted {
666    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
667        f.write_str("[REDACTED]")
668    }
669}
670
671// ============================================================================
672// Response Prompt
673// ============================================================================
674
675/// Reference to a prompt template and its variables.
676#[serde_with::skip_serializing_none]
677#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
678pub struct ResponsePrompt {
679    pub id: String,
680    pub variables: Option<HashMap<String, PromptVariable>>,
681    pub version: Option<String>,
682}
683
684/// A prompt variable value: plain string or a typed input (text, image, file).
685///
686/// Variant order matters for `#[serde(untagged)]`: a bare JSON string succeeds
687/// as `String`; a JSON object falls through to `Typed`.
688#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
689#[serde(untagged)]
690pub enum PromptVariable {
691    String(String),
692    Typed(PromptVariableTyped),
693}
694
695/// Typed prompt variable input.
696#[serde_with::skip_serializing_none]
697#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
698#[serde(tag = "type")]
699#[expect(
700    clippy::enum_variant_names,
701    reason = "variant names match OpenAI API spec"
702)]
703pub enum PromptVariableTyped {
704    #[serde(rename = "input_text")]
705    ResponseInputText { text: String },
706    #[serde(rename = "input_image")]
707    ResponseInputImage {
708        detail: Option<Detail>,
709        file_id: Option<String>,
710        image_url: Option<String>,
711    },
712    #[serde(rename = "input_file")]
713    ResponseInputFile {
714        file_data: Option<String>,
715        file_id: Option<String>,
716        file_url: Option<String>,
717        filename: Option<String>,
718    },
719}
720
721/// Image detail level for [`PromptVariableTyped::ResponseInputImage`] and
722/// [`crate::responses::ResponseContentPart::InputImage`]. Spec allows
723/// `"low" | "high" | "auto" | "original"`.
724#[derive(Debug, Clone, Serialize, Deserialize, Default, schemars::JsonSchema)]
725#[serde(rename_all = "snake_case")]
726pub enum Detail {
727    Low,
728    High,
729    #[default]
730    Auto,
731    Original,
732}
733
734// ============================================================================
735// Responses API: prompt-cache retention & context management
736// ============================================================================
737
738/// Retention policy for prompt-cache entries on the Responses API.
739///
740/// Spec: `prompt_cache_retention: "in-memory" | "24h"`.
741#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
742pub enum PromptCacheRetention {
743    #[serde(rename = "in-memory")]
744    InMemory,
745    #[serde(rename = "24h")]
746    Duration24h,
747}
748
749/// A single entry in the Responses API `context_management` array.
750///
751/// Spec: each entry has `type` (currently only `"compaction"`) and an optional
752/// `compact_threshold` token count.
753#[serde_with::skip_serializing_none]
754#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
755pub struct ContextManagementEntry {
756    #[serde(rename = "type")]
757    pub r#type: ContextManagementType,
758    pub compact_threshold: Option<u32>,
759}
760
761/// Type tag for [`ContextManagementEntry`]. Currently only `compaction` is
762/// defined by the spec; the enum is kept small so unknown values serde-fail
763/// (consistent with P5's fail-fast direction).
764#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
765#[serde(rename_all = "snake_case")]
766pub enum ContextManagementType {
767    Compaction,
768}
769
770// ============================================================================
771// Responses API: conversation reference
772// ============================================================================
773
774/// Reference to a conversation the response belongs to.
775///
776/// Spec: `conversation: string | ResponseConversationParam { id: string }`.
777/// Variant order matters for `#[serde(untagged)]`: a bare JSON string succeeds
778/// as `Id`; an object falls through to `Object`.
779#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
780#[serde(untagged)]
781pub enum ConversationRef {
782    Id(String),
783    Object { id: String },
784}
785
786impl ConversationRef {
787    /// Return the underlying conversation id regardless of the wire shape.
788    pub fn as_id(&self) -> &str {
789        match self {
790            Self::Id(id) | Self::Object { id } => id.as_str(),
791        }
792    }
793
794    /// `true` when the underlying conversation id is the empty string.
795    /// Mirrors `String::is_empty` for callers that previously treated
796    /// `Option<String>` empty values as "unset".
797    pub fn is_empty(&self) -> bool {
798        self.as_id().is_empty()
799    }
800}
801
802#[cfg(test)]
803mod tests {
804    use serde::Deserialize;
805    use serde_json::json;
806
807    use super::*;
808
809    #[derive(Deserialize)]
810    struct NullableBoolTest {
811        #[serde(default, deserialize_with = "deserialize_null_as_false")]
812        field: bool,
813    }
814
815    #[test]
816    fn test_deserialize_null_as_false() {
817        let cases = [
818            (json!({"field": true}), true),
819            (json!({"field": false}), false),
820            (json!({"field": null}), false),
821            (json!({}), false),
822        ];
823        for (input, expected) in cases {
824            let t: NullableBoolTest = serde_json::from_value(input).unwrap();
825            assert_eq!(t.field, expected);
826        }
827    }
828
829    #[test]
830    fn test_deserialize_null_as_false_rejects_non_bool() {
831        let result = serde_json::from_value::<NullableBoolTest>(json!({"field": "yes"}));
832        assert!(result.is_err());
833    }
834
835    #[test]
836    fn conversation_ref_deserializes_bare_string() {
837        let v = json!("conv_abc");
838        let r: ConversationRef = serde_json::from_value(v).expect("string form");
839        assert!(matches!(r, ConversationRef::Id(ref s) if s == "conv_abc"));
840        assert_eq!(r.as_id(), "conv_abc");
841        // Bare string round-trips back to a JSON string.
842        assert_eq!(serde_json::to_value(&r).unwrap(), json!("conv_abc"));
843    }
844
845    #[test]
846    fn conversation_ref_deserializes_object() {
847        let v = json!({"id": "conv_xyz"});
848        let r: ConversationRef = serde_json::from_value(v).expect("object form");
849        assert!(matches!(r, ConversationRef::Object { ref id } if id == "conv_xyz"));
850        assert_eq!(r.as_id(), "conv_xyz");
851        // Object round-trips back to an object.
852        assert_eq!(serde_json::to_value(&r).unwrap(), json!({"id": "conv_xyz"}));
853    }
854
855    #[test]
856    fn conversation_ref_is_empty() {
857        assert!(ConversationRef::Id(String::new()).is_empty());
858        assert!(!ConversationRef::Id("conv_1".to_string()).is_empty());
859        assert!(ConversationRef::Object { id: String::new() }.is_empty());
860    }
861}