use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ImageUrl {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct VideoUrl {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentPart {
Text { text: String },
ImageUrl { image_url: ImageUrl },
VideoUrl { video_url: VideoUrl },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum MessageContent {
Text(String),
Parts(Vec<ContentPart>),
}
impl MessageContent {
pub fn as_text(&self) -> Option<&str> {
match self {
MessageContent::Text(s) => Some(s),
MessageContent::Parts(parts) => {
for part in parts {
if let ContentPart::Text { text } = part {
return Some(text);
}
}
None
}
}
}
pub fn text(&self) -> &str {
match self {
MessageContent::Text(s) => s,
MessageContent::Parts(parts) => {
for part in parts {
if let ContentPart::Text { text } = part {
return text;
}
}
""
}
}
}
}
impl From<String> for MessageContent {
fn from(s: String) -> Self {
MessageContent::Text(s)
}
}
impl From<&str> for MessageContent {
fn from(s: &str) -> Self {
MessageContent::Text(s.to_string())
}
}
impl std::fmt::Display for MessageContent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MessageContent::Text(s) => write!(f, "{}", s),
MessageContent::Parts(parts) => {
for part in parts {
match part {
ContentPart::Text { text } => write!(f, "{}", text)?,
ContentPart::ImageUrl { image_url } => {
write!(f, "[Image: {}]", image_url.url)?
}
ContentPart::VideoUrl { video_url } => {
write!(f, "[Video: {}]", video_url.url)?
}
}
}
Ok(())
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ChatMessage {
pub role: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<MessageContent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
}
impl ChatMessage {
pub fn system<S: Into<MessageContent>>(content: S) -> Self {
Self {
role: "system".to_string(),
content: Some(content.into()),
tool_calls: None,
tool_call_id: None,
}
}
pub fn user<S: Into<MessageContent>>(content: S) -> Self {
Self {
role: "user".to_string(),
content: Some(content.into()),
tool_calls: None,
tool_call_id: None,
}
}
pub fn user_parts(parts: Vec<ContentPart>) -> Self {
Self {
role: "user".to_string(),
content: Some(MessageContent::Parts(parts)),
tool_calls: None,
tool_call_id: None,
}
}
pub fn assistant<S: Into<MessageContent>>(content: S) -> Self {
Self {
role: "assistant".to_string(),
content: Some(content.into()),
tool_calls: None,
tool_call_id: None,
}
}
pub fn assistant_with_tools<S: Into<MessageContent>>(
content: Option<S>,
tool_calls: Vec<ToolCall>,
) -> Self {
Self {
role: "assistant".to_string(),
content: content.map(|c| c.into()),
tool_calls: Some(tool_calls),
tool_call_id: None,
}
}
pub fn tool(content: impl Into<String>, tool_call_id: impl Into<String>) -> Self {
Self {
role: "tool".to_string(),
content: Some(MessageContent::Text(content.into())),
tool_calls: None,
tool_call_id: Some(tool_call_id.into()),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ChatRequest {
pub model: String,
pub messages: Vec<ChatMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub frequency_penalty: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub presence_penalty: Option<f32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatResponse {
pub id: String,
pub object: String,
pub created: u64,
pub model: String,
pub choices: Vec<Choice>,
pub usage: Usage,
}
impl ChatResponse {
pub fn content(&self) -> Option<&str> {
self.choices
.first()
.and_then(|choice| choice.message.content.as_ref())
.and_then(|content| content.as_text())
}
pub fn message(&self) -> Option<&Message> {
self.choices.first().map(|choice| &choice.message)
}
pub fn tool_calls(&self) -> Option<&[ToolCall]> {
self.choices
.first()
.and_then(|choice| choice.message.tool_calls.as_deref())
}
pub fn has_tool_calls(&self) -> bool {
self.tool_calls().is_some_and(|calls| !calls.is_empty())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Choice {
pub index: u32,
pub message: Message,
pub finish_reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub role: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<MessageContent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolCall {
pub id: String,
#[serde(rename = "type")]
pub call_type: String,
pub function: FunctionCall,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FunctionCall {
pub name: String,
pub arguments: String,
}
impl FunctionCall {
pub fn parse_arguments(&self) -> Result<Value, serde_json::Error> {
serde_json::from_str(&self.arguments)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Usage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub cached_prompt_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_tokens: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Model {
Grok4_3,
Grok4_20_0309Reasoning,
Grok4_20NonReasoning,
Grok4_20_0309NonReasoning,
Grok4_20MultiAgent0309,
#[deprecated(
since = "0.1.7",
note = "Retires 2026-05-15. Migrate to Model::Grok4_3 (grok-4.3)."
)]
Grok4_1FastReasoning,
#[deprecated(
since = "0.1.7",
note = "Retires 2026-05-15. Migrate to Model::Grok4_20NonReasoning (grok-4.20-non-reasoning)."
)]
Grok4_1FastNonReasoning,
#[deprecated(
since = "0.1.7",
note = "Retires 2026-05-15. Migrate to Model::Grok4_3 (grok-4.3)."
)]
Grok4_0709,
#[deprecated(
since = "0.1.7",
note = "Retires 2026-05-15. Migrate to Model::Grok4_3 (grok-4.3)."
)]
Grok3,
Grok3Mini,
#[deprecated(
since = "0.1.7",
note = "Retires 2026-05-15. Migrate to Model::Grok4_3 (grok-4.3)."
)]
GrokCodeFast1,
#[deprecated(
since = "0.1.7",
note = "Retires 2026-05-15. Migrate to Model::GrokImagineImage (grok-imagine-image)."
)]
GrokImagineImagePro,
GrokImagineImage,
GrokImagineVideo,
}
#[allow(deprecated)]
impl Model {
pub fn as_str(&self) -> &'static str {
match self {
Model::Grok4_3 => "grok-4.3",
Model::Grok4_20_0309Reasoning => "grok-4.20-0309-reasoning",
Model::Grok4_20NonReasoning => "grok-4.20-non-reasoning",
Model::Grok4_20_0309NonReasoning => "grok-4.20-0309-non-reasoning",
Model::Grok4_20MultiAgent0309 => "grok-4.20-multi-agent-0309",
Model::Grok4_1FastReasoning => "grok-4-1-fast-reasoning",
Model::Grok4_1FastNonReasoning => "grok-4-1-fast-non-reasoning",
Model::Grok4_0709 => "grok-4-0709",
Model::Grok3 => "grok-3",
Model::Grok3Mini => "grok-3-mini",
Model::GrokCodeFast1 => "grok-code-fast-1",
Model::GrokImagineImagePro => "grok-imagine-image-pro",
Model::GrokImagineImage => "grok-imagine-image",
Model::GrokImagineVideo => "grok-imagine-video",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"grok-4.3" => Some(Model::Grok4_3),
"grok-4.20-0309-reasoning" => Some(Model::Grok4_20_0309Reasoning),
"grok-4.20-non-reasoning" => Some(Model::Grok4_20NonReasoning),
"grok-4.20-0309-non-reasoning" => Some(Model::Grok4_20_0309NonReasoning),
"grok-4.20-multi-agent-0309" => Some(Model::Grok4_20MultiAgent0309),
#[allow(deprecated)]
"grok-4-1-fast-reasoning" => Some(Model::Grok4_1FastReasoning),
#[allow(deprecated)]
"grok-4-1-fast-non-reasoning" => Some(Model::Grok4_1FastNonReasoning),
#[allow(deprecated)]
"grok-4-0709" => Some(Model::Grok4_0709),
#[allow(deprecated)]
"grok-3" => Some(Model::Grok3),
"grok-3-mini" => Some(Model::Grok3Mini),
#[allow(deprecated)]
"grok-code-fast-1" => Some(Model::GrokCodeFast1),
#[allow(deprecated)]
"grok-imagine-image-pro" => Some(Model::GrokImagineImagePro),
"grok-imagine-image" => Some(Model::GrokImagineImage),
"grok-imagine-video" => Some(Model::GrokImagineVideo),
_ => None,
}
}
pub fn all() -> Vec<Self> {
vec![
Model::Grok4_3,
Model::Grok4_20_0309Reasoning,
Model::Grok4_20NonReasoning,
Model::Grok4_20_0309NonReasoning,
Model::Grok4_20MultiAgent0309,
Model::Grok3Mini,
Model::GrokImagineImage,
Model::GrokImagineVideo,
]
}
pub fn all_including_deprecated() -> Vec<Self> {
vec![
Model::Grok4_3,
Model::Grok4_20_0309Reasoning,
Model::Grok4_20NonReasoning,
Model::Grok4_20_0309NonReasoning,
Model::Grok4_20MultiAgent0309,
#[allow(deprecated)]
Model::Grok4_1FastReasoning,
#[allow(deprecated)]
Model::Grok4_1FastNonReasoning,
#[allow(deprecated)]
Model::Grok4_0709,
#[allow(deprecated)]
Model::Grok3,
Model::Grok3Mini,
#[allow(deprecated)]
Model::GrokCodeFast1,
#[allow(deprecated)]
Model::GrokImagineImagePro,
Model::GrokImagineImage,
Model::GrokImagineVideo,
]
}
pub fn is_reasoning_model(&self) -> bool {
matches!(
self,
Model::Grok4_20_0309Reasoning
| Model::Grok4_20MultiAgent0309
| Model::Grok4_1FastReasoning
| Model::Grok4_0709
)
}
pub fn supports_frequency_presence_penalty(&self) -> bool {
!self.is_reasoning_model()
}
pub fn supports_reasoning_effort(&self) -> bool {
matches!(self, Model::Grok4_3)
}
pub fn supports_logprobs(&self) -> bool {
!matches!(
self,
Model::Grok4_3
| Model::Grok4_20_0309Reasoning
| Model::Grok4_20NonReasoning
| Model::Grok4_20_0309NonReasoning
| Model::Grok4_20MultiAgent0309
)
}
pub fn context_window(&self) -> Option<u32> {
match self {
Model::Grok4_3 => Some(1_000_000),
Model::Grok4_20_0309Reasoning
| Model::Grok4_20NonReasoning
| Model::Grok4_20_0309NonReasoning
| Model::Grok4_20MultiAgent0309
| Model::Grok4_1FastReasoning
| Model::Grok4_1FastNonReasoning
| Model::Grok4_0709 => Some(2_000_000),
Model::Grok3 | Model::Grok3Mini => Some(131_072),
Model::GrokCodeFast1 => Some(131_072),
Model::GrokImagineImagePro | Model::GrokImagineImage | Model::GrokImagineVideo => None,
}
}
pub fn is_language_model(&self) -> bool {
!matches!(
self,
Model::GrokImagineImagePro | Model::GrokImagineImage | Model::GrokImagineVideo
)
}
pub fn is_image_model(&self) -> bool {
matches!(self, Model::GrokImagineImagePro | Model::GrokImagineImage)
}
pub fn is_video_model(&self) -> bool {
matches!(self, Model::GrokImagineVideo)
}
}
impl std::fmt::Display for Model {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl From<Model> for String {
fn from(model: Model) -> Self {
model.as_str().to_string()
}
}
#[cfg(test)]
mod tests {
}