open_agent/types/openai.rs
1/// OpenAI API message format for serialization.
2///
3/// This struct represents the wire format for messages when communicating
4/// with OpenAI-compatible APIs. It differs from the internal [`Message`]
5/// type to accommodate the specific serialization requirements of the
6/// OpenAI API.
7///
8/// # Key Differences from Internal Message Type
9///
10/// - Content is a flat string rather than structured blocks
11/// - Tool calls are represented in OpenAI's specific format
12/// - Supports both sending tool calls (via `tool_calls`) and tool results
13/// (via `tool_call_id`)
14///
15/// # Serialization
16///
17/// Optional fields are skipped when `None` to keep payloads minimal.
18///
19/// # Usage
20///
21/// This type is typically created by the SDK internally when converting
22/// from [`Message`] to API format. Users rarely need to construct these
23/// directly.
24///
25/// # OpenAI Content Format
26///
27/// OpenAI content format supporting both string and array.
28///
29/// For backward compatibility, text-only messages use string format.
30/// Messages with images use array format with multiple content parts.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32#[serde(untagged)]
33pub enum OpenAIContent {
34 /// Simple text string (backward compatible)
35 Text(String),
36 /// Array of content parts (text and/or images)
37 Parts(Vec<OpenAIContentPart>),
38}
39
40/// A single content part in an OpenAI message.
41///
42/// Can be either text or an image URL. This is a tagged enum that prevents
43/// invalid states (e.g., having both text and image_url, or neither).
44#[derive(Debug, Clone, Serialize, Deserialize)]
45#[serde(tag = "type", rename_all = "snake_case")]
46pub enum OpenAIContentPart {
47 /// Text content part
48 Text {
49 /// The text content
50 text: String,
51 },
52 /// Image URL content part
53 #[serde(rename = "image_url")]
54 ImageUrl {
55 /// The image URL details
56 image_url: OpenAIImageUrl,
57 },
58}
59
60impl OpenAIContentPart {
61 /// Creates a text content part.
62 ///
63 /// # Example
64 ///
65 /// ```
66 /// use open_agent::OpenAIContentPart;
67 ///
68 /// let part = OpenAIContentPart::text("Hello world");
69 /// ```
70 pub fn text(text: impl Into<String>) -> Self {
71 Self::Text { text: text.into() }
72 }
73
74 /// Creates an image content part from a validated ImageBlock.
75 ///
76 /// This is the preferred way to create image content parts as it ensures
77 /// the image URL has been validated against security issues (XSS, file disclosure, etc.)
78 ///
79 /// # Example
80 ///
81 /// ```
82 /// use open_agent::{OpenAIContentPart, ImageBlock, ImageDetail};
83 ///
84 /// let image = ImageBlock::from_url("https://example.com/img.jpg")
85 /// .expect("Valid URL");
86 /// let part = OpenAIContentPart::from_image(&image);
87 /// ```
88 pub fn from_image(image: &ImageBlock) -> Self {
89 Self::ImageUrl {
90 image_url: OpenAIImageUrl {
91 url: image.url().to_string(),
92 detail: Some(image.detail().to_string()),
93 },
94 }
95 }
96
97 /// Creates an image URL content part directly (DEPRECATED).
98 ///
99 /// # Security Warning
100 ///
101 /// This method bypasses validation checks performed by `ImageBlock::from_url()`
102 /// and `ImageBlock::from_base64()`. Prefer using `from_image()` instead.
103 ///
104 /// # Deprecation
105 ///
106 /// This method is deprecated and will be removed in v1.0. Use `from_image()` instead.
107 ///
108 /// # Example
109 ///
110 /// ```
111 /// use open_agent::{OpenAIContentPart, ImageDetail};
112 ///
113 /// // Deprecated approach:
114 /// let part = OpenAIContentPart::image_url("https://example.com/img.jpg", ImageDetail::High);
115 ///
116 /// // Preferred approach:
117 /// use open_agent::ImageBlock;
118 /// let image = ImageBlock::from_url("https://example.com/img.jpg").expect("Valid URL");
119 /// let part = OpenAIContentPart::from_image(&image);
120 /// ```
121 #[deprecated(
122 since = "0.6.0",
123 note = "Use `from_image()` instead to ensure proper validation"
124 )]
125 pub fn image_url(url: impl Into<String>, detail: ImageDetail) -> Self {
126 Self::ImageUrl {
127 image_url: OpenAIImageUrl {
128 url: url.into(),
129 detail: Some(detail.to_string()),
130 },
131 }
132 }
133}
134
135/// OpenAI image URL structure.
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct OpenAIImageUrl {
138 /// Image URL or data URI
139 pub url: String,
140 /// Detail level: "low", "high", or "auto"
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub detail: Option<String>,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct OpenAIMessage {
147 /// Message role as a string ("system", "user", "assistant", "tool").
148 pub role: String,
149
150 /// Message content (string for text-only, array for text+images).
151 #[serde(skip_serializing_if = "Option::is_none")]
152 pub content: Option<OpenAIContent>,
153
154 /// Tool calls requested by the assistant (assistant messages only).
155 ///
156 /// When the model wants to call tools, this field contains the list
157 /// of tool invocations with their parameters. Only present in assistant
158 /// messages.
159 #[serde(skip_serializing_if = "Option::is_none")]
160 pub tool_calls: Option<Vec<OpenAIToolCall>>,
161
162 /// ID of the tool call this message is responding to (tool messages only).
163 ///
164 /// When sending tool results back to the model, this field links the
165 /// result to the original tool call request. Only present in tool
166 /// messages.
167 #[serde(skip_serializing_if = "Option::is_none")]
168 pub tool_call_id: Option<String>,
169}
170
171/// OpenAI tool call representation in API messages.
172///
173/// Represents a request from the model to execute a specific function/tool.
174/// This is the wire format used in the OpenAI API, distinct from the internal
175/// [`ToolUseBlock`] representation.
176///
177/// # Structure
178///
179/// Each tool call has:
180/// - A unique ID for correlation with results
181/// - A type (always "function" in current OpenAI API)
182/// - Function details (name and arguments)
183///
184/// # Example JSON
185///
186/// ```json
187/// {
188/// "id": "call_abc123",
189/// "type": "function",
190/// "function": {
191/// "name": "get_weather",
192/// "arguments": "{\"location\":\"San Francisco\"}"
193/// }
194/// }
195/// ```
196#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct OpenAIToolCall {
198 /// Unique identifier for this tool call.
199 ///
200 /// Generated by the model. Used to correlate tool results back to
201 /// this specific call.
202 pub id: String,
203
204 /// Type of the call (always "function" in current API).
205 ///
206 /// The `rename` attribute ensures this serializes as `"type"` in JSON
207 /// since `type` is a Rust keyword.
208 #[serde(rename = "type")]
209 pub call_type: String,
210
211 /// Function/tool details (name and arguments).
212 pub function: OpenAIFunction,
213}
214
215/// OpenAI function call details.
216///
217/// Contains the function name and its arguments in the OpenAI API format.
218/// Note that arguments are serialized as a JSON string, not a JSON object,
219/// which is an OpenAI API quirk.
220///
221/// # Arguments Format
222///
223/// The `arguments` field is a **JSON string**, not a parsed JSON object.
224/// For example: `"{\"x\": 1, \"y\": 2}"` not `{"x": 1, "y": 2}`.
225/// This must be parsed before use.
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct OpenAIFunction {
228 /// Name of the function/tool to call.
229 pub name: String,
230
231 /// Function arguments as a **JSON string** (OpenAI API quirk).
232 ///
233 /// Must be parsed as JSON before use. For example, this might contain
234 /// the string `"{\"location\":\"NYC\",\"units\":\"fahrenheit\"}"` which
235 /// needs to be parsed into an actual JSON value.
236 pub arguments: String,
237}
238
239/// Complete request payload for OpenAI chat completions API.
240///
241/// This struct is serialized and sent as the request body when making
242/// API calls to OpenAI-compatible endpoints. It includes the model,
243/// conversation history, and configuration parameters.
244///
245/// # Streaming
246///
247/// The SDK always uses streaming mode (`stream: true`) to enable real-time
248/// response processing and better user experience.
249///
250/// # Optional Fields
251///
252/// Fields marked with `skip_serializing_if` are omitted from the JSON payload
253/// when `None`, allowing the API provider to use its defaults.
254///
255/// # Example
256///
257/// ```ignore
258/// use open_agent_sdk::types::{OpenAIRequest, OpenAIMessage};
259///
260/// let request = OpenAIRequest {
261/// model: "gpt-4".to_string(),
262/// messages: vec![
263/// OpenAIMessage {
264/// role: "user".to_string(),
265/// content: "Hello!".to_string(),
266/// tool_calls: None,
267/// tool_call_id: None,
268/// }
269/// ],
270/// stream: true,
271/// max_tokens: Some(1000),
272/// temperature: Some(0.7),
273/// tools: None,
274/// };
275/// ```
276#[derive(Debug, Clone, Serialize)]
277pub struct OpenAIRequest {
278 /// Model identifier (e.g., "gpt-4", "qwen2.5-32b-instruct").
279 pub model: String,
280
281 /// Conversation history as a sequence of messages.
282 ///
283 /// Includes system prompt, user messages, assistant responses, and
284 /// tool results. Order matters - messages are processed sequentially.
285 pub messages: Vec<OpenAIMessage>,
286
287 /// Whether to stream the response.
288 ///
289 /// The SDK always sets this to `true` for better user experience.
290 /// Streaming allows incremental processing of responses rather than
291 /// waiting for the entire completion.
292 pub stream: bool,
293
294 /// Maximum tokens to generate (optional).
295 ///
296 /// `None` uses the provider's default. Some providers require this
297 /// to be set explicitly.
298 #[serde(skip_serializing_if = "Option::is_none")]
299 pub max_tokens: Option<u32>,
300
301 /// Sampling temperature (optional).
302 ///
303 /// `None` uses the provider's default. Controls randomness in
304 /// generation.
305 #[serde(skip_serializing_if = "Option::is_none")]
306 pub temperature: Option<f32>,
307
308 /// Tools/functions available to the model (optional).
309 ///
310 /// When present, enables function calling. Each tool is described
311 /// with a JSON schema defining its parameters. `None` means no
312 /// tools are available.
313 #[serde(skip_serializing_if = "Option::is_none")]
314 pub tools: Option<Vec<serde_json::Value>>,
315}