1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5use super::super::common::*;
6use super::chat_stream::ChatCompletionChunk;
7use super::chat_tail::{
8 ChatAnnotation, ChatAudio, ChatAudioParam, ChatAudioRef, ChatChoiceLogprobs, ChatFileRef,
9 ChatWebSearchOptions, CompletionUsage, CustomToolCall, ImageUrl, InputAudio, PredictionContent,
10 StreamOptions,
11};
12
13pub type ChatCompletionWireModel = OpenAiWireModel<ChatCompletionRequest, ChatCompletionResponse>;
14pub type ChatCompletionStreamWireModel =
15 OpenAiWireModel<ChatCompletionRequest, ChatCompletionChunk>;
16
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18pub struct ChatCompletionRequest {
19 pub messages: Vec<ChatCompletionMessageParam>,
20 pub model: OpenAiModelId,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 pub audio: Option<ChatAudioParam>,
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub frequency_penalty: Option<f64>,
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub function_call: Option<LegacyFunctionCallChoice>,
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub functions: Option<Vec<LegacyFunctionDefinition>>,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub logit_bias: Option<LogitBias>,
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub logprobs: Option<bool>,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub max_completion_tokens: Option<u32>,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub max_tokens: Option<u32>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub metadata: Option<Metadata>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub modalities: Option<Vec<TextOrAudioModality>>,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub moderation: Option<ModerationConfig>,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub n: Option<u32>,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub parallel_tool_calls: Option<bool>,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub prediction: Option<PredictionContent>,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub presence_penalty: Option<f64>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub prompt_cache_key: Option<String>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub prompt_cache_options: Option<PromptCacheOptions>,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub prompt_cache_retention: Option<PromptCacheRetention>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub reasoning_effort: Option<ReasoningEffort>,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub response_format: Option<ChatResponseFormat>,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub safety_identifier: Option<String>,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub seed: Option<i64>,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub service_tier: Option<ServiceTier>,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub stop: Option<StringOrList>,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub store: Option<bool>,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub stream: Option<bool>,
73 #[serde(skip_serializing_if = "Option::is_none")]
74 pub stream_options: Option<StreamOptions>,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub temperature: Option<f64>,
77 #[serde(skip_serializing_if = "Option::is_none")]
78 pub tool_choice: Option<ChatToolChoice>,
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub tools: Option<Vec<ChatTool>>,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub top_logprobs: Option<u32>,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub top_p: Option<f64>,
85 #[serde(skip_serializing_if = "Option::is_none")]
86 pub user: Option<String>,
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub verbosity: Option<Verbosity>,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub web_search_options: Option<ChatWebSearchOptions>,
91 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
92 pub extra: Extra,
93}
94
95#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96#[serde(tag = "role")]
97#[allow(clippy::large_enum_variant)]
98pub enum ChatCompletionMessageParam {
99 #[serde(rename = "developer")]
100 Developer {
101 content: ChatTextContent,
102 #[serde(skip_serializing_if = "Option::is_none")]
103 name: Option<String>,
104 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
105 extra: Extra,
106 },
107 #[serde(rename = "system")]
108 System {
109 content: ChatTextContent,
110 #[serde(skip_serializing_if = "Option::is_none")]
111 name: Option<String>,
112 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
113 extra: Extra,
114 },
115 #[serde(rename = "user")]
116 User {
117 content: ChatContent,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 name: Option<String>,
120 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
121 extra: Extra,
122 },
123 #[serde(rename = "assistant")]
124 Assistant {
125 #[serde(skip_serializing_if = "Option::is_none")]
126 content: Option<ChatAssistantContent>,
127 #[serde(skip_serializing_if = "Option::is_none")]
128 audio: Option<ChatAudioRef>,
129 #[serde(skip_serializing_if = "Option::is_none")]
130 function_call: Option<FunctionCall>,
131 #[serde(skip_serializing_if = "Option::is_none")]
132 name: Option<String>,
133 #[serde(skip_serializing_if = "Option::is_none")]
134 reasoning_content: Option<String>,
135 #[serde(skip_serializing_if = "Option::is_none")]
136 refusal: Option<String>,
137 #[serde(skip_serializing_if = "Option::is_none")]
138 tool_calls: Option<Vec<ChatToolCall>>,
139 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
140 extra: Extra,
141 },
142 #[serde(rename = "tool")]
143 Tool {
144 content: ChatTextContent,
145 tool_call_id: String,
146 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
147 extra: Extra,
148 },
149 #[serde(rename = "function")]
150 Function {
151 content: String,
152 name: String,
153 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
154 extra: Extra,
155 },
156}
157
158#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
159#[serde(untagged)]
160pub enum ChatTextContent {
161 Text(String),
162 Parts(Vec<ChatTextContentPart>),
163}
164
165#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
166#[serde(tag = "type")]
167pub enum ChatTextContentPart {
168 #[serde(rename = "text")]
169 Text {
170 text: String,
171 #[serde(skip_serializing_if = "Option::is_none")]
172 prompt_cache_breakpoint: Option<PromptCacheBreakpoint>,
173 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
174 extra: Extra,
175 },
176}
177
178#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
179#[serde(untagged)]
180pub enum ChatAssistantContent {
181 Text(String),
182 Parts(Vec<ChatAssistantContentPart>),
183}
184
185#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
186#[serde(tag = "type")]
187pub enum ChatAssistantContentPart {
188 #[serde(rename = "text")]
189 Text {
190 text: String,
191 #[serde(skip_serializing_if = "Option::is_none")]
192 prompt_cache_breakpoint: Option<PromptCacheBreakpoint>,
193 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
194 extra: Extra,
195 },
196 #[serde(rename = "refusal")]
197 Refusal {
198 refusal: String,
199 #[serde(skip_serializing_if = "Option::is_none")]
200 prompt_cache_breakpoint: Option<PromptCacheBreakpoint>,
201 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
202 extra: Extra,
203 },
204}
205
206#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
207#[serde(untagged)]
208pub enum ChatContent {
209 Text(String),
210 Parts(Vec<ChatContentPart>),
211}
212
213#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
214#[serde(tag = "type")]
215pub enum ChatContentPart {
216 #[serde(rename = "text")]
217 Text {
218 text: String,
219 #[serde(skip_serializing_if = "Option::is_none")]
220 prompt_cache_breakpoint: Option<PromptCacheBreakpoint>,
221 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
222 extra: Extra,
223 },
224 #[serde(rename = "image_url")]
225 ImageUrl {
226 image_url: ImageUrl,
227 #[serde(skip_serializing_if = "Option::is_none")]
228 prompt_cache_breakpoint: Option<PromptCacheBreakpoint>,
229 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
230 extra: Extra,
231 },
232 #[serde(rename = "input_audio")]
233 InputAudio {
234 input_audio: InputAudio,
235 #[serde(skip_serializing_if = "Option::is_none")]
236 prompt_cache_breakpoint: Option<PromptCacheBreakpoint>,
237 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
238 extra: Extra,
239 },
240 #[serde(rename = "file")]
241 File {
242 file: ChatFileRef,
243 #[serde(skip_serializing_if = "Option::is_none")]
244 prompt_cache_breakpoint: Option<PromptCacheBreakpoint>,
245 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
246 extra: Extra,
247 },
248}
249
250#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
251#[serde(tag = "type")]
252pub enum ChatTool {
253 #[serde(rename = "function")]
254 Function {
255 function: FunctionDefinition,
256 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
257 extra: Extra,
258 },
259 #[serde(rename = "custom")]
260 Custom {
261 custom: CustomToolDefinition,
262 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
263 extra: Extra,
264 },
265}
266
267#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
268#[serde(tag = "type")]
269pub enum ChatToolCall {
270 #[serde(rename = "function")]
271 Function {
272 id: String,
273 function: FunctionCall,
274 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
275 extra: Extra,
276 },
277 #[serde(rename = "custom")]
278 Custom {
279 id: String,
280 custom: CustomToolCall,
281 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
282 extra: Extra,
283 },
284}
285
286#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
287pub struct ChatCompletionResponse {
288 pub id: String,
289 pub choices: Vec<ChatCompletionChoice>,
290 #[serde(default)]
293 pub created: u64,
294 pub model: OpenAiModelId,
295 #[serde(default)]
297 pub object: ChatCompletionObjectType,
298 #[serde(skip_serializing_if = "Option::is_none")]
299 pub moderation: Option<ChatCompletionModeration>,
300 #[serde(skip_serializing_if = "Option::is_none")]
301 pub service_tier: Option<ServiceTier>,
302 #[serde(skip_serializing_if = "Option::is_none")]
303 pub system_fingerprint: Option<String>,
304 #[serde(skip_serializing_if = "Option::is_none")]
305 pub usage: Option<CompletionUsage>,
306 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
307 pub extra: Extra,
308}
309
310#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
311pub struct ChatCompletionChoice {
312 pub finish_reason: ChatFinishReason,
313 pub index: u32,
314 #[serde(skip_serializing_if = "Option::is_none")]
315 pub logprobs: Option<ChatChoiceLogprobs>,
316 pub message: ChatMessage,
317 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
318 pub extra: Extra,
319}
320
321#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
322pub struct ChatCompletionModeration {
323 pub input: ChatCompletionModerationOutcome,
324 pub output: ChatCompletionModerationOutcome,
325 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
326 pub extra: Extra,
327}
328
329#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
330#[serde(untagged)]
331pub enum ChatCompletionModerationOutcome {
332 Results(ChatCompletionModerationResults),
333 Error(ModerationError),
334}
335
336#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
337pub struct ChatCompletionModerationResults {
338 pub model: OpenAiModelId,
339 pub results: Vec<ModerationResult>,
340 #[serde(rename = "type")]
341 pub type_: ChatCompletionModerationResultsType,
342 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
343 pub extra: Extra,
344}
345
346#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
347pub enum ChatCompletionModerationResultsType {
348 #[serde(rename = "moderation_results")]
349 ModerationResults,
350}
351
352#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
353pub struct ChatMessage {
354 pub role: ChatCompletionMessageRole,
355 #[serde(skip_serializing_if = "Option::is_none")]
356 pub content: Option<String>,
357 #[serde(skip_serializing_if = "Option::is_none")]
358 pub refusal: Option<String>,
359 #[serde(skip_serializing_if = "Option::is_none")]
360 pub annotations: Option<Vec<ChatAnnotation>>,
361 #[serde(skip_serializing_if = "Option::is_none")]
362 pub audio: Option<ChatAudio>,
363 #[serde(skip_serializing_if = "Option::is_none")]
364 pub function_call: Option<FunctionCall>,
365 #[serde(skip_serializing_if = "Option::is_none")]
366 pub reasoning_content: Option<String>,
367 #[serde(skip_serializing_if = "Option::is_none")]
368 pub tool_calls: Option<Vec<ChatToolCall>>,
369 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
370 pub extra: Extra,
371}
372
373#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
374pub enum ChatCompletionMessageRole {
375 #[serde(rename = "assistant")]
376 Assistant,
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382
383 #[test]
388 fn chat_response_tolerates_minimal_and_unknown_tier() {
389 let r: ChatCompletionResponse =
390 serde_json::from_str(r#"{"id":"x","choices":[],"model":"gpt-4o-mini"}"#).unwrap();
391 assert_eq!(r.created, 0);
392
393 let r: ChatCompletionResponse = serde_json::from_str(
394 r#"{"id":"x","choices":[],"model":"m","service_tier":"on_demand"}"#,
395 )
396 .unwrap();
397 assert!(r.service_tier.is_some());
398 }
399}