Skip to main content

xai_grok/
types.rs

1//! Wire types for the xAI Grok chat-completions API.
2//!
3//! xAI's `/v1/chat/completions` endpoint is `OpenAI`-compatible, so these types
4//! mirror the `OpenAI` chat schema: `messages` (text or multimodal content
5//! parts), `tools` / `tool_calls` for function calling, `response_format` for
6//! structured outputs, and the streaming `chat.completion.chunk` delta shape.
7//!
8//! They are intentionally `serde`-only data holders — all transport and
9//! streaming logic lives in [`crate::client`].
10
11use serde::{Deserialize, Serialize};
12
13/// The role of a chat message.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum Role {
17    /// System / developer instruction.
18    System,
19    /// End-user input.
20    User,
21    /// Model output.
22    Assistant,
23    /// The result of a tool/function call, fed back to the model.
24    Tool,
25}
26
27/// A single piece of message content.
28///
29/// A message's content is either a plain string or an array of typed parts
30/// (text + images) for vision requests. Both serialize to the shapes xAI
31/// accepts.
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33#[serde(untagged)]
34pub enum Content {
35    /// Plain text content (the common case).
36    Text(String),
37    /// Multimodal content: a mix of text and image parts.
38    Parts(Vec<ContentPart>),
39}
40
41impl Content {
42    /// Convenience constructor for plain text.
43    pub fn text(s: impl Into<String>) -> Self {
44        Self::Text(s.into())
45    }
46}
47
48impl<T: Into<String>> From<T> for Content {
49    fn from(s: T) -> Self {
50        Self::Text(s.into())
51    }
52}
53
54/// One part of a multimodal message.
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56#[serde(tag = "type", rename_all = "snake_case")]
57pub enum ContentPart {
58    /// A text fragment.
59    Text {
60        /// The text.
61        text: String,
62    },
63    /// An image, referenced by URL or `data:` URI.
64    ImageUrl {
65        /// The image reference.
66        image_url: ImageUrl,
67    },
68}
69
70impl ContentPart {
71    /// A text part.
72    pub fn text(s: impl Into<String>) -> Self {
73        Self::Text { text: s.into() }
74    }
75
76    /// An image part from a URL or `data:` URI.
77    pub fn image(url: impl Into<String>) -> Self {
78        Self::ImageUrl {
79            image_url: ImageUrl {
80                url: url.into(),
81                detail: None,
82            },
83        }
84    }
85}
86
87/// An image reference for a vision request.
88#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
89pub struct ImageUrl {
90    /// A public URL or a base64 `data:image/...;base64,...` URI.
91    pub url: String,
92    /// Optional detail hint (`"low"`, `"high"`, `"auto"`).
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub detail: Option<String>,
95}
96
97/// A chat message.
98#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
99pub struct Message {
100    /// Who authored the message.
101    pub role: Role,
102    /// The message content. Optional because an assistant message that only
103    /// makes tool calls carries `null` content.
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub content: Option<Content>,
106    /// Tool calls requested by the assistant (assistant messages only).
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub tool_calls: Option<Vec<ToolCall>>,
109    /// For `role = "tool"`: which tool call this message answers.
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub tool_call_id: Option<String>,
112}
113
114impl Message {
115    /// A system message.
116    pub fn system(content: impl Into<Content>) -> Self {
117        Self::simple(Role::System, content)
118    }
119
120    /// A user message.
121    pub fn user(content: impl Into<Content>) -> Self {
122        Self::simple(Role::User, content)
123    }
124
125    /// An assistant message.
126    pub fn assistant(content: impl Into<Content>) -> Self {
127        Self::simple(Role::Assistant, content)
128    }
129
130    /// A tool-result message answering `tool_call_id`.
131    pub fn tool(tool_call_id: impl Into<String>, content: impl Into<Content>) -> Self {
132        Self {
133            role: Role::Tool,
134            content: Some(content.into()),
135            tool_calls: None,
136            tool_call_id: Some(tool_call_id.into()),
137        }
138    }
139
140    fn simple(role: Role, content: impl Into<Content>) -> Self {
141        Self {
142            role,
143            content: Some(content.into()),
144            tool_calls: None,
145            tool_call_id: None,
146        }
147    }
148}
149
150/// A tool the model may call. Currently only function tools exist.
151#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
152#[serde(tag = "type", rename_all = "snake_case")]
153pub enum Tool {
154    /// A function tool, described by a JSON Schema for its parameters.
155    Function {
156        /// The function definition.
157        function: FunctionDef,
158    },
159}
160
161impl Tool {
162    /// Build a function tool from a name, description, and JSON-Schema
163    /// parameters object.
164    pub fn function(
165        name: impl Into<String>,
166        description: impl Into<String>,
167        parameters: serde_json::Value,
168    ) -> Self {
169        Self::Function {
170            function: FunctionDef {
171                name: name.into(),
172                description: Some(description.into()),
173                parameters: Some(parameters),
174            },
175        }
176    }
177}
178
179/// A function tool definition.
180#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
181pub struct FunctionDef {
182    /// The function name the model uses when calling it.
183    pub name: String,
184    /// A natural-language description of what the function does.
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub description: Option<String>,
187    /// A JSON Schema object describing the parameters.
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub parameters: Option<serde_json::Value>,
190}
191
192/// A tool call emitted by the model.
193#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
194pub struct ToolCall {
195    /// The unique id of this call (echo it back on the tool result message).
196    pub id: String,
197    /// The tool type (always `"function"` today).
198    #[serde(rename = "type", default = "default_tool_type")]
199    pub kind: String,
200    /// The called function and its arguments.
201    pub function: FunctionCall,
202}
203
204fn default_tool_type() -> String {
205    "function".to_string()
206}
207
208/// The function part of a [`ToolCall`].
209#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
210pub struct FunctionCall {
211    /// The function name.
212    pub name: String,
213    /// The arguments as a JSON string (parse with `serde_json`).
214    pub arguments: String,
215}
216
217/// Controls how the model selects tools.
218#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
219#[serde(untagged)]
220pub enum ToolChoice {
221    /// `"auto"`, `"none"`, or `"required"`.
222    Mode(String),
223    /// Force a specific named function.
224    Named {
225        /// Always `"function"`.
226        #[serde(rename = "type")]
227        kind: String,
228        /// The forced function.
229        function: NamedFunction,
230    },
231}
232
233/// The function targeted by a forced [`ToolChoice`].
234#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
235pub struct NamedFunction {
236    /// The function name to force.
237    pub name: String,
238}
239
240/// Structured-output specification (`response_format`).
241#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
242#[serde(tag = "type", rename_all = "snake_case")]
243pub enum ResponseFormat {
244    /// Free-form text (the default).
245    Text,
246    /// Any valid JSON object.
247    JsonObject,
248    /// JSON constrained to a schema.
249    JsonSchema {
250        /// The schema wrapper.
251        json_schema: JsonSchema,
252    },
253}
254
255impl ResponseFormat {
256    /// Build a strict JSON-schema response format.
257    pub fn json_schema(name: impl Into<String>, schema: serde_json::Value) -> Self {
258        Self::JsonSchema {
259            json_schema: JsonSchema {
260                name: name.into(),
261                schema,
262                strict: Some(true),
263            },
264        }
265    }
266}
267
268/// A named JSON Schema for structured outputs.
269#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
270pub struct JsonSchema {
271    /// A name for the schema.
272    pub name: String,
273    /// The JSON Schema itself.
274    pub schema: serde_json::Value,
275    /// Whether the model must adhere strictly to the schema.
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub strict: Option<bool>,
278}
279
280/// A chat-completions request body.
281#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
282pub struct ChatRequest {
283    /// The model id, e.g. `"grok-4.3"`.
284    pub model: String,
285    /// The conversation so far.
286    pub messages: Vec<Message>,
287    /// Whether to stream the response as SSE.
288    #[serde(skip_serializing_if = "Option::is_none")]
289    pub stream: Option<bool>,
290    /// Sampling temperature.
291    #[serde(skip_serializing_if = "Option::is_none")]
292    pub temperature: Option<f32>,
293    /// Maximum tokens to generate.
294    #[serde(skip_serializing_if = "Option::is_none")]
295    pub max_tokens: Option<u32>,
296    /// Available tools.
297    #[serde(skip_serializing_if = "Option::is_none")]
298    pub tools: Option<Vec<Tool>>,
299    /// Tool-selection policy.
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub tool_choice: Option<ToolChoice>,
302    /// Structured-output specification.
303    #[serde(skip_serializing_if = "Option::is_none")]
304    pub response_format: Option<ResponseFormat>,
305}
306
307impl ChatRequest {
308    /// Start a request for `model` with the given messages.
309    pub fn new(model: impl Into<String>, messages: Vec<Message>) -> Self {
310        Self {
311            model: model.into(),
312            messages,
313            stream: None,
314            temperature: None,
315            max_tokens: None,
316            tools: None,
317            tool_choice: None,
318            response_format: None,
319        }
320    }
321
322    /// Set the sampling temperature (builder style).
323    #[must_use]
324    pub fn temperature(mut self, t: f32) -> Self {
325        self.temperature = Some(t);
326        self
327    }
328
329    /// Set the max output tokens (builder style).
330    #[must_use]
331    pub fn max_tokens(mut self, n: u32) -> Self {
332        self.max_tokens = Some(n);
333        self
334    }
335
336    /// Attach available tools (builder style).
337    #[must_use]
338    pub fn tools(mut self, tools: Vec<Tool>) -> Self {
339        self.tools = Some(tools);
340        self
341    }
342
343    /// Set the tool-selection policy (builder style).
344    #[must_use]
345    pub fn tool_choice(mut self, choice: ToolChoice) -> Self {
346        self.tool_choice = Some(choice);
347        self
348    }
349
350    /// Set the structured-output format (builder style).
351    #[must_use]
352    pub fn response_format(mut self, format: ResponseFormat) -> Self {
353        self.response_format = Some(format);
354        self
355    }
356}
357
358/// Token-usage accounting returned with a completion.
359#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
360pub struct Usage {
361    /// Tokens in the prompt.
362    pub prompt_tokens: u32,
363    /// Tokens in the completion.
364    pub completion_tokens: u32,
365    /// Total tokens billed.
366    pub total_tokens: u32,
367}
368
369/// A non-streamed chat completion response.
370#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
371pub struct ChatResponse {
372    /// The completion id.
373    pub id: String,
374    /// The model that produced it.
375    pub model: String,
376    /// The returned choices.
377    pub choices: Vec<ResponseChoice>,
378    /// Usage accounting, if reported.
379    #[serde(default)]
380    pub usage: Option<Usage>,
381}
382
383impl ChatResponse {
384    /// The first choice's message content as text, if any.
385    pub fn content(&self) -> Option<&str> {
386        self.choices
387            .first()
388            .and_then(|c| c.message.content.as_ref())
389            .and_then(|c| match c {
390                Content::Text(s) => Some(s.as_str()),
391                Content::Parts(_) => None,
392            })
393    }
394
395    /// The tool calls from the first choice, if any.
396    pub fn tool_calls(&self) -> &[ToolCall] {
397        self.choices
398            .first()
399            .and_then(|c| c.message.tool_calls.as_deref())
400            .unwrap_or(&[])
401    }
402}
403
404/// One choice in a non-streamed response.
405#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
406pub struct ResponseChoice {
407    /// The choice index.
408    pub index: u32,
409    /// The assistant message.
410    pub message: Message,
411    /// Why generation stopped (`"stop"`, `"tool_calls"`, `"length"`, ...).
412    #[serde(default)]
413    pub finish_reason: Option<String>,
414}
415
416// ---------------------------------------------------------------------------
417// Streaming chunk types (`chat.completion.chunk`)
418// ---------------------------------------------------------------------------
419
420/// One streamed `chat.completion.chunk` object (parsed from an SSE `data:`
421/// payload). The `[DONE]` sentinel is handled by the client, not represented
422/// here.
423#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
424pub struct ChatChunk {
425    /// The completion id (stable across chunks).
426    #[serde(default)]
427    pub id: String,
428    /// The per-choice deltas in this chunk.
429    pub choices: Vec<ChunkChoice>,
430    /// Usage, present only on the final chunk when requested.
431    #[serde(default)]
432    pub usage: Option<Usage>,
433}
434
435/// A per-choice delta within a [`ChatChunk`].
436#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
437pub struct ChunkChoice {
438    /// The choice index this delta applies to.
439    pub index: u32,
440    /// The incremental delta.
441    pub delta: Delta,
442    /// The finish reason, set on the choice's final chunk.
443    #[serde(default)]
444    pub finish_reason: Option<String>,
445}
446
447/// The incremental delta carried by a streaming chunk.
448#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
449pub struct Delta {
450    /// The role, sent on the first delta for a choice.
451    #[serde(default)]
452    pub role: Option<String>,
453    /// A content fragment to append.
454    #[serde(default)]
455    pub content: Option<String>,
456    /// Tool-call fragments to merge by `index`.
457    #[serde(default)]
458    pub tool_calls: Option<Vec<ToolCallDelta>>,
459}
460
461/// A streamed tool-call fragment.
462#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
463pub struct ToolCallDelta {
464    /// Which tool call (by position) this fragment belongs to.
465    pub index: u32,
466    /// The tool-call id, sent on the first fragment.
467    #[serde(default)]
468    pub id: Option<String>,
469    /// The function name/arguments fragment.
470    #[serde(default)]
471    pub function: Option<FunctionDelta>,
472}
473
474/// The function part of a [`ToolCallDelta`].
475#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
476pub struct FunctionDelta {
477    /// The function name, sent on the first fragment.
478    #[serde(default)]
479    pub name: Option<String>,
480    /// An arguments-JSON fragment to concatenate.
481    #[serde(default)]
482    pub arguments: Option<String>,
483}