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