1#![allow(missing_docs)]
6
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10use super::RequestMetadata;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum FinishReason {
16 Stop,
17 Length,
18 Cancelled,
19 ContentFilter,
20 Error,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum ChatFinishReason {
27 Stop,
28 Length,
29 ToolCalls,
30 ContentFilter,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "lowercase")]
36pub enum ChatRole {
37 System,
38 User,
39 Assistant,
40 Tool,
41 Developer,
42}
43
44#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
49#[serde(untagged)]
50pub enum Grammar {
51 JsonSchema {
53 json_schema: Value,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
55 label: Option<String>,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
57 strict: Option<bool>,
58 },
59 Regex {
61 regex: String,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
63 label: Option<String>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
65 strict: Option<bool>,
66 },
67 Ebnf {
69 ebnf: String,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
71 label: Option<String>,
72 #[serde(default, skip_serializing_if = "Option::is_none")]
73 strict: Option<bool>,
74 },
75}
76
77impl Grammar {
78 pub fn json_schema(schema: Value) -> Self {
80 Self::JsonSchema {
81 json_schema: schema,
82 label: None,
83 strict: None,
84 }
85 }
86
87 pub fn regex(pattern: impl Into<String>) -> Self {
89 Self::Regex {
90 regex: pattern.into(),
91 label: None,
92 strict: None,
93 }
94 }
95
96 pub fn ebnf(grammar: impl Into<String>) -> Self {
98 Self::Ebnf {
99 ebnf: grammar.into(),
100 label: None,
101 strict: None,
102 }
103 }
104}
105
106#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
108pub struct GenerationUsage {
109 #[serde(default, deserialize_with = "crate::types::null_as_default")]
110 pub prompt_tokens: u64,
111 #[serde(default, deserialize_with = "crate::types::null_as_default")]
112 pub completion_tokens: u64,
113 #[serde(default, deserialize_with = "crate::types::null_as_default")]
114 pub total_tokens: u64,
115 #[serde(default, skip_serializing_if = "Option::is_none")]
116 pub credits_charged: Option<u64>,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
118 pub rate_book_version: Option<String>,
119}
120
121#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
123pub struct GenerateResult {
124 pub model: String,
125 pub text: String,
126 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub finish_reason: Option<FinishReason>,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub usage: Option<GenerationUsage>,
130 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub attempt_id: Option<String>,
132 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub ttft_ms: Option<f64>,
135 #[serde(default, skip_serializing_if = "Option::is_none")]
137 pub tpot_ms: Option<f64>,
138 #[serde(default, skip_serializing_if = "Option::is_none")]
139 pub request: Option<RequestMetadata>,
140}
141
142#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
144pub struct GenerateChunk {
145 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub request_id: Option<String>,
147 #[serde(default, skip_serializing_if = "Option::is_none")]
149 pub seq: Option<u64>,
150 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub text_delta: Option<String>,
152 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub logprobs: Option<Vec<Value>>,
154 #[serde(default, deserialize_with = "crate::types::null_as_default")]
155 pub done: bool,
156 #[serde(default, skip_serializing_if = "Option::is_none")]
157 pub finish_reason: Option<FinishReason>,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub usage: Option<GenerationUsage>,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub ttft_ms: Option<f64>,
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
167#[serde(untagged)]
168pub enum ChatImageUrl {
169 Url(String),
171 Object { url: String },
173}
174
175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
177pub struct ChatContentPart {
178 #[serde(rename = "type")]
179 pub kind: String,
180 #[serde(default, skip_serializing_if = "Option::is_none")]
181 pub text: Option<String>,
182 #[serde(default, skip_serializing_if = "Option::is_none")]
183 pub image_url: Option<ChatImageUrl>,
184}
185
186impl ChatContentPart {
187 pub fn text(text: impl Into<String>) -> Self {
189 Self {
190 kind: "text".to_string(),
191 text: Some(text.into()),
192 image_url: None,
193 }
194 }
195
196 pub fn image_url(url: impl Into<String>) -> Self {
198 Self {
199 kind: "image_url".to_string(),
200 text: None,
201 image_url: Some(ChatImageUrl::Url(url.into())),
202 }
203 }
204}
205
206#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
208#[serde(untagged)]
209pub enum ChatContent {
210 Text(String),
211 Parts(Vec<ChatContentPart>),
212}
213
214#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
216pub struct ChatMessage {
217 pub role: ChatRole,
218 #[serde(default, skip_serializing_if = "Option::is_none")]
219 pub content: Option<ChatContent>,
220 #[serde(default, skip_serializing_if = "Option::is_none")]
221 pub name: Option<String>,
222 #[serde(default, skip_serializing_if = "Option::is_none")]
223 pub tool_call_id: Option<String>,
224 #[serde(default, skip_serializing_if = "Option::is_none")]
225 pub tool_calls: Option<Vec<Value>>,
226}
227
228impl ChatMessage {
229 fn simple(role: ChatRole, content: impl Into<String>) -> Self {
230 Self {
231 role,
232 content: Some(ChatContent::Text(content.into())),
233 name: None,
234 tool_call_id: None,
235 tool_calls: None,
236 }
237 }
238
239 pub fn system(content: impl Into<String>) -> Self {
241 Self::simple(ChatRole::System, content)
242 }
243
244 pub fn user(content: impl Into<String>) -> Self {
246 Self::simple(ChatRole::User, content)
247 }
248
249 pub fn assistant(content: impl Into<String>) -> Self {
251 Self::simple(ChatRole::Assistant, content)
252 }
253
254 pub fn developer(content: impl Into<String>) -> Self {
256 Self::simple(ChatRole::Developer, content)
257 }
258
259 pub fn tool(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
261 Self {
262 tool_call_id: Some(tool_call_id.into()),
263 ..Self::simple(ChatRole::Tool, content)
264 }
265 }
266
267 pub fn user_parts(parts: impl IntoIterator<Item = ChatContentPart>) -> Self {
269 Self {
270 role: ChatRole::User,
271 content: Some(ChatContent::Parts(parts.into_iter().collect())),
272 name: None,
273 tool_call_id: None,
274 tool_calls: None,
275 }
276 }
277
278 pub fn text(&self) -> Option<&str> {
280 match &self.content {
281 Some(ChatContent::Text(text)) => Some(text),
282 _ => None,
283 }
284 }
285}
286
287#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
289pub struct ChatUsage {
290 #[serde(default, deserialize_with = "crate::types::null_as_default")]
291 pub prompt_tokens: u64,
292 #[serde(default, deserialize_with = "crate::types::null_as_default")]
293 pub completion_tokens: u64,
294 #[serde(default, deserialize_with = "crate::types::null_as_default")]
295 pub total_tokens: u64,
296}
297
298#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
300pub struct ChatChoice {
301 #[serde(default, deserialize_with = "crate::types::null_as_default")]
302 pub index: u32,
303 #[serde(default, skip_serializing_if = "Option::is_none")]
304 pub message: Option<ChatMessage>,
305 #[serde(default, skip_serializing_if = "Option::is_none")]
306 pub finish_reason: Option<ChatFinishReason>,
307 #[serde(default, skip_serializing_if = "Option::is_none")]
308 pub logprobs: Option<Value>,
309}
310
311#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
313pub struct ChatCompletion {
314 #[serde(default, deserialize_with = "crate::types::null_as_default")]
315 pub id: String,
316 #[serde(
317 default,
318 deserialize_with = "crate::types::null_as_default",
319 rename = "object"
320 )]
321 pub object_kind: String,
322 #[serde(default, deserialize_with = "crate::types::null_as_default")]
323 pub created: i64,
324 #[serde(default, deserialize_with = "crate::types::null_as_default")]
325 pub model: String,
326 #[serde(default, skip_serializing_if = "Option::is_none")]
327 pub system_fingerprint: Option<String>,
328 #[serde(default, deserialize_with = "crate::types::null_as_default")]
329 pub choices: Vec<ChatChoice>,
330 #[serde(default, skip_serializing_if = "Option::is_none")]
331 pub usage: Option<ChatUsage>,
332 #[serde(default, skip_serializing_if = "Option::is_none")]
333 pub request: Option<RequestMetadata>,
334}
335
336impl ChatCompletion {
337 pub fn text(&self) -> Option<&str> {
339 self.choices.first()?.message.as_ref()?.text()
340 }
341}
342
343#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
345pub struct ChatDelta {
346 #[serde(default, skip_serializing_if = "Option::is_none")]
347 pub role: Option<String>,
348 #[serde(default, skip_serializing_if = "Option::is_none")]
349 pub content: Option<String>,
350 #[serde(default, skip_serializing_if = "Option::is_none")]
351 pub tool_calls: Option<Vec<Value>>,
352}
353
354#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
356pub struct ChatChunkChoice {
357 #[serde(default, deserialize_with = "crate::types::null_as_default")]
358 pub index: u32,
359 #[serde(default, deserialize_with = "crate::types::null_as_default")]
360 pub delta: ChatDelta,
361 #[serde(default, skip_serializing_if = "Option::is_none")]
362 pub finish_reason: Option<ChatFinishReason>,
363 #[serde(default, skip_serializing_if = "Option::is_none")]
364 pub logprobs: Option<Value>,
365}
366
367#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
372pub struct ChatCompletionChunk {
373 #[serde(default, deserialize_with = "crate::types::null_as_default")]
374 pub id: String,
375 #[serde(
376 default,
377 deserialize_with = "crate::types::null_as_default",
378 rename = "object"
379 )]
380 pub object_kind: String,
381 #[serde(default, deserialize_with = "crate::types::null_as_default")]
382 pub created: i64,
383 #[serde(default, deserialize_with = "crate::types::null_as_default")]
384 pub model: String,
385 #[serde(default, skip_serializing_if = "Option::is_none")]
386 pub system_fingerprint: Option<String>,
387 #[serde(default, deserialize_with = "crate::types::null_as_default")]
388 pub choices: Vec<ChatChunkChoice>,
389 #[serde(default, skip_serializing_if = "Option::is_none")]
390 pub usage: Option<ChatUsage>,
391}
392
393impl ChatCompletionChunk {
394 pub fn delta(&self) -> Option<&str> {
396 self.choices.first()?.delta.content.as_deref()
397 }
398}
399
400#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
402pub struct ResponseInputMessage {
403 pub role: ChatRole,
404 pub content: ChatContent,
405}
406
407impl ResponseInputMessage {
408 pub fn user(content: impl Into<String>) -> Self {
410 Self {
411 role: ChatRole::User,
412 content: ChatContent::Text(content.into()),
413 }
414 }
415
416 pub fn system(content: impl Into<String>) -> Self {
418 Self {
419 role: ChatRole::System,
420 content: ChatContent::Text(content.into()),
421 }
422 }
423}
424
425#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
427pub struct ResponseOutputText {
428 #[serde(rename = "type", default)]
429 pub kind: String,
430 #[serde(default, deserialize_with = "crate::types::null_as_default")]
431 pub text: String,
432 #[serde(default, deserialize_with = "crate::types::null_as_default")]
433 pub annotations: Vec<Value>,
434}
435
436#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
438pub struct ResponseOutputMessage {
439 #[serde(rename = "type", default)]
440 pub kind: String,
441 #[serde(default, deserialize_with = "crate::types::null_as_default")]
442 pub id: String,
443 #[serde(default, deserialize_with = "crate::types::null_as_default")]
444 pub role: String,
445 #[serde(default, deserialize_with = "crate::types::null_as_default")]
446 pub status: String,
447 #[serde(default, deserialize_with = "crate::types::null_as_default")]
448 pub content: Vec<ResponseOutputText>,
449}
450
451#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
455pub struct ResponseUsage {
456 #[serde(default, deserialize_with = "crate::types::null_as_default")]
457 pub input_tokens: u64,
458 #[serde(default, deserialize_with = "crate::types::null_as_default")]
459 pub output_tokens: u64,
460 #[serde(default, deserialize_with = "crate::types::null_as_default")]
461 pub total_tokens: u64,
462}
463
464#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
466pub struct ResponseResult {
467 #[serde(default, deserialize_with = "crate::types::null_as_default")]
468 pub id: String,
469 #[serde(
470 default,
471 deserialize_with = "crate::types::null_as_default",
472 rename = "object"
473 )]
474 pub object_kind: String,
475 #[serde(default, deserialize_with = "crate::types::null_as_default")]
476 pub created_at: i64,
477 #[serde(default, deserialize_with = "crate::types::null_as_default")]
478 pub model: String,
479 #[serde(default, deserialize_with = "crate::types::null_as_default")]
480 pub status: String,
481 #[serde(default, deserialize_with = "crate::types::null_as_default")]
482 pub output: Vec<ResponseOutputMessage>,
483 #[serde(default, skip_serializing_if = "Option::is_none")]
484 pub usage: Option<ResponseUsage>,
485 #[serde(default, skip_serializing_if = "Option::is_none")]
486 pub request: Option<RequestMetadata>,
487}
488
489impl ResponseResult {
490 pub fn text(&self) -> Option<&str> {
492 Some(self.output.first()?.content.first()?.text.as_str())
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499 use serde_json::json;
500
501 #[test]
502 fn grammar_serializes_to_exactly_one_arm() {
503 assert_eq!(
504 serde_json::to_value(Grammar::json_schema(json!({"type": "object"}))).unwrap(),
505 json!({"json_schema": {"type": "object"}})
506 );
507 assert_eq!(
508 serde_json::to_value(Grammar::regex("[0-9]+")).unwrap(),
509 json!({"regex": "[0-9]+"})
510 );
511 assert_eq!(
512 serde_json::to_value(Grammar::ebnf("root ::= \"a\"")).unwrap(),
513 json!({"ebnf": "root ::= \"a\""})
514 );
515 }
516
517 #[test]
518 fn grammar_carries_optional_label_and_strict() {
519 let grammar = Grammar::Regex {
520 regex: "a+".to_string(),
521 label: Some("digits".to_string()),
522 strict: Some(true),
523 };
524 assert_eq!(
525 serde_json::to_value(grammar).unwrap(),
526 json!({"regex": "a+", "label": "digits", "strict": true})
527 );
528 }
529
530 #[test]
531 fn chat_messages_serialize_only_what_was_set() {
532 assert_eq!(
533 serde_json::to_value(ChatMessage::user("hi")).unwrap(),
534 json!({"role": "user", "content": "hi"})
535 );
536 assert_eq!(
537 serde_json::to_value(ChatMessage::tool("call_1", "42")).unwrap(),
538 json!({"role": "tool", "content": "42", "tool_call_id": "call_1"})
539 );
540 }
541
542 #[test]
543 fn multimodal_messages_keep_their_part_order() {
544 let message = ChatMessage::user_parts([
545 ChatContentPart::text("What is this?"),
546 ChatContentPart::image_url("https://example.com/a.png"),
547 ]);
548 assert_eq!(
549 serde_json::to_value(message).unwrap(),
550 json!({"role": "user", "content": [
551 {"type": "text", "text": "What is this?"},
552 {"type": "image_url", "image_url": "https://example.com/a.png"}
553 ]})
554 );
555 }
556
557 #[test]
558 fn completions_expose_their_first_choice_text() {
559 let completion: ChatCompletion = serde_json::from_value(json!({
560 "id": "cmpl-1", "object": "chat.completion", "created": 1, "model": "m",
561 "choices": [{"index": 0, "message": {"role": "assistant", "content": "hello"},
562 "finish_reason": "stop"}],
563 "usage": {"prompt_tokens": 3, "completion_tokens": 1, "total_tokens": 4}
564 }))
565 .unwrap();
566 assert_eq!(completion.text(), Some("hello"));
567 assert_eq!(completion.object_kind, "chat.completion");
568 assert_eq!(
569 completion.choices[0].finish_reason,
570 Some(ChatFinishReason::Stop)
571 );
572 assert_eq!(completion.usage.unwrap().total_tokens, 4);
573 }
574
575 #[test]
576 fn stream_chunks_expose_their_delta_and_tolerate_the_usage_only_tail() {
577 let chunk: ChatCompletionChunk = serde_json::from_value(json!({
578 "id": "cmpl-1", "choices": [{"index": 0, "delta": {"content": "he"}}]
579 }))
580 .unwrap();
581 assert_eq!(chunk.delta(), Some("he"));
582
583 let tail: ChatCompletionChunk = serde_json::from_value(json!({
584 "id": "cmpl-1", "choices": [],
585 "usage": {"prompt_tokens": 3, "completion_tokens": 1, "total_tokens": 4}
586 }))
587 .unwrap();
588 assert!(tail.delta().is_none());
589 assert_eq!(tail.usage.unwrap().completion_tokens, 1);
590 }
591
592 #[test]
593 fn responses_expose_their_first_output_text() {
594 let result: ResponseResult = serde_json::from_value(json!({
595 "id": "resp-1", "object": "response", "model": "m", "status": "completed",
596 "output": [{"type": "message", "id": "m1", "role": "assistant", "status": "completed",
597 "content": [{"type": "output_text", "text": "answer", "annotations": []}]}],
598 "usage": {"input_tokens": 2, "output_tokens": 1, "total_tokens": 3}
599 }))
600 .unwrap();
601 assert_eq!(result.text(), Some("answer"));
602 assert_eq!(result.usage.unwrap().input_tokens, 2);
603 }
604
605 #[test]
606 fn native_generate_results_decode() {
607 let result: GenerateResult = serde_json::from_value(json!({
608 "model": "m", "text": "out", "finish_reason": "length",
609 "usage": {"prompt_tokens": 1, "completion_tokens": 2, "total_tokens": 3,
610 "credits_charged": 7, "rate_book_version": "2026-01"},
611 "ttft_ms": 12.5
612 }))
613 .unwrap();
614 assert_eq!(result.finish_reason, Some(FinishReason::Length));
615 assert_eq!(result.usage.unwrap().credits_charged, Some(7));
616 assert_eq!(result.ttft_ms, Some(12.5));
617 }
618}