Skip to main content

openai_protocol/
common.rs

1use std::collections::HashMap;
2
3use serde::{
4    de::{self, value::SeqAccessDeserializer, SeqAccess, Visitor},
5    Deserialize, Deserializer, Serialize,
6};
7use serde_json::{Map, Value};
8use validator;
9
10// ============================================================================
11// Default value helpers
12// ============================================================================
13
14/// Default model for endpoints where model is optional (e.g., /generate).
15/// Uses UNKNOWN_MODEL_ID so routers treat it as "any available worker."
16pub fn default_unknown_model() -> String {
17    super::UNKNOWN_MODEL_ID.to_string()
18}
19
20/// Helper function for serde default value (returns true)
21pub fn default_true() -> bool {
22    true
23}
24
25/// Helper for `#[serde(skip_serializing_if = "is_false")]` on default-`false` flags.
26#[expect(
27    clippy::trivially_copy_pass_by_ref,
28    reason = "serde skip_serializing_if passes &T"
29)]
30pub fn is_false(v: &bool) -> bool {
31    !*v
32}
33
34/// Helper for `#[serde(skip_serializing_if = "is_true")]` on default-`true` flags.
35#[expect(
36    clippy::trivially_copy_pass_by_ref,
37    reason = "serde skip_serializing_if passes &T"
38)]
39pub fn is_true(v: &bool) -> bool {
40    *v
41}
42
43/// Deserialize a bool that also accepts JSON `null` (mapped to `false`).
44///
45/// Use with `#[serde(default, deserialize_with = "deserialize_null_as_false")]`
46/// on fields that the OpenAI spec defines as `Optional[bool]` defaulting to `false`.
47pub fn deserialize_null_as_false<'de, D>(deserializer: D) -> Result<bool, D::Error>
48where
49    D: Deserializer<'de>,
50{
51    Option::<bool>::deserialize(deserializer).map(|opt| opt.unwrap_or(false))
52}
53
54// ============================================================================
55// GenerationRequest Trait
56// ============================================================================
57
58/// Trait for unified access to generation request properties
59/// Implemented by ChatCompletionRequest, CompletionRequest, GenerateRequest,
60/// EmbeddingRequest, RerankRequest, and ResponsesRequest
61pub trait GenerationRequest: Send + Sync {
62    /// Check if the request is for streaming
63    fn is_stream(&self) -> bool;
64
65    /// Get the model name if specified
66    fn get_model(&self) -> Option<&str>;
67
68    /// Extract text content for routing decisions
69    fn extract_text_for_routing(&self) -> String;
70
71    /// Token IDs for routing when the request is already tokenized.
72    /// Some(_) routes on the token radix tree instead of the decimal-string
73    /// rendering of the same IDs.
74    fn routing_tokens(&self) -> Option<&[i32]> {
75        None
76    }
77
78    /// Client-provided request id, when the protocol carries one. Routing may
79    /// derive a session-affinity key from it; a batch reports its first id.
80    fn rid(&self) -> Option<&str> {
81        None
82    }
83}
84
85// ============================================================================
86// String/Array Utilities
87// ============================================================================
88
89/// A type that can be either a single string or an array of strings
90#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, schemars::JsonSchema)]
91#[serde(untagged)]
92pub enum StringOrArray {
93    String(String),
94    Array(Vec<String>),
95}
96
97impl StringOrArray {
98    /// Get the number of items in the StringOrArray
99    pub fn len(&self) -> usize {
100        match self {
101            StringOrArray::String(_) => 1,
102            StringOrArray::Array(arr) => arr.len(),
103        }
104    }
105
106    /// Check if the StringOrArray is empty
107    pub fn is_empty(&self) -> bool {
108        match self {
109            StringOrArray::String(s) => s.is_empty(),
110            StringOrArray::Array(arr) => arr.is_empty(),
111        }
112    }
113
114    /// Convert to a vector of strings (clones the data)
115    pub fn to_vec(&self) -> Vec<String> {
116        match self {
117            StringOrArray::String(s) => vec![s.clone()],
118            StringOrArray::Array(arr) => arr.clone(),
119        }
120    }
121
122    /// Returns an iterator over string references without cloning.
123    /// Use this instead of `to_vec()` when you only need to iterate.
124    pub fn iter(&self) -> StringOrArrayIter<'_> {
125        StringOrArrayIter {
126            inner: self,
127            index: 0,
128        }
129    }
130
131    /// Returns the first string, or None if empty
132    pub fn first(&self) -> Option<&str> {
133        match self {
134            StringOrArray::String(s) => {
135                if s.is_empty() {
136                    None
137                } else {
138                    Some(s)
139                }
140            }
141            StringOrArray::Array(arr) => arr.first().map(|s| s.as_str()),
142        }
143    }
144}
145
146/// Iterator over StringOrArray that yields string references without cloning
147pub struct StringOrArrayIter<'a> {
148    inner: &'a StringOrArray,
149    index: usize,
150}
151
152impl<'a> Iterator for StringOrArrayIter<'a> {
153    type Item = &'a str;
154
155    fn next(&mut self) -> Option<Self::Item> {
156        match self.inner {
157            StringOrArray::String(s) => {
158                if self.index == 0 {
159                    self.index = 1;
160                    Some(s.as_str())
161                } else {
162                    None
163                }
164            }
165            StringOrArray::Array(arr) => {
166                if self.index < arr.len() {
167                    let item = &arr[self.index];
168                    self.index += 1;
169                    Some(item.as_str())
170                } else {
171                    None
172                }
173            }
174        }
175    }
176
177    fn size_hint(&self) -> (usize, Option<usize>) {
178        let remaining = match self.inner {
179            StringOrArray::String(_) => 1 - self.index,
180            StringOrArray::Array(arr) => arr.len() - self.index,
181        };
182        (remaining, Some(remaining))
183    }
184}
185
186impl<'a> ExactSizeIterator for StringOrArrayIter<'a> {}
187
188/// Validates stop sequences (max 4, non-empty strings)
189/// Used by both ChatCompletionRequest and ResponsesRequest
190pub fn validate_stop(stop: &StringOrArray) -> Result<(), validator::ValidationError> {
191    match stop {
192        StringOrArray::String(s) => {
193            if s.is_empty() {
194                return Err(validator::ValidationError::new(
195                    "stop sequences cannot be empty",
196                ));
197            }
198        }
199        StringOrArray::Array(arr) => {
200            if arr.len() > 4 {
201                return Err(validator::ValidationError::new(
202                    "maximum 4 stop sequences allowed",
203                ));
204            }
205            for s in arr {
206                if s.is_empty() {
207                    return Err(validator::ValidationError::new(
208                        "stop sequences cannot be empty",
209                    ));
210                }
211            }
212        }
213    }
214    Ok(())
215}
216
217// ============================================================================
218// Content Parts (for multimodal messages)
219// ============================================================================
220
221#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
222#[serde(tag = "type")]
223pub enum ContentPart {
224    #[serde(rename = "text")]
225    Text { text: String },
226    #[serde(rename = "image_url")]
227    ImageUrl { image_url: ImageUrl },
228    #[serde(rename = "audio_url")]
229    AudioUrl { audio_url: AudioUrl },
230    #[serde(rename = "input_audio")]
231    InputAudio { input_audio: InputAudio },
232    #[serde(rename = "video_url")]
233    VideoUrl { video_url: VideoUrl },
234}
235
236#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
237pub struct ImageUrl {
238    pub url: String,
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub detail: Option<String>, // "auto", "low", or "high"
241}
242
243#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
244pub struct AudioUrl {
245    pub url: String,
246}
247
248#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
249pub struct InputAudio {
250    /// Base64-encoded audio bytes.
251    pub data: String,
252    /// Encoded audio format. The OpenAI Chat API supports `wav` and `mp3`.
253    pub format: String,
254}
255
256#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
257pub struct VideoUrl {
258    pub url: String,
259}
260
261// ============================================================================
262// Response Format (for structured outputs)
263// ============================================================================
264
265#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
266#[serde(tag = "type")]
267pub enum ResponseFormat {
268    #[serde(rename = "text")]
269    Text,
270    #[serde(rename = "json_object")]
271    JsonObject,
272    #[serde(rename = "json_schema")]
273    JsonSchema { json_schema: JsonSchemaFormat },
274}
275
276#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
277pub struct JsonSchemaFormat {
278    pub name: String,
279    pub schema: Value,
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub strict: Option<bool>,
282}
283
284// ============================================================================
285// Streaming
286// ============================================================================
287
288#[derive(Debug, Clone, Default, Deserialize, Serialize, schemars::JsonSchema)]
289pub struct StreamOptions {
290    /// Chat Completions / Completions: include usage block at end of stream.
291    #[serde(skip_serializing_if = "Option::is_none")]
292    pub include_usage: Option<bool>,
293
294    /// Chat Completions / Completions: emit a usage chunk with every streamed
295    /// delta instead of only in the final chunk.
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub continuous_usage_stats: Option<bool>,
298
299    /// Responses API: add random chars on `obfuscation` field of delta events
300    /// to normalize payload sizes. Defaults to `true` upstream when absent.
301    #[serde(skip_serializing_if = "Option::is_none")]
302    pub include_obfuscation: Option<bool>,
303
304    /// Additional fields not explicitly defined above (e.g. engine-specific
305    /// streaming options). Without this, the gateway silently drops them while
306    /// re-serializing the request for the backend.
307    #[serde(flatten)]
308    pub other: Map<String, Value>,
309}
310
311#[serde_with::skip_serializing_none]
312#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
313pub struct ToolCallDelta {
314    pub index: u32,
315    pub id: Option<String>,
316    #[serde(rename = "type")]
317    pub tool_type: Option<String>,
318    pub function: Option<FunctionCallDelta>,
319}
320
321#[serde_with::skip_serializing_none]
322#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
323pub struct FunctionCallDelta {
324    pub name: Option<String>,
325    pub arguments: Option<String>,
326}
327
328// ============================================================================
329// Tools and Function Calling
330// ============================================================================
331
332/// Tool choice value for simple string options
333#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
334#[serde(rename_all = "snake_case")]
335pub enum ToolChoiceValue {
336    Auto,
337    Required,
338    None,
339}
340
341/// Tool choice for both Chat Completion and Responses APIs
342#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
343#[serde(untagged)]
344pub enum ToolChoice {
345    Value(ToolChoiceValue),
346    Function {
347        #[serde(rename = "type")]
348        tool_type: String, // "function"
349        function: FunctionChoice,
350    },
351    AllowedTools {
352        #[serde(rename = "type")]
353        tool_type: String, // "allowed_tools"
354        mode: String, // "auto" | "required" TODO: need validation
355        tools: Vec<ToolReference>,
356    },
357}
358
359impl Default for ToolChoice {
360    fn default() -> Self {
361        Self::Value(ToolChoiceValue::Auto)
362    }
363}
364
365impl ToolChoice {
366    /// Serialize tool_choice to string for ResponsesResponse
367    ///
368    /// Returns the JSON-serialized tool_choice or "auto" as default
369    pub fn serialize_to_string(tool_choice: Option<&ToolChoice>) -> String {
370        tool_choice
371            .map(|tc| serde_json::to_string(tc).unwrap_or_else(|_| "auto".to_string()))
372            .unwrap_or_else(|| "auto".to_string())
373    }
374}
375
376/// Function choice specification for ToolChoice::Function
377#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
378pub struct FunctionChoice {
379    pub name: String,
380}
381
382/// Tool reference for ToolChoice::AllowedTools
383///
384/// Represents a reference to a specific tool in the allowed_tools array.
385/// Different tool types have different required fields.
386#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
387#[serde(tag = "type")]
388#[serde(rename_all = "snake_case")]
389pub enum ToolReference {
390    /// Reference to a function tool
391    #[serde(rename = "function")]
392    Function { name: String },
393
394    /// Reference to an MCP tool
395    #[serde(rename = "mcp")]
396    Mcp {
397        server_label: String,
398        #[serde(skip_serializing_if = "Option::is_none")]
399        name: Option<String>,
400    },
401
402    /// File search hosted tool
403    #[serde(rename = "file_search")]
404    FileSearch,
405
406    /// Web search preview hosted tool
407    #[serde(rename = "web_search_preview")]
408    WebSearchPreview,
409
410    /// Computer use preview hosted tool
411    #[serde(rename = "computer_use_preview")]
412    ComputerUsePreview,
413
414    /// Code interpreter hosted tool
415    #[serde(rename = "code_interpreter")]
416    CodeInterpreter,
417
418    /// Image generation hosted tool
419    #[serde(rename = "image_generation")]
420    ImageGeneration,
421}
422
423impl ToolReference {
424    /// Get a unique identifier for this tool reference
425    pub fn identifier(&self) -> String {
426        match self {
427            ToolReference::Function { name } => format!("function:{name}"),
428            ToolReference::Mcp { server_label, name } => {
429                if let Some(n) = name {
430                    format!("mcp:{server_label}:{n}")
431                } else {
432                    format!("mcp:{server_label}")
433                }
434            }
435            ToolReference::FileSearch => "file_search".to_string(),
436            ToolReference::WebSearchPreview => "web_search_preview".to_string(),
437            ToolReference::ComputerUsePreview => "computer_use_preview".to_string(),
438            ToolReference::CodeInterpreter => "code_interpreter".to_string(),
439            ToolReference::ImageGeneration => "image_generation".to_string(),
440        }
441    }
442
443    /// Get the tool name if this is a function tool
444    pub fn function_name(&self) -> Option<&str> {
445        match self {
446            ToolReference::Function { name } => Some(name.as_str()),
447            _ => None,
448        }
449    }
450}
451
452#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
453pub struct Tool {
454    #[serde(rename = "type")]
455    pub tool_type: String, // "function"
456    pub function: Function,
457}
458
459/// Per the OpenAI spec, omitting `parameters` defines a function with an
460/// empty parameter list, so a missing field deserializes to an empty schema.
461fn empty_parameters_schema() -> Value {
462    Value::Object(Map::new())
463}
464
465#[serde_with::skip_serializing_none]
466#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
467pub struct Function {
468    pub name: String,
469    pub description: Option<String>,
470    #[serde(default = "empty_parameters_schema")]
471    pub parameters: Value, // JSON Schema
472    /// Whether to enable strict schema adherence (OpenAI structured outputs)
473    pub strict: Option<bool>,
474}
475
476#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
477pub struct ToolCall {
478    pub id: String,
479    #[serde(rename = "type")]
480    pub tool_type: String, // "function"
481    pub function: FunctionCallResponse,
482}
483
484/// Deprecated `function_call` field from the OpenAI API.
485/// Can be `"none"`, `"auto"`, or `{"name": "function_name"}`.
486#[derive(Debug, Clone)]
487pub enum FunctionCall {
488    None,
489    Auto,
490    Function { name: String },
491}
492
493impl Serialize for FunctionCall {
494    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
495        match self {
496            FunctionCall::None => serializer.serialize_str("none"),
497            FunctionCall::Auto => serializer.serialize_str("auto"),
498            FunctionCall::Function { name } => {
499                use serde::ser::SerializeMap;
500                let mut map = serializer.serialize_map(Some(1))?;
501                map.serialize_entry("name", name)?;
502                map.end()
503            }
504        }
505    }
506}
507
508impl<'de> Deserialize<'de> for FunctionCall {
509    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
510        let value = Value::deserialize(deserializer)?;
511        match &value {
512            Value::String(s) => match s.as_str() {
513                "none" => Ok(FunctionCall::None),
514                "auto" => Ok(FunctionCall::Auto),
515                other => Err(de::Error::custom(format!(
516                    "unknown function_call value: \"{other}\""
517                ))),
518            },
519            Value::Object(map) => {
520                if let Some(Value::String(name)) = map.get("name") {
521                    Ok(FunctionCall::Function { name: name.clone() })
522                } else {
523                    Err(de::Error::custom(
524                        "function_call object must have a \"name\" string field",
525                    ))
526                }
527            }
528            _ => Err(de::Error::custom(
529                "function_call must be a string or object",
530            )),
531        }
532    }
533}
534
535impl schemars::JsonSchema for FunctionCall {
536    fn schema_name() -> std::borrow::Cow<'static, str> {
537        "FunctionCall".into()
538    }
539    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
540        // FunctionCall is either "none", "auto", or {"name": "..."}
541        let name_schema = generator.subschema_for::<String>();
542        schemars::json_schema!({
543            "anyOf": [
544                {
545                    "type": "string",
546                    "enum": ["none", "auto"]
547                },
548                {
549                    "type": "object",
550                    "properties": { "name": name_schema },
551                    "required": ["name"]
552                }
553            ]
554        })
555    }
556}
557
558#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
559pub struct FunctionCallResponse {
560    pub name: String,
561    #[serde(default)]
562    pub arguments: Option<String>, // JSON string
563}
564
565// ============================================================================
566// Usage and Logging
567// ============================================================================
568#[serde_with::skip_serializing_none]
569#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
570pub struct Usage {
571    pub prompt_tokens: u32,
572    pub completion_tokens: u32,
573    pub total_tokens: u32,
574    pub prompt_tokens_details: Option<PromptTokenUsageInfo>,
575    pub completion_tokens_details: Option<CompletionTokensDetails>,
576}
577
578impl Usage {
579    /// Create a Usage from prompt and completion token counts
580    pub fn from_counts(prompt_tokens: u32, completion_tokens: u32) -> Self {
581        Self {
582            prompt_tokens,
583            completion_tokens,
584            total_tokens: prompt_tokens + completion_tokens,
585            prompt_tokens_details: None,
586            completion_tokens_details: None,
587        }
588    }
589
590    /// Add cached token details to this Usage
591    pub fn with_cached_tokens(mut self, cached_tokens: u32) -> Self {
592        // Calling this builder means the backend supplied cache accounting.
593        // Zero is therefore evidence of a cold miss, not absence of support,
594        // and must remain distinguishable from `prompt_tokens_details: None`.
595        self.prompt_tokens_details = Some(PromptTokenUsageInfo { cached_tokens });
596        self
597    }
598
599    /// Add reasoning token details to this Usage
600    pub fn with_reasoning_tokens(mut self, reasoning_tokens: u32) -> Self {
601        if reasoning_tokens > 0 {
602            self.completion_tokens_details = Some(CompletionTokensDetails {
603                reasoning_tokens: Some(reasoning_tokens),
604                accepted_prediction_tokens: None,
605                rejected_prediction_tokens: None,
606            });
607        }
608        self
609    }
610}
611
612#[serde_with::skip_serializing_none]
613#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
614pub struct CompletionTokensDetails {
615    pub reasoning_tokens: Option<u32>,
616    pub accepted_prediction_tokens: Option<u32>,
617    pub rejected_prediction_tokens: Option<u32>,
618}
619
620/// Usage information (used by rerank and other endpoints)
621#[serde_with::skip_serializing_none]
622#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
623pub struct UsageInfo {
624    pub prompt_tokens: u32,
625    pub completion_tokens: u32,
626    pub total_tokens: u32,
627    pub reasoning_tokens: Option<u32>,
628    pub prompt_tokens_details: Option<PromptTokenUsageInfo>,
629}
630
631#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
632pub struct PromptTokenUsageInfo {
633    pub cached_tokens: u32,
634}
635
636#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
637pub struct LogProbs {
638    pub tokens: Vec<String>,
639    pub token_logprobs: Vec<Option<f32>>,
640    pub top_logprobs: Vec<Option<HashMap<String, f32>>>,
641    pub text_offset: Vec<u32>,
642}
643
644#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
645#[serde(untagged)]
646pub enum ChatLogProbs {
647    Detailed {
648        #[serde(skip_serializing_if = "Option::is_none")]
649        content: Option<Vec<ChatLogProbsContent>>,
650    },
651    Raw(Value),
652}
653
654#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
655pub struct ChatLogProbsContent {
656    pub token: String,
657    pub logprob: f32,
658    pub bytes: Option<Vec<u8>>,
659    pub top_logprobs: Vec<TopLogProb>,
660}
661
662#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
663pub struct TopLogProb {
664    pub token: String,
665    pub logprob: f32,
666    pub bytes: Option<Vec<u8>>,
667}
668
669// ============================================================================
670// Error Types
671// ============================================================================
672
673#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
674pub struct ErrorResponse {
675    pub error: ErrorDetail,
676}
677
678#[serde_with::skip_serializing_none]
679#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
680pub struct ErrorDetail {
681    pub message: String,
682    #[serde(rename = "type")]
683    pub error_type: String,
684    pub param: Option<String>,
685    pub code: Option<String>,
686}
687
688// ============================================================================
689// Input Types
690// ============================================================================
691
692#[derive(Debug, Clone, Serialize, schemars::JsonSchema)]
693#[serde(untagged)]
694pub enum InputIds {
695    Single(Vec<i32>),
696    Batch(Vec<Vec<i32>>),
697}
698
699/// Shape probe for the first `input_ids` element: it alone picks the variant,
700/// so the rest parses in place instead of through untagged-enum buffering.
701enum FirstInputId {
702    Id(i32),
703    Ids(Vec<i32>),
704}
705
706impl<'de> Deserialize<'de> for FirstInputId {
707    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
708    where
709        D: Deserializer<'de>,
710    {
711        struct FirstInputIdVisitor;
712
713        impl<'de> Visitor<'de> for FirstInputIdVisitor {
714            type Value = FirstInputId;
715
716            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
717                f.write_str("a token id or an array of token ids")
718            }
719
720            fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
721                i32::try_from(v)
722                    .map(FirstInputId::Id)
723                    .map_err(|_| E::invalid_value(de::Unexpected::Signed(v), &self))
724            }
725
726            fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
727                i32::try_from(v)
728                    .map(FirstInputId::Id)
729                    .map_err(|_| E::invalid_value(de::Unexpected::Unsigned(v), &self))
730            }
731
732            fn visit_seq<A: SeqAccess<'de>>(self, seq: A) -> Result<Self::Value, A::Error> {
733                Deserialize::deserialize(SeqAccessDeserializer::new(seq)).map(FirstInputId::Ids)
734            }
735        }
736
737        deserializer.deserialize_any(FirstInputIdVisitor)
738    }
739}
740
741impl<'de> Deserialize<'de> for InputIds {
742    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
743    where
744        D: Deserializer<'de>,
745    {
746        struct InputIdsVisitor;
747
748        impl<'de> Visitor<'de> for InputIdsVisitor {
749            type Value = InputIds;
750
751            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
752                f.write_str("an array of token ids or an array of token id arrays")
753            }
754
755            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
756            where
757                A: SeqAccess<'de>,
758            {
759                // An empty array is Single, matching untagged first-match order.
760                let Some(first) = seq.next_element::<FirstInputId>()? else {
761                    return Ok(InputIds::Single(Vec::new()));
762                };
763                let remaining = seq.size_hint().unwrap_or(0);
764                match first {
765                    FirstInputId::Id(id) => {
766                        let mut ids = Vec::with_capacity(remaining.saturating_add(1));
767                        ids.push(id);
768                        while let Some(id) = seq.next_element()? {
769                            ids.push(id);
770                        }
771                        Ok(InputIds::Single(ids))
772                    }
773                    FirstInputId::Ids(head) => {
774                        let mut seqs = Vec::with_capacity(remaining.saturating_add(1));
775                        seqs.push(head);
776                        while let Some(ids) = seq.next_element()? {
777                            seqs.push(ids);
778                        }
779                        Ok(InputIds::Batch(seqs))
780                    }
781                }
782            }
783        }
784
785        deserializer.deserialize_seq(InputIdsVisitor)
786    }
787}
788
789/// LoRA adapter path - can be single path or batch of paths (SGLang extension)
790#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
791#[serde(untagged)]
792pub enum LoRAPath {
793    Single(Option<String>),
794    Batch(Vec<Option<String>>),
795}
796
797// ============================================================================
798// Redacted Types
799// ============================================================================
800#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)]
801pub struct Redacted(pub String);
802
803impl std::fmt::Debug for Redacted {
804    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
805        f.write_str("[REDACTED]")
806    }
807}
808
809// ============================================================================
810// Response Prompt
811// ============================================================================
812
813/// Reference to a prompt template and its variables.
814#[serde_with::skip_serializing_none]
815#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
816pub struct ResponsePrompt {
817    pub id: String,
818    pub variables: Option<HashMap<String, PromptVariable>>,
819    pub version: Option<String>,
820}
821
822/// A prompt variable value: plain string or a typed input (text, image, file).
823///
824/// Variant order matters for `#[serde(untagged)]`: a bare JSON string succeeds
825/// as `String`; a JSON object falls through to `Typed`.
826#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
827#[serde(untagged)]
828pub enum PromptVariable {
829    String(String),
830    Typed(PromptVariableTyped),
831}
832
833/// Typed prompt variable input.
834#[serde_with::skip_serializing_none]
835#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
836#[serde(tag = "type")]
837#[expect(
838    clippy::enum_variant_names,
839    reason = "variant names match OpenAI API spec"
840)]
841pub enum PromptVariableTyped {
842    #[serde(rename = "input_text")]
843    ResponseInputText { text: String },
844    #[serde(rename = "input_image")]
845    ResponseInputImage {
846        detail: Option<Detail>,
847        file_id: Option<String>,
848        image_url: Option<String>,
849    },
850    #[serde(rename = "input_file")]
851    ResponseInputFile {
852        file_data: Option<String>,
853        file_id: Option<String>,
854        file_url: Option<String>,
855        filename: Option<String>,
856    },
857}
858
859/// Image detail level for [`PromptVariableTyped::ResponseInputImage`] and
860/// [`crate::responses::ResponseContentPart::InputImage`]. Spec allows
861/// `"low" | "high" | "auto" | "original"`.
862#[derive(Debug, Clone, Serialize, Deserialize, Default, schemars::JsonSchema)]
863#[serde(rename_all = "snake_case")]
864pub enum Detail {
865    Low,
866    High,
867    #[default]
868    Auto,
869    Original,
870}
871
872// ============================================================================
873// Responses API: prompt-cache retention & context management
874// ============================================================================
875
876/// Retention policy for prompt-cache entries on the Responses API.
877///
878/// Spec: `prompt_cache_retention: "in-memory" | "24h"`.
879#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
880pub enum PromptCacheRetention {
881    #[serde(rename = "in-memory")]
882    InMemory,
883    #[serde(rename = "24h")]
884    Duration24h,
885}
886
887/// A single entry in the Responses API `context_management` array.
888///
889/// Spec: each entry has `type` (currently only `"compaction"`) and an optional
890/// `compact_threshold` token count.
891#[serde_with::skip_serializing_none]
892#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
893pub struct ContextManagementEntry {
894    #[serde(rename = "type")]
895    pub r#type: ContextManagementType,
896    pub compact_threshold: Option<u32>,
897}
898
899/// Type tag for [`ContextManagementEntry`]. Currently only `compaction` is
900/// defined by the spec; the enum is kept small so unknown values serde-fail
901/// (consistent with P5's fail-fast direction).
902#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
903#[serde(rename_all = "snake_case")]
904pub enum ContextManagementType {
905    Compaction,
906}
907
908// ============================================================================
909// Responses API: conversation reference
910// ============================================================================
911
912/// Reference to a conversation the response belongs to.
913///
914/// Spec: `conversation: string | ResponseConversationParam { id: string }`.
915/// Variant order matters for `#[serde(untagged)]`: a bare JSON string succeeds
916/// as `Id`; an object falls through to `Object`.
917#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
918#[serde(untagged)]
919pub enum ConversationRef {
920    Id(String),
921    Object { id: String },
922}
923
924impl ConversationRef {
925    /// Return the underlying conversation id regardless of the wire shape.
926    pub fn as_id(&self) -> &str {
927        match self {
928            Self::Id(id) | Self::Object { id } => id.as_str(),
929        }
930    }
931
932    /// `true` when the underlying conversation id is the empty string.
933    /// Mirrors `String::is_empty` for callers that previously treated
934    /// `Option<String>` empty values as "unset".
935    pub fn is_empty(&self) -> bool {
936        self.as_id().is_empty()
937    }
938}
939
940#[cfg(test)]
941mod tests {
942    use serde::Deserialize;
943    use serde_json::json;
944
945    use super::*;
946
947    #[derive(Deserialize)]
948    struct NullableBoolTest {
949        #[serde(default, deserialize_with = "deserialize_null_as_false")]
950        field: bool,
951    }
952
953    #[test]
954    fn test_deserialize_null_as_false() {
955        let cases = [
956            (json!({"field": true}), true),
957            (json!({"field": false}), false),
958            (json!({"field": null}), false),
959            (json!({}), false),
960        ];
961        for (input, expected) in cases {
962            let t: NullableBoolTest = serde_json::from_value(input).unwrap();
963            assert_eq!(t.field, expected);
964        }
965    }
966
967    #[test]
968    fn test_deserialize_null_as_false_rejects_non_bool() {
969        let result = serde_json::from_value::<NullableBoolTest>(json!({"field": "yes"}));
970        assert!(result.is_err());
971    }
972
973    #[test]
974    fn stream_options_preserve_unknown_fields() {
975        let raw = json!({
976            "include_usage": true,
977            "continuous_usage_stats": true,
978            "step_usage_chunks": "all",
979            "engine_specific": {"nested": 1},
980        });
981
982        let opts: StreamOptions = serde_json::from_value(raw.clone()).unwrap();
983        assert_eq!(opts.include_usage, Some(true));
984        assert_eq!(opts.continuous_usage_stats, Some(true));
985        assert_eq!(opts.other.get("step_usage_chunks"), Some(&json!("all")));
986
987        // Re-serializing must round-trip the engine-specific keys, otherwise the
988        // gateway would strip them on the way to the backend.
989        assert_eq!(serde_json::to_value(&opts).unwrap(), raw);
990    }
991
992    #[test]
993    fn stream_options_without_unknown_fields_stay_compact() {
994        let opts: StreamOptions = serde_json::from_value(json!({"include_usage": true})).unwrap();
995        assert!(opts.other.is_empty());
996        assert_eq!(
997            serde_json::to_value(&opts).unwrap(),
998            json!({"include_usage": true})
999        );
1000    }
1001
1002    #[test]
1003    fn cached_token_builder_preserves_explicit_zero() {
1004        let usage = Usage::from_counts(16, 1).with_cached_tokens(0);
1005        assert!(matches!(
1006            usage.prompt_tokens_details,
1007            Some(PromptTokenUsageInfo { cached_tokens: 0 })
1008        ));
1009    }
1010
1011    #[test]
1012    fn content_part_deserializes_audio_url() {
1013        let value = json!({
1014            "type": "audio_url",
1015            "audio_url": {
1016                "url": "https://example.com/audio.wav"
1017            }
1018        });
1019        let part: ContentPart = serde_json::from_value(value).expect("audio_url content part");
1020        assert_eq!(
1021            part,
1022            ContentPart::AudioUrl {
1023                audio_url: AudioUrl {
1024                    url: "https://example.com/audio.wav".to_string(),
1025                },
1026            }
1027        );
1028    }
1029
1030    #[test]
1031    fn content_part_round_trips_input_audio() {
1032        let value = json!({
1033            "type": "input_audio",
1034            "input_audio": {
1035                "data": "UklGRg==",
1036                "format": "wav"
1037            }
1038        });
1039        let part: ContentPart =
1040            serde_json::from_value(value.clone()).expect("input_audio content part");
1041        assert_eq!(
1042            part,
1043            ContentPart::InputAudio {
1044                input_audio: InputAudio {
1045                    data: "UklGRg==".to_string(),
1046                    format: "wav".to_string(),
1047                },
1048            }
1049        );
1050        assert_eq!(serde_json::to_value(part).unwrap(), value);
1051    }
1052
1053    #[test]
1054    fn conversation_ref_deserializes_bare_string() {
1055        let v = json!("conv_abc");
1056        let r: ConversationRef = serde_json::from_value(v).expect("string form");
1057        assert!(matches!(r, ConversationRef::Id(ref s) if s == "conv_abc"));
1058        assert_eq!(r.as_id(), "conv_abc");
1059        // Bare string round-trips back to a JSON string.
1060        assert_eq!(serde_json::to_value(&r).unwrap(), json!("conv_abc"));
1061    }
1062
1063    #[test]
1064    fn conversation_ref_deserializes_object() {
1065        let v = json!({"id": "conv_xyz"});
1066        let r: ConversationRef = serde_json::from_value(v).expect("object form");
1067        assert!(matches!(r, ConversationRef::Object { ref id } if id == "conv_xyz"));
1068        assert_eq!(r.as_id(), "conv_xyz");
1069        // Object round-trips back to an object.
1070        assert_eq!(serde_json::to_value(&r).unwrap(), json!({"id": "conv_xyz"}));
1071    }
1072
1073    #[test]
1074    fn conversation_ref_is_empty() {
1075        assert!(ConversationRef::Id(String::new()).is_empty());
1076        assert!(!ConversationRef::Id("conv_1".to_string()).is_empty());
1077        assert!(ConversationRef::Object { id: String::new() }.is_empty());
1078    }
1079
1080    #[test]
1081    fn input_ids_deserializes_single() {
1082        let ids: InputIds = serde_json::from_str("[1, -2, 3]").unwrap();
1083        assert!(matches!(ids, InputIds::Single(ref v) if v == &[1, -2, 3]));
1084    }
1085
1086    #[test]
1087    fn input_ids_deserializes_batch() {
1088        let ids: InputIds = serde_json::from_str("[[1, 2], [3], []]").unwrap();
1089        assert!(matches!(ids, InputIds::Batch(ref v) if v == &[vec![1, 2], vec![3], vec![]]));
1090    }
1091
1092    #[test]
1093    fn input_ids_empty_array_is_single() {
1094        let ids: InputIds = serde_json::from_str("[]").unwrap();
1095        assert!(matches!(ids, InputIds::Single(ref v) if v.is_empty()));
1096    }
1097
1098    #[test]
1099    fn input_ids_rejects_invalid_input() {
1100        for input in [
1101            "[1, [2]]",
1102            "[[1], 2]",
1103            "[\"a\"]",
1104            "[1.5]",
1105            "[5000000000]",
1106            "\"nope\"",
1107            "null",
1108            "7",
1109        ] {
1110            assert!(
1111                serde_json::from_str::<InputIds>(input).is_err(),
1112                "accepted {input}"
1113            );
1114        }
1115    }
1116
1117    #[test]
1118    fn input_ids_round_trip() {
1119        for input in [json!([1, 2, 3]), json!([[1, 2], [3]]), json!([])] {
1120            let ids: InputIds = serde_json::from_value(input.clone()).unwrap();
1121            assert_eq!(serde_json::to_value(&ids).unwrap(), input);
1122        }
1123    }
1124
1125    #[test]
1126    fn function_deserializes_without_parameters() {
1127        // Per the OpenAI spec, omitting `parameters` defines a function with
1128        // an empty parameter list.
1129        let value = json!({"name": "web_search", "description": ""});
1130        let function: Function = serde_json::from_value(value).expect("parameterless function");
1131        assert_eq!(function.parameters, json!({}));
1132    }
1133}