#![allow(missing_docs)]
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::RequestMetadata;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FinishReason {
Stop,
Length,
Cancelled,
ContentFilter,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ChatFinishReason {
Stop,
Length,
ToolCalls,
ContentFilter,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ChatRole {
System,
User,
Assistant,
Tool,
Developer,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Grammar {
JsonSchema {
json_schema: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
strict: Option<bool>,
},
Regex {
regex: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
strict: Option<bool>,
},
Ebnf {
ebnf: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
strict: Option<bool>,
},
}
impl Grammar {
pub fn json_schema(schema: Value) -> Self {
Self::JsonSchema {
json_schema: schema,
label: None,
strict: None,
}
}
pub fn regex(pattern: impl Into<String>) -> Self {
Self::Regex {
regex: pattern.into(),
label: None,
strict: None,
}
}
pub fn ebnf(grammar: impl Into<String>) -> Self {
Self::Ebnf {
ebnf: grammar.into(),
label: None,
strict: None,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct GenerationUsage {
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub prompt_tokens: u64,
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub completion_tokens: u64,
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub total_tokens: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub credits_charged: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rate_book_version: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct GenerateResult {
pub model: String,
pub text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub finish_reason: Option<FinishReason>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usage: Option<GenerationUsage>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attempt_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ttft_ms: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tpot_ms: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request: Option<RequestMetadata>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct GenerateChunk {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub seq: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text_delta: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub logprobs: Option<Vec<Value>>,
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub done: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub finish_reason: Option<FinishReason>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usage: Option<GenerationUsage>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ttft_ms: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ChatImageUrl {
Url(String),
Object { url: String },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChatContentPart {
#[serde(rename = "type")]
pub kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub image_url: Option<ChatImageUrl>,
}
impl ChatContentPart {
pub fn text(text: impl Into<String>) -> Self {
Self {
kind: "text".to_string(),
text: Some(text.into()),
image_url: None,
}
}
pub fn image_url(url: impl Into<String>) -> Self {
Self {
kind: "image_url".to_string(),
text: None,
image_url: Some(ChatImageUrl::Url(url.into())),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ChatContent {
Text(String),
Parts(Vec<ChatContentPart>),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChatMessage {
pub role: ChatRole,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content: Option<ChatContent>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<Value>>,
}
impl ChatMessage {
fn simple(role: ChatRole, content: impl Into<String>) -> Self {
Self {
role,
content: Some(ChatContent::Text(content.into())),
name: None,
tool_call_id: None,
tool_calls: None,
}
}
pub fn system(content: impl Into<String>) -> Self {
Self::simple(ChatRole::System, content)
}
pub fn user(content: impl Into<String>) -> Self {
Self::simple(ChatRole::User, content)
}
pub fn assistant(content: impl Into<String>) -> Self {
Self::simple(ChatRole::Assistant, content)
}
pub fn developer(content: impl Into<String>) -> Self {
Self::simple(ChatRole::Developer, content)
}
pub fn tool(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
Self {
tool_call_id: Some(tool_call_id.into()),
..Self::simple(ChatRole::Tool, content)
}
}
pub fn user_parts(parts: impl IntoIterator<Item = ChatContentPart>) -> Self {
Self {
role: ChatRole::User,
content: Some(ChatContent::Parts(parts.into_iter().collect())),
name: None,
tool_call_id: None,
tool_calls: None,
}
}
pub fn text(&self) -> Option<&str> {
match &self.content {
Some(ChatContent::Text(text)) => Some(text),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChatUsage {
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub prompt_tokens: u64,
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub completion_tokens: u64,
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub total_tokens: u64,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChatChoice {
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub index: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<ChatMessage>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub finish_reason: Option<ChatFinishReason>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub logprobs: Option<Value>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChatCompletion {
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub id: String,
#[serde(
default,
deserialize_with = "crate::types::null_as_default",
rename = "object"
)]
pub object_kind: String,
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub created: i64,
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub model: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub system_fingerprint: Option<String>,
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub choices: Vec<ChatChoice>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usage: Option<ChatUsage>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request: Option<RequestMetadata>,
}
impl ChatCompletion {
pub fn text(&self) -> Option<&str> {
self.choices.first()?.message.as_ref()?.text()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChatDelta {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub role: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<Value>>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChatChunkChoice {
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub index: u32,
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub delta: ChatDelta,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub finish_reason: Option<ChatFinishReason>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub logprobs: Option<Value>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChatCompletionChunk {
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub id: String,
#[serde(
default,
deserialize_with = "crate::types::null_as_default",
rename = "object"
)]
pub object_kind: String,
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub created: i64,
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub model: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub system_fingerprint: Option<String>,
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub choices: Vec<ChatChunkChoice>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usage: Option<ChatUsage>,
}
impl ChatCompletionChunk {
pub fn delta(&self) -> Option<&str> {
self.choices.first()?.delta.content.as_deref()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResponseInputMessage {
pub role: ChatRole,
pub content: ChatContent,
}
impl ResponseInputMessage {
pub fn user(content: impl Into<String>) -> Self {
Self {
role: ChatRole::User,
content: ChatContent::Text(content.into()),
}
}
pub fn system(content: impl Into<String>) -> Self {
Self {
role: ChatRole::System,
content: ChatContent::Text(content.into()),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResponseOutputText {
#[serde(rename = "type", default)]
pub kind: String,
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub text: String,
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub annotations: Vec<Value>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResponseOutputMessage {
#[serde(rename = "type", default)]
pub kind: String,
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub id: String,
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub role: String,
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub status: String,
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub content: Vec<ResponseOutputText>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResponseUsage {
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub input_tokens: u64,
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub output_tokens: u64,
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub total_tokens: u64,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResponseResult {
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub id: String,
#[serde(
default,
deserialize_with = "crate::types::null_as_default",
rename = "object"
)]
pub object_kind: String,
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub created_at: i64,
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub model: String,
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub status: String,
#[serde(default, deserialize_with = "crate::types::null_as_default")]
pub output: Vec<ResponseOutputMessage>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usage: Option<ResponseUsage>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request: Option<RequestMetadata>,
}
impl ResponseResult {
pub fn text(&self) -> Option<&str> {
Some(self.output.first()?.content.first()?.text.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn grammar_serializes_to_exactly_one_arm() {
assert_eq!(
serde_json::to_value(Grammar::json_schema(json!({"type": "object"}))).unwrap(),
json!({"json_schema": {"type": "object"}})
);
assert_eq!(
serde_json::to_value(Grammar::regex("[0-9]+")).unwrap(),
json!({"regex": "[0-9]+"})
);
assert_eq!(
serde_json::to_value(Grammar::ebnf("root ::= \"a\"")).unwrap(),
json!({"ebnf": "root ::= \"a\""})
);
}
#[test]
fn grammar_carries_optional_label_and_strict() {
let grammar = Grammar::Regex {
regex: "a+".to_string(),
label: Some("digits".to_string()),
strict: Some(true),
};
assert_eq!(
serde_json::to_value(grammar).unwrap(),
json!({"regex": "a+", "label": "digits", "strict": true})
);
}
#[test]
fn chat_messages_serialize_only_what_was_set() {
assert_eq!(
serde_json::to_value(ChatMessage::user("hi")).unwrap(),
json!({"role": "user", "content": "hi"})
);
assert_eq!(
serde_json::to_value(ChatMessage::tool("call_1", "42")).unwrap(),
json!({"role": "tool", "content": "42", "tool_call_id": "call_1"})
);
}
#[test]
fn multimodal_messages_keep_their_part_order() {
let message = ChatMessage::user_parts([
ChatContentPart::text("What is this?"),
ChatContentPart::image_url("https://example.com/a.png"),
]);
assert_eq!(
serde_json::to_value(message).unwrap(),
json!({"role": "user", "content": [
{"type": "text", "text": "What is this?"},
{"type": "image_url", "image_url": "https://example.com/a.png"}
]})
);
}
#[test]
fn completions_expose_their_first_choice_text() {
let completion: ChatCompletion = serde_json::from_value(json!({
"id": "cmpl-1", "object": "chat.completion", "created": 1, "model": "m",
"choices": [{"index": 0, "message": {"role": "assistant", "content": "hello"},
"finish_reason": "stop"}],
"usage": {"prompt_tokens": 3, "completion_tokens": 1, "total_tokens": 4}
}))
.unwrap();
assert_eq!(completion.text(), Some("hello"));
assert_eq!(completion.object_kind, "chat.completion");
assert_eq!(
completion.choices[0].finish_reason,
Some(ChatFinishReason::Stop)
);
assert_eq!(completion.usage.unwrap().total_tokens, 4);
}
#[test]
fn stream_chunks_expose_their_delta_and_tolerate_the_usage_only_tail() {
let chunk: ChatCompletionChunk = serde_json::from_value(json!({
"id": "cmpl-1", "choices": [{"index": 0, "delta": {"content": "he"}}]
}))
.unwrap();
assert_eq!(chunk.delta(), Some("he"));
let tail: ChatCompletionChunk = serde_json::from_value(json!({
"id": "cmpl-1", "choices": [],
"usage": {"prompt_tokens": 3, "completion_tokens": 1, "total_tokens": 4}
}))
.unwrap();
assert!(tail.delta().is_none());
assert_eq!(tail.usage.unwrap().completion_tokens, 1);
}
#[test]
fn responses_expose_their_first_output_text() {
let result: ResponseResult = serde_json::from_value(json!({
"id": "resp-1", "object": "response", "model": "m", "status": "completed",
"output": [{"type": "message", "id": "m1", "role": "assistant", "status": "completed",
"content": [{"type": "output_text", "text": "answer", "annotations": []}]}],
"usage": {"input_tokens": 2, "output_tokens": 1, "total_tokens": 3}
}))
.unwrap();
assert_eq!(result.text(), Some("answer"));
assert_eq!(result.usage.unwrap().input_tokens, 2);
}
#[test]
fn native_generate_results_decode() {
let result: GenerateResult = serde_json::from_value(json!({
"model": "m", "text": "out", "finish_reason": "length",
"usage": {"prompt_tokens": 1, "completion_tokens": 2, "total_tokens": 3,
"credits_charged": 7, "rate_book_version": "2026-01"},
"ttft_ms": 12.5
}))
.unwrap();
assert_eq!(result.finish_reason, Some(FinishReason::Length));
assert_eq!(result.usage.unwrap().credits_charged, Some(7));
assert_eq!(result.ttft_ms, Some(12.5));
}
}