potato_type/openai/
chat.rs

1use crate::error::TypeError;
2use crate::SettingsType;
3use potato_util::json_to_pydict;
4use potato_util::{pyobject_to_json, PyHelperFuncs, UtilError};
5use pyo3::prelude::*;
6use pyo3::types::PyDict;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12#[pyclass]
13pub struct AudioParam {
14    #[pyo3(get, set)]
15    pub format: String,
16    #[pyo3(get, set)]
17    pub voice: String,
18}
19
20#[pymethods]
21impl AudioParam {
22    #[new]
23    pub fn new(format: String, voice: String) -> Self {
24        AudioParam { format, voice }
25    }
26
27    pub fn model_dump<'py>(&self, py: Python<'py>) -> Result<Bound<'py, PyDict>, TypeError> {
28        // iterate over each field in model_settings and add to the dict if it is not None
29        let pydict = PyDict::new(py);
30        pydict.set_item("format", &self.format)?;
31        pydict.set_item("voice", &self.voice)?;
32        Ok(pydict)
33    }
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
37#[pyclass]
38pub struct ContentPart {
39    #[pyo3(get, set)]
40    pub r#type: String,
41    #[pyo3(get, set)]
42    pub text: String,
43}
44
45#[pymethods]
46impl ContentPart {
47    #[new]
48    pub fn new(r#type: String, text: String) -> Self {
49        ContentPart { r#type, text }
50    }
51
52    pub fn model_dump<'py>(&self, py: Python<'py>) -> Result<Bound<'py, PyDict>, TypeError> {
53        // iterate over each field in model_settings and add to the dict if it is not None
54        let pydict = PyDict::new(py);
55
56        pydict.set_item("type", &self.r#type)?;
57        pydict.set_item("text", &self.text)?;
58        Ok(pydict)
59    }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
63#[serde(untagged)]
64#[pyclass]
65pub enum Content {
66    Text(String),
67    Array(Vec<ContentPart>),
68}
69
70#[pymethods]
71impl Content {
72    #[new]
73    #[pyo3(signature = (text=None, parts=None))]
74    pub fn new(text: Option<String>, parts: Option<Vec<ContentPart>>) -> Result<Self, TypeError> {
75        match (text, parts) {
76            (Some(t), None) => Ok(Content::Text(t)),
77            (None, Some(p)) => Ok(Content::Array(p)),
78            _ => Err(TypeError::InvalidInput(
79                "Either text or parts must be provided, but not both.".to_string(),
80            )),
81        }
82    }
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
86#[pyclass]
87pub struct Prediction {
88    #[pyo3(get, set)]
89    pub r#type: String,
90    #[pyo3(get, set)]
91    pub content: Content,
92}
93
94#[pymethods]
95impl Prediction {
96    #[new]
97    pub fn new(r#type: String, content: Content) -> Self {
98        Prediction { r#type, content }
99    }
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
103#[pyclass]
104pub struct StreamOptions {
105    #[pyo3(get, set)]
106    pub include_obfuscation: Option<bool>,
107    #[pyo3(get, set)]
108    pub include_usage: Option<bool>,
109}
110
111#[pymethods]
112impl StreamOptions {
113    #[new]
114    pub fn new(include_obfuscation: Option<bool>, include_usage: Option<bool>) -> Self {
115        StreamOptions {
116            include_obfuscation,
117            include_usage,
118        }
119    }
120}
121
122#[pyclass]
123#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
124#[serde(rename_all = "lowercase")]
125pub enum ToolChoiceMode {
126    #[serde(rename = "none")]
127    NA,
128    Auto,
129    Required,
130}
131
132#[pymethods]
133impl ToolChoiceMode {
134    fn __str__(&self) -> String {
135        PyHelperFuncs::__str__(self)
136    }
137}
138
139/// Function specification for function tool choice
140#[pyclass]
141#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
142pub struct FunctionChoice {
143    #[pyo3(get, set)]
144    pub name: String,
145}
146
147#[pymethods]
148impl FunctionChoice {
149    #[new]
150    #[pyo3(signature = (name))]
151    pub fn new(name: String) -> Self {
152        Self { name }
153    }
154
155    fn __str__(&self) -> String {
156        PyHelperFuncs::__str__(self)
157    }
158}
159
160/// Function tool choice specification
161#[pyclass]
162#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
163pub struct FunctionToolChoice {
164    #[pyo3(get)]
165    pub r#type: String,
166    #[pyo3(get, set)]
167    pub function: FunctionChoice,
168}
169
170#[pymethods]
171impl FunctionToolChoice {
172    #[new]
173    #[pyo3(signature = (function))]
174    pub fn new(function: FunctionChoice) -> Self {
175        Self {
176            r#type: "function".to_string(),
177            function,
178        }
179    }
180
181    fn __str__(&self) -> String {
182        PyHelperFuncs::__str__(self)
183    }
184}
185
186/// Custom tool specification
187#[pyclass]
188#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
189pub struct CustomChoice {
190    #[pyo3(get, set)]
191    pub name: String,
192}
193
194#[pymethods]
195impl CustomChoice {
196    #[new]
197    #[pyo3(signature = (name))]
198    pub fn new(name: String) -> Self {
199        Self { name }
200    }
201
202    fn __str__(&self) -> String {
203        PyHelperFuncs::__str__(self)
204    }
205}
206
207/// Custom tool choice specification
208#[pyclass]
209#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
210pub struct CustomToolChoice {
211    #[pyo3(get)]
212    pub r#type: String,
213    #[pyo3(get, set)]
214    pub custom: CustomChoice,
215}
216
217#[pymethods]
218impl CustomToolChoice {
219    #[new]
220    #[pyo3(signature = (custom))]
221    pub fn new(custom: CustomChoice) -> Self {
222        Self {
223            r#type: "custom".to_string(),
224            custom,
225        }
226    }
227
228    fn __str__(&self) -> String {
229        PyHelperFuncs::__str__(self)
230    }
231}
232
233#[pyclass]
234#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
235pub struct ToolDefinition {
236    #[pyo3(get)]
237    pub r#type: String,
238    #[pyo3(get, set)]
239    pub function: FunctionChoice,
240}
241
242#[pymethods]
243impl ToolDefinition {
244    #[new]
245    #[pyo3(signature = (function_name))]
246    pub fn new(function_name: String) -> Self {
247        Self {
248            r#type: "function".to_string(),
249            function: FunctionChoice::new(function_name),
250        }
251    }
252
253    fn __str__(&self) -> String {
254        PyHelperFuncs::__str__(self)
255    }
256}
257
258/// Mode for allowed tools constraint
259#[pyclass]
260#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
261#[serde(rename_all = "lowercase")]
262pub enum AllowedToolsMode {
263    /// Model can pick from allowed tools or generate a message
264    Auto,
265    /// Model must call one or more of the allowed tools
266    Required,
267}
268
269#[pymethods]
270impl AllowedToolsMode {
271    fn __str__(&self) -> String {
272        PyHelperFuncs::__str__(self)
273    }
274}
275
276#[pyclass]
277#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
278pub struct InnerAllowedTools {
279    #[pyo3(get)]
280    pub mode: AllowedToolsMode,
281    #[pyo3(get)]
282    pub tools: Vec<ToolDefinition>,
283}
284
285#[pyclass]
286#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
287pub struct AllowedTools {
288    #[pyo3(get)]
289    pub r#type: String,
290    #[pyo3(get)]
291    pub allowed_tools: InnerAllowedTools,
292}
293
294#[pymethods]
295impl AllowedTools {
296    #[new]
297    #[pyo3(signature = (mode, tools))]
298    pub fn new(mode: AllowedToolsMode, tools: Vec<ToolDefinition>) -> Self {
299        Self {
300            r#type: "allowed_tools".to_string(),
301            allowed_tools: InnerAllowedTools { mode, tools },
302        }
303    }
304
305    /// Create AllowedTools from function names
306    #[staticmethod]
307    #[pyo3(signature = (mode, function_names))]
308    pub fn from_function_names(mode: AllowedToolsMode, function_names: Vec<String>) -> Self {
309        let tools = function_names
310            .into_iter()
311            .map(ToolDefinition::new)
312            .collect();
313
314        Self::new(mode, tools)
315    }
316
317    fn __str__(&self) -> String {
318        PyHelperFuncs::__str__(self)
319    }
320}
321
322/// Tool choice configuration - can be a mode string or specific tool object
323#[pyclass]
324#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
325#[serde(untagged)]
326pub enum ToolChoice {
327    /// Simple mode selection (none, auto, required)
328    Mode(ToolChoiceMode),
329    /// Specific function tool choice
330    Function(FunctionToolChoice),
331    /// Custom tool choice
332    Custom(CustomToolChoice),
333    /// Allowed tools configuration
334    Allowed(AllowedTools),
335}
336
337#[pymethods]
338impl ToolChoice {
339    /// Create tool choice from mode string
340    #[staticmethod]
341    #[pyo3(signature = (mode))]
342    pub fn from_mode(mode: &ToolChoiceMode) -> Self {
343        ToolChoice::Mode(mode.clone())
344    }
345
346    /// Create tool choice for specific function
347    #[staticmethod]
348    #[pyo3(signature = (function_name))]
349    pub fn from_function(function_name: String) -> Self {
350        ToolChoice::Function(FunctionToolChoice::new(FunctionChoice::new(function_name)))
351    }
352
353    /// Create tool choice for custom tool
354    #[staticmethod]
355    #[pyo3(signature = (custom_name))]
356    pub fn from_custom(custom_name: String) -> Self {
357        ToolChoice::Custom(CustomToolChoice::new(CustomChoice::new(custom_name)))
358    }
359
360    /// Create allowed tools configuration
361    #[staticmethod]
362    #[pyo3(signature = (allowed_tools))]
363    pub fn from_allowed_tools(allowed_tools: AllowedTools) -> Self {
364        ToolChoice::Allowed(allowed_tools)
365    }
366
367    fn __str__(&self) -> String {
368        PyHelperFuncs::__str__(self)
369    }
370}
371
372impl Default for ToolChoice {
373    fn default() -> Self {
374        ToolChoice::Mode(ToolChoiceMode::Auto)
375    }
376}
377
378#[pyclass]
379#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
380pub struct FunctionDefinition {
381    #[pyo3(get, set)]
382    pub name: String,
383
384    #[pyo3(get, set)]
385    #[serde(skip_serializing_if = "Option::is_none")]
386    pub description: Option<String>,
387
388    #[serde(skip_serializing_if = "Option::is_none")]
389    pub parameters: Option<Value>,
390
391    #[pyo3(get, set)]
392    #[serde(skip_serializing_if = "Option::is_none")]
393    pub strict: Option<bool>,
394}
395
396#[pymethods]
397impl FunctionDefinition {
398    #[new]
399    #[pyo3(signature = (name, description=None, parameters=None, strict=None))]
400    pub fn new(
401        name: String,
402        description: Option<String>,
403        parameters: Option<&Bound<'_, PyAny>>,
404        strict: Option<bool>,
405    ) -> Result<Self, UtilError> {
406        let params = match parameters {
407            Some(obj) => Some(pyobject_to_json(obj)?),
408            None => None,
409        };
410
411        Ok(Self {
412            name,
413            description,
414            parameters: params,
415            strict,
416        })
417    }
418
419    fn __str__(&self) -> String {
420        PyHelperFuncs::__str__(self)
421    }
422}
423
424#[pyclass]
425#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
426pub struct FunctionTool {
427    #[pyo3(get, set)]
428    pub function: FunctionDefinition,
429
430    #[pyo3(get, set)]
431    pub r#type: String,
432}
433
434#[pymethods]
435impl FunctionTool {
436    #[new]
437    #[pyo3(signature = (function, r#type))]
438    pub fn new(function: FunctionDefinition, r#type: String) -> Self {
439        FunctionTool { function, r#type }
440    }
441}
442
443#[pyclass]
444#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
445pub struct TextFormat {
446    #[pyo3(get, set)]
447    pub r#type: String,
448}
449
450#[pymethods]
451impl TextFormat {
452    #[new]
453    #[pyo3(signature = (r#type))]
454    pub fn new(r#type: String) -> Self {
455        TextFormat { r#type }
456    }
457}
458
459#[pyclass]
460#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
461pub struct Grammar {
462    /// The grammar definition
463    #[pyo3(get, set)]
464    pub definition: String,
465
466    /// The syntax of the grammar definition (lark or regex)
467    #[pyo3(get, set)]
468    pub syntax: String,
469}
470
471#[pymethods]
472impl Grammar {
473    #[new]
474    #[pyo3(signature = (definition, syntax))]
475    pub fn new(definition: String, syntax: String) -> Self {
476        Self { definition, syntax }
477    }
478
479    fn __str__(&self) -> String {
480        PyHelperFuncs::__str__(self)
481    }
482}
483
484#[pyclass]
485#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
486pub struct GrammarFormat {
487    #[pyo3(get, set)]
488    pub grammar: Grammar,
489    #[pyo3(get, set)]
490    pub r#type: String,
491}
492
493#[pymethods]
494impl GrammarFormat {
495    #[new]
496    #[pyo3(signature = (grammar, r#type))]
497    pub fn new(grammar: Grammar, r#type: String) -> Self {
498        Self { grammar, r#type }
499    }
500}
501
502#[pyclass]
503#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
504#[serde(untagged)]
505pub enum CustomToolFormat {
506    /// Unconstrained free-form text
507    Text(TextFormat),
508    /// A grammar defined by the user
509    Grammar(GrammarFormat),
510}
511
512#[pymethods]
513impl CustomToolFormat {
514    #[new]
515    #[pyo3(signature = (r#type=None, grammar=None))]
516    pub fn new(r#type: Option<String>, grammar: Option<Grammar>) -> Self {
517        match (r#type, grammar) {
518            (Some(r#type), None) => CustomToolFormat::Text(TextFormat::new(r#type)),
519            (None, Some(grammar)) => {
520                CustomToolFormat::Grammar(GrammarFormat::new(grammar, String::new()))
521            }
522            _ => CustomToolFormat::Text(TextFormat::new(String::new())),
523        }
524    }
525}
526
527#[pyclass]
528#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
529pub struct CustomDefinition {
530    #[pyo3(get, set)]
531    pub name: String,
532
533    #[pyo3(get, set)]
534    #[serde(skip_serializing_if = "Option::is_none")]
535    pub description: Option<String>,
536
537    #[pyo3(get, set)]
538    #[serde(skip_serializing_if = "Option::is_none")]
539    pub format: Option<CustomToolFormat>,
540}
541
542#[pymethods]
543impl CustomDefinition {
544    #[new]
545    #[pyo3(signature = (name, description=None, format=None))]
546    pub fn new(
547        name: String,
548        description: Option<String>,
549        format: Option<CustomToolFormat>,
550    ) -> Self {
551        Self {
552            name,
553            description,
554            format,
555        }
556    }
557}
558
559#[pyclass]
560#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
561pub struct CustomTool {
562    #[pyo3(get, set)]
563    pub custom: CustomDefinition,
564
565    #[pyo3(get, set)]
566    pub r#type: String,
567}
568
569#[pymethods]
570impl CustomTool {
571    #[new]
572    #[pyo3(signature = (custom, r#type))]
573    pub fn new(custom: CustomDefinition, r#type: String) -> Self {
574        CustomTool { custom, r#type }
575    }
576}
577
578#[pyclass]
579#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
580#[serde(untagged)]
581pub enum Tool {
582    Function(FunctionTool),
583
584    Custom(CustomTool),
585}
586
587#[pymethods]
588impl Tool {
589    #[new]
590    #[pyo3(signature = (function=None, custom=None))]
591    pub fn new(
592        function: Option<FunctionTool>,
593        custom: Option<CustomTool>,
594    ) -> Result<Self, TypeError> {
595        match (function, custom) {
596            (Some(function), None) => Ok(Tool::Function(function)),
597            (None, Some(custom)) => Ok(Tool::Custom(custom)),
598            _ => Err(TypeError::InvalidInput("Invalid tool definition".into())),
599        }
600    }
601}
602
603#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
604#[pyclass]
605pub struct OpenAIChatSettings {
606    #[pyo3(get, set)]
607    #[serde(skip_serializing_if = "Option::is_none")]
608    pub max_completion_tokens: Option<usize>,
609
610    #[pyo3(get, set)]
611    #[serde(skip_serializing_if = "Option::is_none")]
612    pub temperature: Option<f32>,
613
614    #[pyo3(get, set)]
615    #[serde(skip_serializing_if = "Option::is_none")]
616    pub top_p: Option<f32>,
617
618    #[pyo3(get, set)]
619    #[serde(skip_serializing_if = "Option::is_none")]
620    pub top_k: Option<i32>,
621
622    #[pyo3(get, set)]
623    #[serde(skip_serializing_if = "Option::is_none")]
624    pub frequency_penalty: Option<f32>,
625
626    #[pyo3(get, set)]
627    #[serde(skip_serializing_if = "Option::is_none")]
628    pub timeout: Option<f32>,
629
630    #[pyo3(get, set)]
631    #[serde(skip_serializing_if = "Option::is_none")]
632    pub parallel_tool_calls: Option<bool>,
633
634    #[pyo3(get, set)]
635    #[serde(skip_serializing_if = "Option::is_none")]
636    pub seed: Option<u64>,
637
638    #[pyo3(get, set)]
639    #[serde(skip_serializing_if = "Option::is_none")]
640    pub logit_bias: Option<HashMap<String, i32>>,
641
642    #[pyo3(get, set)]
643    #[serde(skip_serializing_if = "Option::is_none")]
644    pub stop_sequences: Option<Vec<String>>,
645
646    #[pyo3(get, set)]
647    #[serde(skip_serializing_if = "Option::is_none")]
648    pub logprobs: Option<bool>,
649
650    #[pyo3(get, set)]
651    #[serde(skip_serializing_if = "Option::is_none")]
652    pub audio: Option<AudioParam>,
653
654    #[pyo3(get, set)]
655    #[serde(skip_serializing_if = "Option::is_none")]
656    pub metadata: Option<HashMap<String, String>>,
657
658    #[pyo3(get, set)]
659    #[serde(skip_serializing_if = "Option::is_none")]
660    pub modalities: Option<Vec<String>>,
661
662    #[pyo3(get, set)]
663    #[serde(skip_serializing_if = "Option::is_none")]
664    pub n: Option<usize>,
665
666    #[pyo3(get, set)]
667    #[serde(skip_serializing_if = "Option::is_none")]
668    pub prediction: Option<Prediction>,
669
670    #[pyo3(get, set)]
671    #[serde(skip_serializing_if = "Option::is_none")]
672    pub presence_penalty: Option<f32>,
673
674    #[pyo3(get, set)]
675    #[serde(skip_serializing_if = "Option::is_none")]
676    pub prompt_cache_key: Option<String>,
677
678    #[pyo3(get, set)]
679    #[serde(skip_serializing_if = "Option::is_none")]
680    pub reasoning_effort: Option<String>,
681
682    #[pyo3(get, set)]
683    #[serde(skip_serializing_if = "Option::is_none")]
684    pub safety_identifier: Option<String>,
685
686    #[pyo3(get, set)]
687    #[serde(skip_serializing_if = "Option::is_none")]
688    pub service_tier: Option<String>,
689
690    #[pyo3(get, set)]
691    #[serde(skip_serializing_if = "Option::is_none")]
692    pub store: Option<bool>,
693
694    #[pyo3(get, set)]
695    #[serde(skip_serializing_if = "Option::is_none")]
696    pub stream: Option<bool>,
697
698    #[pyo3(get, set)]
699    #[serde(skip_serializing_if = "Option::is_none")]
700    pub stream_options: Option<StreamOptions>,
701
702    #[pyo3(get, set)]
703    #[serde(skip_serializing_if = "Option::is_none")]
704    pub tool_choice: Option<ToolChoice>,
705
706    #[pyo3(get, set)]
707    #[serde(skip_serializing_if = "Option::is_none")]
708    pub tools: Option<Vec<Tool>>,
709
710    #[pyo3(get, set)]
711    #[serde(skip_serializing_if = "Option::is_none")]
712    pub top_logprobs: Option<i32>,
713
714    #[pyo3(get, set)]
715    #[serde(skip_serializing_if = "Option::is_none")]
716    pub verbosity: Option<String>,
717
718    // TODO: add web_search_arg later
719    #[serde(skip_serializing_if = "Option::is_none")]
720    pub extra_body: Option<Value>,
721}
722
723#[pymethods]
724impl OpenAIChatSettings {
725    #[new]
726    #[pyo3(signature = (max_completion_tokens=None, temperature=None, top_p=None, top_k=None, frequency_penalty=None, timeout=None, parallel_tool_calls=None, seed=None, logit_bias=None, stop_sequences=None, logprobs=None, audio=None, metadata=None, modalities=None, n=None, prediction=None, presence_penalty=None, prompt_cache_key=None, reasoning_effort=None, safety_identifier=None, service_tier=None, store=None, stream=None, stream_options=None, tool_choice=None, tools=None, top_logprobs=None, verbosity=None, extra_body=None))]
727    #[allow(clippy::too_many_arguments)]
728    pub fn new(
729        max_completion_tokens: Option<usize>,
730        temperature: Option<f32>,
731        top_p: Option<f32>,
732        top_k: Option<i32>,
733        frequency_penalty: Option<f32>,
734        timeout: Option<f32>,
735        parallel_tool_calls: Option<bool>,
736        seed: Option<u64>,
737        logit_bias: Option<HashMap<String, i32>>,
738        stop_sequences: Option<Vec<String>>,
739        logprobs: Option<bool>,
740        audio: Option<AudioParam>,
741        metadata: Option<HashMap<String, String>>,
742        modalities: Option<Vec<String>>,
743        n: Option<usize>,
744        prediction: Option<Prediction>,
745        presence_penalty: Option<f32>,
746        prompt_cache_key: Option<String>,
747        reasoning_effort: Option<String>,
748        safety_identifier: Option<String>,
749        service_tier: Option<String>,
750        store: Option<bool>,
751        stream: Option<bool>,
752        stream_options: Option<StreamOptions>,
753        tool_choice: Option<ToolChoice>,
754        tools: Option<Vec<Tool>>,
755        top_logprobs: Option<i32>,
756        verbosity: Option<String>,
757        extra_body: Option<&Bound<'_, PyAny>>,
758    ) -> Result<Self, UtilError> {
759        let extra = match extra_body {
760            Some(obj) => Some(pyobject_to_json(obj)?),
761            None => None,
762        };
763
764        Ok(OpenAIChatSettings {
765            max_completion_tokens,
766            temperature,
767            top_p,
768            top_k,
769            frequency_penalty,
770            timeout,
771            parallel_tool_calls,
772            seed,
773            logit_bias,
774            stop_sequences,
775            logprobs,
776            audio,
777            metadata,
778            modalities,
779            n,
780            prediction,
781            presence_penalty,
782            prompt_cache_key,
783            reasoning_effort,
784            safety_identifier,
785            service_tier,
786            store,
787            stream,
788            stream_options,
789            tool_choice,
790            tools,
791            top_logprobs,
792            verbosity,
793            extra_body: extra,
794        })
795    }
796
797    fn __str__(&self) -> String {
798        PyHelperFuncs::__str__(self)
799    }
800
801    #[getter]
802    pub fn extra_body<'py>(
803        &self,
804        py: Python<'py>,
805    ) -> Result<Option<Bound<'py, PyDict>>, TypeError> {
806        // error if extra body is None
807        Ok(self
808            .extra_body
809            .as_ref()
810            .map(|v| {
811                let pydict = PyDict::new(py);
812                json_to_pydict(py, v, &pydict)
813            })
814            .transpose()?)
815    }
816
817    pub fn model_dump<'py>(&self, py: Python<'py>) -> Result<Bound<'py, PyDict>, TypeError> {
818        // iterate over each field in model_settings and add to the dict if it is not None
819        let json = serde_json::to_value(self)?;
820        let pydict = PyDict::new(py);
821        json_to_pydict(py, &json, &pydict)?;
822        Ok(pydict)
823    }
824
825    pub fn settings_type(&self) -> SettingsType {
826        SettingsType::OpenAIChat
827    }
828}