openai_tools/chat/request.rs
1//! OpenAI Chat Completions API Request Module
2//!
3//! This module provides the functionality to build and send requests to the OpenAI Chat Completions API.
4//! It offers a builder pattern for constructing requests with various parameters and options,
5//! making it easy to interact with OpenAI's conversational AI models.
6//!
7//! # Key Features
8//!
9//! - **Builder Pattern**: Fluent API for constructing requests
10//! - **Structured Output**: Support for JSON schema-based responses
11//! - **Function Calling**: Tool integration for extended model capabilities
12//! - **Comprehensive Parameters**: Full support for all OpenAI API parameters
13//! - **Error Handling**: Robust error management and validation
14//!
15//! # Quick Start
16//!
17//! ```rust,no_run
18//! use openai_tools::chat::request::ChatCompletion;
19//! use openai_tools::common::message::Message;
20//! use openai_tools::common::role::Role;
21//!
22//! #[tokio::main]
23//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
24//! // Initialize the chat completion client
25//! let mut chat = ChatCompletion::new();
26//!
27//! // Create a simple conversation
28//! let messages = vec![
29//! Message::from_string(Role::User, "Hello! How are you?")
30//! ];
31//!
32//! // Send the request and get a response
33//! let response = chat
34//! .model_id("gpt-4o-mini")
35//! .messages(messages)
36//! .temperature(0.7)
37//! .chat()
38//! .await?;
39//!
40//! println!("AI Response: {}",
41//! response.choices[0].message.content.as_ref().unwrap().text.as_ref().unwrap());
42//! Ok(())
43//! }
44//! ```
45//!
46//! # Advanced Usage
47//!
48//! ## Structured Output with JSON Schema
49//!
50//! ```rust,no_run
51//! use openai_tools::chat::request::ChatCompletion;
52//! use openai_tools::common::message::Message;
53//! use openai_tools::common::role::Role;
54//! use openai_tools::common::structured_output::Schema;
55//! use serde::{Deserialize, Serialize};
56//!
57//! #[derive(Serialize, Deserialize)]
58//! struct PersonInfo {
59//! name: String,
60//! age: u32,
61//! occupation: String,
62//! }
63//!
64//! #[tokio::main]
65//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
66//! let mut chat = ChatCompletion::new();
67//!
68//! // Define JSON schema for structured output
69//! let mut schema = Schema::chat_json_schema("person_info");
70//! schema.add_property("name", "string", "Person's full name");
71//! schema.add_property("age", "number", "Person's age in years");
72//! schema.add_property("occupation", "string", "Person's job or profession");
73//!
74//! let messages = vec![
75//! Message::from_string(Role::User,
76//! "Extract information about: John Smith, 30 years old, software engineer")
77//! ];
78//!
79//! let response = chat
80//! .model_id("gpt-4o-mini")
81//! .messages(messages)
82//! .json_schema(schema)
83//! .chat()
84//! .await?;
85//!
86//! // Parse structured response
87//! let person: PersonInfo = serde_json::from_str(
88//! response.choices[0].message.content.as_ref().unwrap().text.as_ref().unwrap()
89//! )?;
90//!
91//! println!("Extracted: {} (age: {}, job: {})",
92//! person.name, person.age, person.occupation);
93//! Ok(())
94//! }
95//! ```
96//!
97//! ## Function Calling with Tools
98//!
99//! ```rust,no_run
100//! use openai_tools::chat::request::ChatCompletion;
101//! use openai_tools::common::message::Message;
102//! use openai_tools::common::role::Role;
103//! use openai_tools::common::tool::Tool;
104//! use openai_tools::common::parameters::ParameterProperty;
105//!
106//! #[tokio::main]
107//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
108//! let mut chat = ChatCompletion::new();
109//!
110//! // Define a weather checking tool
111//! let weather_tool = Tool::function(
112//! "get_weather",
113//! "Get current weather information for a location",
114//! vec![
115//! ("location", ParameterProperty::from_string("The city and country")),
116//! ("unit", ParameterProperty::from_string("Temperature unit (celsius/fahrenheit)")),
117//! ],
118//! false,
119//! );
120//!
121//! let messages = vec![
122//! Message::from_string(Role::User,
123//! "What's the weather like in Tokyo today?")
124//! ];
125//!
126//! let response = chat
127//! .model_id("gpt-4o-mini")
128//! .messages(messages)
129//! .tools(vec![weather_tool])
130//! .temperature(0.1)
131//! .chat()
132//! .await?;
133//!
134//! // Handle tool calls
135//! if let Some(tool_calls) = &response.choices[0].message.tool_calls {
136//! for call in tool_calls {
137//! println!("Tool called: {}", call.function.name);
138//! if let Ok(args) = call.function.arguments_as_map() {
139//! println!("Arguments: {:?}", args);
140//! }
141//! // Execute the function and continue the conversation...
142//! }
143//! }
144//! Ok(())
145//! }
146//! ```
147//!
148//! # Environment Setup
149//!
150//! Before using this module, ensure you have set up your OpenAI API key:
151//!
152//! ```bash
153//! export OPENAI_API_KEY="your-api-key-here"
154//! ```
155//!
156//! Or create a `.env` file in your project root:
157//!
158//! ```text
159//! OPENAI_API_KEY=your-api-key-here
160//! ```
161//!
162//!
163//! # Error Handling
164//!
165//! All methods return a `Result` type for proper error handling:
166//!
167//! ```rust,no_run
168//! use openai_tools::chat::request::ChatCompletion;
169//! use openai_tools::common::errors::OpenAIToolError;
170//!
171//! #[tokio::main]
172//! async fn main() {
173//! let mut chat = ChatCompletion::new();
174//!
175//! match chat.model_id("gpt-4o-mini").chat().await {
176//! Ok(response) => {
177//! if let Some(content) = &response.choices[0].message.content {
178//! if let Some(text) = &content.text {
179//! println!("Success: {}", text);
180//! }
181//! }
182//! }
183//! Err(OpenAIToolError::RequestError(e)) => {
184//! eprintln!("Network error: {}", e);
185//! }
186//! Err(OpenAIToolError::SerdeJsonError(e)) => {
187//! eprintln!("JSON parsing error: {}", e);
188//! }
189//! Err(e) => {
190//! eprintln!("Other error: {}", e);
191//! }
192//! }
193//! }
194//! ```
195
196use crate::chat::response::Response;
197use crate::common::{
198 auth::{AuthProvider, OpenAIAuth},
199 client::create_http_client,
200 errors::{ErrorResponse, OpenAIToolError, Result},
201 message::{Content, Message},
202 models::{ChatModel, ParameterRestriction},
203 structured_output::Schema,
204 tool::Tool,
205};
206use core::str;
207use serde::{Deserialize, Serialize};
208use std::collections::HashMap;
209use std::time::Duration;
210
211/// Response format structure for OpenAI API requests
212///
213/// This structure is used for structured output when JSON schema is specified.
214#[derive(Debug, Clone, Deserialize, Serialize)]
215pub(crate) struct Format {
216 #[serde(rename = "type")]
217 type_name: String,
218 json_schema: Schema,
219}
220
221impl Format {
222 /// Creates a new Format structure
223 ///
224 /// # Arguments
225 ///
226 /// * `type_name` - The type name for the response format
227 /// * `json_schema` - The JSON schema definition
228 ///
229 /// # Returns
230 ///
231 /// A new Format structure instance
232 pub fn new<T: AsRef<str>>(type_name: T, json_schema: Schema) -> Self {
233 Self { type_name: type_name.as_ref().to_string(), json_schema }
234 }
235}
236
237// =============================================================================
238// Chat API serialization wrappers
239//
240// The shared `Content` type uses Responses API format ("input_text", "input_image"),
241// but Chat Completions API expects different type names and structure:
242// - "input_text" → {"type": "text", "text": "..."}
243// - "input_image" → {"type": "image_url", "image_url": {"url": "..."}}
244//
245// These zero-copy wrappers convert at serialization time without changing
246// the public API or affecting the Responses API path.
247// =============================================================================
248
249/// Wraps `&Content` to serialize in Chat Completions API format.
250struct ChatContentRef<'a>(&'a Content);
251
252impl<'a> Serialize for ChatContentRef<'a> {
253 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
254 where
255 S: serde::Serializer,
256 {
257 use serde::ser::SerializeStruct;
258
259 match self.0.type_name.as_str() {
260 "input_text" => {
261 let mut state = serializer.serialize_struct("Content", 2)?;
262 state.serialize_field("type", "text")?;
263 state.serialize_field("text", &self.0.text)?;
264 state.end()
265 }
266 "input_image" => {
267 #[derive(Serialize)]
268 struct ImageUrl<'b> {
269 url: &'b str,
270 }
271 let mut state = serializer.serialize_struct("Content", 2)?;
272 state.serialize_field("type", "image_url")?;
273 if let Some(ref url) = self.0.image_url {
274 state.serialize_field("image_url", &ImageUrl { url })?;
275 }
276 state.end()
277 }
278 other => {
279 // Pass through unknown types as-is
280 let mut state = serializer.serialize_struct("Content", 3)?;
281 state.serialize_field("type", other)?;
282 if let Some(ref text) = self.0.text {
283 state.serialize_field("text", text)?;
284 }
285 if let Some(ref url) = self.0.image_url {
286 state.serialize_field("image_url", url)?;
287 }
288 state.end()
289 }
290 }
291 }
292}
293
294/// Wraps `&Message` to serialize in Chat Completions API format.
295///
296/// - Single content (`content` field): extracts `.text` as a plain string (existing behavior)
297/// - Content list (`content_list` field): wraps each element with `ChatContentRef`
298struct ChatMessageRef<'a>(&'a Message);
299
300impl<'a> Serialize for ChatMessageRef<'a> {
301 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
302 where
303 S: serde::Serializer,
304 {
305 use serde::ser::SerializeStruct;
306
307 let msg = self.0;
308 let mut state = serializer.serialize_struct("Message", 3)?;
309 state.serialize_field("role", &msg.role)?;
310
311 if let Some(ref content) = msg.content {
312 // Single content: serialize as plain text string
313 state.serialize_field("content", &content.text)?;
314 } else if let Some(ref contents) = msg.content_list {
315 // Multi-modal content: wrap each element with ChatContentRef
316 let chat_contents: Vec<ChatContentRef<'_>> = contents.iter().map(ChatContentRef).collect();
317 state.serialize_field("content", &chat_contents)?;
318 }
319
320 if let Some(ref tool_call_id) = msg.tool_call_id {
321 state.serialize_field("tool_call_id", tool_call_id)?;
322 }
323 if let Some(ref tool_calls) = msg.tool_calls {
324 state.serialize_field("tool_calls", tool_calls)?;
325 }
326
327 state.end()
328 }
329}
330
331/// Custom serializer for `Vec<Message>` that converts to Chat API format.
332fn serialize_chat_messages<S>(messages: &Vec<Message>, serializer: S) -> std::result::Result<S::Ok, S::Error>
333where
334 S: serde::Serializer,
335{
336 use serde::ser::SerializeSeq;
337 let mut seq = serializer.serialize_seq(Some(messages.len()))?;
338 for msg in messages {
339 seq.serialize_element(&ChatMessageRef(msg))?;
340 }
341 seq.end()
342}
343
344/// Request body structure for OpenAI Chat Completions API
345///
346/// This structure represents the parameters that will be sent in the request body
347/// to the OpenAI API. Each field corresponds to the API specification.
348#[derive(Debug, Clone, Deserialize, Serialize, Default)]
349pub(crate) struct Body {
350 pub(crate) model: ChatModel,
351 #[serde(serialize_with = "serialize_chat_messages")]
352 pub(crate) messages: Vec<Message>,
353 /// Whether to store the request and response at OpenAI
354 #[serde(skip_serializing_if = "Option::is_none")]
355 pub(crate) store: Option<bool>,
356 /// Frequency penalty parameter to reduce repetition (-2.0 to 2.0)
357 #[serde(skip_serializing_if = "Option::is_none")]
358 pub(crate) frequency_penalty: Option<f32>,
359 /// Logit bias to adjust the probability of specific tokens
360 #[serde(skip_serializing_if = "Option::is_none")]
361 pub(crate) logit_bias: Option<HashMap<String, i32>>,
362 /// Whether to include probability information for each token
363 #[serde(skip_serializing_if = "Option::is_none")]
364 pub(crate) logprobs: Option<bool>,
365 /// Number of top probabilities to return for each token (0-20)
366 #[serde(skip_serializing_if = "Option::is_none")]
367 pub(crate) top_logprobs: Option<u8>,
368 /// Maximum number of tokens to generate
369 #[serde(skip_serializing_if = "Option::is_none")]
370 pub(crate) max_completion_tokens: Option<u64>,
371 /// Number of responses to generate
372 #[serde(skip_serializing_if = "Option::is_none")]
373 pub(crate) n: Option<u32>,
374 /// Available modalities for the response (e.g., text, audio)
375 #[serde(skip_serializing_if = "Option::is_none")]
376 pub(crate) modalities: Option<Vec<String>>,
377 /// Presence penalty to encourage new topics (-2.0 to 2.0)
378 #[serde(skip_serializing_if = "Option::is_none")]
379 pub(crate) presence_penalty: Option<f32>,
380 /// Temperature parameter to control response randomness (0.0 to 2.0)
381 #[serde(skip_serializing_if = "Option::is_none")]
382 pub(crate) temperature: Option<f32>,
383 /// Response format specification (e.g., JSON schema)
384 #[serde(skip_serializing_if = "Option::is_none")]
385 pub(crate) response_format: Option<Format>,
386 /// Optional tools that can be used by the model
387 #[serde(skip_serializing_if = "Option::is_none")]
388 pub(crate) tools: Option<Vec<Tool>>,
389 /// A stable identifier for the end user, used for safety monitoring and abuse detection
390 #[serde(skip_serializing_if = "Option::is_none")]
391 pub(crate) safety_identifier: Option<String>,
392 #[serde(skip_serializing_if = "Option::is_none")]
393 pub(crate) user: Option<String>,
394}
395
396/// OpenAI Chat Completions API client
397///
398/// This structure manages interactions with the OpenAI Chat Completions API.
399/// It handles API key management, request parameter configuration, and API calls.
400///
401/// # Example
402///
403/// ```rust
404/// use openai_tools::chat::request::ChatCompletion;
405/// use openai_tools::common::message::Message;
406/// use openai_tools::common::role::Role;
407///
408/// # #[tokio::main]
409/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
410/// let mut chat = ChatCompletion::new();
411/// let messages = vec![Message::from_string(Role::User, "Hello!")];
412///
413/// let response = chat
414/// .model_id("gpt-4o-mini")
415/// .messages(messages)
416/// .temperature(1.0)
417/// .chat()
418/// .await?;
419/// # Ok::<(), Box<dyn std::error::Error>>(())
420/// # }
421/// ```
422/// Default API path for Chat Completions
423const CHAT_COMPLETIONS_PATH: &str = "chat/completions";
424
425/// OpenAI Chat Completions API client
426///
427/// This structure manages interactions with the OpenAI Chat Completions API
428/// and Azure OpenAI API. It handles authentication, request parameter
429/// configuration, and API calls.
430///
431/// # Providers
432///
433/// The client supports two providers:
434/// - **OpenAI**: Standard OpenAI API (default)
435/// - **Azure**: Azure OpenAI Service
436///
437/// # Examples
438///
439/// ## OpenAI (existing behavior - unchanged)
440///
441/// ```rust,no_run
442/// use openai_tools::chat::request::ChatCompletion;
443/// use openai_tools::common::message::Message;
444/// use openai_tools::common::role::Role;
445///
446/// # #[tokio::main]
447/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
448/// let mut chat = ChatCompletion::new();
449/// let messages = vec![Message::from_string(Role::User, "Hello!")];
450///
451/// let response = chat
452/// .model_id("gpt-4o-mini")
453/// .messages(messages)
454/// .chat()
455/// .await?;
456/// # Ok(())
457/// # }
458/// ```
459///
460/// ## Azure OpenAI
461///
462/// ```rust,no_run
463/// use openai_tools::chat::request::ChatCompletion;
464/// use openai_tools::common::message::Message;
465/// use openai_tools::common::role::Role;
466///
467/// # #[tokio::main]
468/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
469/// // From environment variables
470/// let mut chat = ChatCompletion::azure()?;
471///
472/// let messages = vec![Message::from_string(Role::User, "Hello!")];
473/// let response = chat.messages(messages).chat().await?;
474/// # Ok(())
475/// # }
476/// ```
477#[derive(Debug, Clone)]
478pub struct ChatCompletion {
479 /// Authentication provider (OpenAI or Azure)
480 auth: AuthProvider,
481 /// The request body containing all parameters for the API call
482 pub(crate) request_body: Body,
483 /// Optional request timeout duration
484 timeout: Option<Duration>,
485}
486
487impl Default for ChatCompletion {
488 fn default() -> Self {
489 Self::new()
490 }
491}
492
493impl ChatCompletion {
494 /// Creates a new ChatCompletion instance for OpenAI API
495 ///
496 /// Loads the API key from the `OPENAI_API_KEY` environment variable.
497 /// If a `.env` file exists, it will also be loaded.
498 ///
499 /// # Panics
500 ///
501 /// Panics if the `OPENAI_API_KEY` environment variable is not set.
502 ///
503 /// # Returns
504 ///
505 /// A new ChatCompletion instance configured for OpenAI API
506 ///
507 /// # Example
508 ///
509 /// ```rust,no_run
510 /// use openai_tools::chat::request::ChatCompletion;
511 ///
512 /// let mut chat = ChatCompletion::new();
513 /// ```
514 pub fn new() -> Self {
515 let auth = AuthProvider::openai_from_env().map_err(|e| OpenAIToolError::Error(format!("Failed to load OpenAI auth: {}", e))).unwrap();
516 Self { auth, request_body: Body::default(), timeout: None }
517 }
518
519 /// Creates a new ChatCompletion instance with a specified model
520 ///
521 /// This is the recommended constructor as it enables parameter validation
522 /// at setter time. When you set parameters like `temperature()`, the model's
523 /// parameter support is checked and warnings are logged for unsupported values.
524 ///
525 /// # Arguments
526 ///
527 /// * `model` - The model to use for chat completion
528 ///
529 /// # Panics
530 ///
531 /// Panics if the `OPENAI_API_KEY` environment variable is not set.
532 ///
533 /// # Returns
534 ///
535 /// A new ChatCompletion instance with the specified model
536 ///
537 /// # Example
538 ///
539 /// ```rust,no_run
540 /// use openai_tools::chat::request::ChatCompletion;
541 /// use openai_tools::common::models::ChatModel;
542 ///
543 /// // Recommended: specify model at creation time
544 /// let mut chat = ChatCompletion::with_model(ChatModel::Gpt4oMini);
545 ///
546 /// // For reasoning models, unsupported parameters are validated at setter time
547 /// let mut reasoning_chat = ChatCompletion::with_model(ChatModel::O3Mini);
548 /// reasoning_chat.temperature(0.5); // Warning logged, value ignored
549 /// ```
550 pub fn with_model(model: ChatModel) -> Self {
551 let auth = AuthProvider::openai_from_env().map_err(|e| OpenAIToolError::Error(format!("Failed to load OpenAI auth: {}", e))).unwrap();
552 Self { auth, request_body: Body { model, ..Default::default() }, timeout: None }
553 }
554
555 /// Creates a new ChatCompletion instance with a custom authentication provider
556 ///
557 /// Use this to explicitly configure OpenAI or Azure authentication.
558 ///
559 /// # Arguments
560 ///
561 /// * `auth` - The authentication provider
562 ///
563 /// # Returns
564 ///
565 /// A new ChatCompletion instance with the specified auth provider
566 ///
567 /// # Example
568 ///
569 /// ```rust
570 /// use openai_tools::chat::request::ChatCompletion;
571 /// use openai_tools::common::auth::{AuthProvider, AzureAuth};
572 ///
573 /// // Explicit Azure configuration with complete base URL
574 /// let auth = AuthProvider::Azure(
575 /// AzureAuth::new(
576 /// "api-key",
577 /// "https://my-resource.openai.azure.com/openai/deployments/gpt-4o?api-version=2024-08-01-preview"
578 /// )
579 /// );
580 /// let mut chat = ChatCompletion::with_auth(auth);
581 /// ```
582 pub fn with_auth(auth: AuthProvider) -> Self {
583 Self { auth, request_body: Body::default(), timeout: None }
584 }
585
586 /// Creates a new ChatCompletion instance for Azure OpenAI API
587 ///
588 /// Loads configuration from Azure-specific environment variables.
589 ///
590 /// # Returns
591 ///
592 /// `Result<ChatCompletion>` - Configured for Azure or error if env vars missing
593 ///
594 /// # Environment Variables
595 ///
596 /// | Variable | Required | Description |
597 /// |----------|----------|-------------|
598 /// | `AZURE_OPENAI_API_KEY` | Yes | Azure API key |
599 /// | `AZURE_OPENAI_BASE_URL` | Yes | Complete endpoint URL including deployment, API path, and api-version |
600 ///
601 /// # Example
602 ///
603 /// ```rust,no_run
604 /// use openai_tools::chat::request::ChatCompletion;
605 ///
606 /// // With environment variables:
607 /// // AZURE_OPENAI_API_KEY=xxx
608 /// // AZURE_OPENAI_BASE_URL=https://my-resource.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-08-01-preview
609 /// let mut chat = ChatCompletion::azure()?;
610 /// # Ok::<(), openai_tools::common::errors::OpenAIToolError>(())
611 /// ```
612 pub fn azure() -> Result<Self> {
613 let auth = AuthProvider::azure_from_env()?;
614 Ok(Self { auth, request_body: Body::default(), timeout: None })
615 }
616
617 /// Creates a new ChatCompletion instance by auto-detecting the provider
618 ///
619 /// Tries Azure first (if AZURE_OPENAI_API_KEY is set), then falls back to OpenAI.
620 ///
621 /// # Returns
622 ///
623 /// `Result<ChatCompletion>` - Auto-configured client or error
624 ///
625 /// # Example
626 ///
627 /// ```rust,no_run
628 /// use openai_tools::chat::request::ChatCompletion;
629 ///
630 /// // Uses Azure if AZURE_OPENAI_API_KEY is set, otherwise OpenAI
631 /// let mut chat = ChatCompletion::detect_provider()?;
632 /// # Ok::<(), openai_tools::common::errors::OpenAIToolError>(())
633 /// ```
634 pub fn detect_provider() -> Result<Self> {
635 let auth = AuthProvider::from_env()?;
636 Ok(Self { auth, request_body: Body::default(), timeout: None })
637 }
638
639 /// Creates a new ChatCompletion instance with URL-based provider detection
640 ///
641 /// Analyzes the URL pattern to determine the provider:
642 /// - URLs containing `.openai.azure.com` → Azure
643 /// - All other URLs → OpenAI-compatible
644 ///
645 /// # Arguments
646 ///
647 /// * `base_url` - The complete base URL for API requests
648 /// * `api_key` - The API key or token
649 ///
650 /// # Returns
651 ///
652 /// `ChatCompletion` - Configured client
653 ///
654 /// # Example
655 ///
656 /// ```rust
657 /// use openai_tools::chat::request::ChatCompletion;
658 ///
659 /// // OpenAI-compatible API (e.g., local Ollama)
660 /// let chat = ChatCompletion::with_url(
661 /// "http://localhost:11434/v1",
662 /// "ollama",
663 /// );
664 ///
665 /// // Azure OpenAI (complete base URL)
666 /// let azure_chat = ChatCompletion::with_url(
667 /// "https://my-resource.openai.azure.com/openai/deployments/gpt-4o?api-version=2024-08-01-preview",
668 /// "azure-key",
669 /// );
670 /// ```
671 pub fn with_url<S: Into<String>>(base_url: S, api_key: S) -> Self {
672 let auth = AuthProvider::from_url_with_key(base_url, api_key);
673 Self { auth, request_body: Body::default(), timeout: None }
674 }
675
676 /// Creates a new ChatCompletion instance from URL using environment variables
677 ///
678 /// Analyzes the URL pattern to determine the provider, then loads
679 /// credentials from the appropriate environment variables.
680 ///
681 /// # Arguments
682 ///
683 /// * `base_url` - The complete base URL for API requests
684 ///
685 /// # Environment Variables
686 ///
687 /// For Azure URLs (`*.openai.azure.com`):
688 /// - `AZURE_OPENAI_API_KEY` (required)
689 ///
690 /// For other URLs:
691 /// - `OPENAI_API_KEY` (required)
692 ///
693 /// # Returns
694 ///
695 /// `Result<ChatCompletion>` - Configured client or error
696 ///
697 /// # Example
698 ///
699 /// ```rust,no_run
700 /// use openai_tools::chat::request::ChatCompletion;
701 ///
702 /// // Uses OPENAI_API_KEY from environment
703 /// let chat = ChatCompletion::from_url("https://api.openai.com/v1")?;
704 ///
705 /// // Uses AZURE_OPENAI_API_KEY from environment (complete base URL)
706 /// let azure = ChatCompletion::from_url(
707 /// "https://my-resource.openai.azure.com/openai/deployments/gpt-4o?api-version=2024-08-01-preview"
708 /// )?;
709 /// # Ok::<(), openai_tools::common::errors::OpenAIToolError>(())
710 /// ```
711 pub fn from_url<S: Into<String>>(base_url: S) -> Result<Self> {
712 let auth = AuthProvider::from_url(base_url)?;
713 Ok(Self { auth, request_body: Body::default(), timeout: None })
714 }
715
716 /// Returns the authentication provider
717 ///
718 /// # Returns
719 ///
720 /// Reference to the authentication provider
721 pub fn auth(&self) -> &AuthProvider {
722 &self.auth
723 }
724
725 /// Sets a custom API endpoint URL (OpenAI only)
726 ///
727 /// Use this to point to alternative OpenAI-compatible APIs (e.g., proxy servers).
728 /// For Azure, use `azure()` or `with_auth()` instead.
729 ///
730 /// # Arguments
731 ///
732 /// * `url` - The base URL (e.g., "https://my-proxy.example.com/v1")
733 ///
734 /// # Returns
735 ///
736 /// A mutable reference to self for method chaining
737 ///
738 /// # Note
739 ///
740 /// This method only works with OpenAI authentication. For Azure, the endpoint
741 /// is constructed from resource name and deployment name.
742 ///
743 /// # Example
744 ///
745 /// ```rust,no_run
746 /// use openai_tools::chat::request::ChatCompletion;
747 ///
748 /// let mut chat = ChatCompletion::new();
749 /// chat.base_url("https://my-proxy.example.com/v1");
750 /// ```
751 pub fn base_url<T: AsRef<str>>(&mut self, url: T) -> &mut Self {
752 // Only modify if OpenAI provider
753 if let AuthProvider::OpenAI(ref openai_auth) = self.auth {
754 let new_auth = OpenAIAuth::new(openai_auth.api_key()).with_base_url(url.as_ref());
755 self.auth = AuthProvider::OpenAI(new_auth);
756 } else {
757 tracing::warn!("base_url() is only supported for OpenAI provider. Use azure() or with_auth() for Azure.");
758 }
759 self
760 }
761
762 /// Sets the model to use for chat completion.
763 ///
764 /// # Arguments
765 ///
766 /// * `model` - The model to use (e.g., `ChatModel::Gpt4oMini`, `ChatModel::Gpt4o`)
767 ///
768 /// # Returns
769 ///
770 /// A mutable reference to self for method chaining
771 ///
772 /// # Example
773 ///
774 /// ```rust,no_run
775 /// use openai_tools::chat::request::ChatCompletion;
776 /// use openai_tools::common::models::ChatModel;
777 ///
778 /// let mut chat = ChatCompletion::new();
779 /// chat.model(ChatModel::Gpt4oMini);
780 /// ```
781 pub fn model(&mut self, model: ChatModel) -> &mut Self {
782 self.request_body.model = model;
783 self
784 }
785
786 /// Sets the model using a string ID (for backward compatibility).
787 ///
788 /// Prefer using [`model`] with `ChatModel` enum for type safety.
789 ///
790 /// # Arguments
791 ///
792 /// * `model_id` - OpenAI model ID string (e.g., "gpt-4o-mini")
793 ///
794 /// # Returns
795 ///
796 /// A mutable reference to self for method chaining
797 ///
798 /// # Example
799 ///
800 /// ```rust,no_run
801 /// use openai_tools::chat::request::ChatCompletion;
802 ///
803 /// let mut chat = ChatCompletion::new();
804 /// chat.model_id("gpt-4o-mini");
805 /// ```
806 #[deprecated(since = "0.2.0", note = "Use `model(ChatModel)` instead for type safety")]
807 pub fn model_id<T: AsRef<str>>(&mut self, model_id: T) -> &mut Self {
808 self.request_body.model = ChatModel::from(model_id.as_ref());
809 self
810 }
811
812 /// Sets the request timeout duration
813 ///
814 /// # Arguments
815 ///
816 /// * `timeout` - The maximum time to wait for a response
817 ///
818 /// # Returns
819 ///
820 /// A mutable reference to self for method chaining
821 ///
822 /// # Example
823 ///
824 /// ```rust,no_run
825 /// use std::time::Duration;
826 /// use openai_tools::chat::request::ChatCompletion;
827 ///
828 /// let mut chat = ChatCompletion::new();
829 /// chat.model_id("gpt-4o-mini")
830 /// .timeout(Duration::from_secs(30));
831 /// ```
832 pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
833 self.timeout = Some(timeout);
834 self
835 }
836
837 /// Sets the chat message history
838 ///
839 /// # Arguments
840 ///
841 /// * `messages` - Vector of chat messages representing the conversation history
842 ///
843 /// # Returns
844 ///
845 /// A mutable reference to self for method chaining
846 pub fn messages(&mut self, messages: Vec<Message>) -> &mut Self {
847 self.request_body.messages = messages;
848 self
849 }
850
851 /// Adds a single message to the conversation history
852 ///
853 /// This method appends a new message to the existing conversation history.
854 /// It's useful for building conversations incrementally.
855 ///
856 /// # Arguments
857 ///
858 /// * `message` - The message to add to the conversation
859 ///
860 /// # Returns
861 ///
862 /// A mutable reference to self for method chaining
863 ///
864 /// # Examples
865 ///
866 /// ```rust,no_run
867 /// use openai_tools::chat::request::ChatCompletion;
868 /// use openai_tools::common::message::Message;
869 /// use openai_tools::common::role::Role;
870 ///
871 /// let mut chat = ChatCompletion::new();
872 /// chat.add_message(Message::from_string(Role::User, "Hello!"))
873 /// .add_message(Message::from_string(Role::Assistant, "Hi there!"))
874 /// .add_message(Message::from_string(Role::User, "How are you?"));
875 /// ```
876 pub fn add_message(&mut self, message: Message) -> &mut Self {
877 self.request_body.messages.push(message);
878 self
879 }
880 /// Sets whether to store the request and response at OpenAI
881 ///
882 /// # Arguments
883 ///
884 /// * `store` - `true` to store, `false` to not store
885 ///
886 /// # Returns
887 ///
888 /// A mutable reference to self for method chaining
889 pub fn store(&mut self, store: bool) -> &mut Self {
890 self.request_body.store = Option::from(store);
891 self
892 }
893
894 /// Sets the frequency penalty
895 ///
896 /// A parameter that penalizes based on word frequency to reduce repetition.
897 /// Positive values decrease repetition, negative values increase it.
898 ///
899 /// **Note:** Reasoning models (GPT-5, o-series) only support frequency_penalty=0.
900 /// For these models, non-zero values will be ignored with a warning.
901 ///
902 /// # Arguments
903 ///
904 /// * `frequency_penalty` - Frequency penalty value (range: -2.0 to 2.0)
905 ///
906 /// # Returns
907 ///
908 /// A mutable reference to self for method chaining
909 pub fn frequency_penalty(&mut self, frequency_penalty: f32) -> &mut Self {
910 let support = self.request_body.model.parameter_support();
911 match support.frequency_penalty {
912 ParameterRestriction::FixedValue(fixed) => {
913 if (frequency_penalty as f64 - fixed).abs() > f64::EPSILON {
914 tracing::warn!(
915 "Model '{}' only supports frequency_penalty={}. Ignoring frequency_penalty={}.",
916 self.request_body.model,
917 fixed,
918 frequency_penalty
919 );
920 return self;
921 }
922 }
923 ParameterRestriction::NotSupported => {
924 tracing::warn!("Model '{}' does not support frequency_penalty parameter. Ignoring.", self.request_body.model);
925 return self;
926 }
927 ParameterRestriction::Any => {}
928 }
929 self.request_body.frequency_penalty = Some(frequency_penalty);
930 self
931 }
932
933 /// Sets logit bias to adjust the probability of specific tokens
934 ///
935 /// **Note:** Reasoning models (GPT-5, o-series) do not support logit_bias.
936 /// For these models, this parameter will be ignored with a warning.
937 ///
938 /// # Arguments
939 ///
940 /// * `logit_bias` - A map of token IDs to adjustment values
941 ///
942 /// # Returns
943 ///
944 /// A mutable reference to self for method chaining
945 pub fn logit_bias<T: AsRef<str>>(&mut self, logit_bias: HashMap<T, i32>) -> &mut Self {
946 let support = self.request_body.model.parameter_support();
947 if !support.logit_bias {
948 tracing::warn!("Model '{}' does not support logit_bias parameter. Ignoring.", self.request_body.model);
949 return self;
950 }
951 self.request_body.logit_bias = Some(logit_bias.into_iter().map(|(k, v)| (k.as_ref().to_string(), v)).collect::<HashMap<String, i32>>());
952 self
953 }
954
955 /// Sets whether to include probability information for each token
956 ///
957 /// **Note:** Reasoning models (GPT-5, o-series) do not support logprobs.
958 /// For these models, this parameter will be ignored with a warning.
959 ///
960 /// # Arguments
961 ///
962 /// * `logprobs` - `true` to include probability information
963 ///
964 /// # Returns
965 ///
966 /// A mutable reference to self for method chaining
967 pub fn logprobs(&mut self, logprobs: bool) -> &mut Self {
968 let support = self.request_body.model.parameter_support();
969 if !support.logprobs {
970 tracing::warn!("Model '{}' does not support logprobs parameter. Ignoring.", self.request_body.model);
971 return self;
972 }
973 self.request_body.logprobs = Some(logprobs);
974 self
975 }
976
977 /// Sets the number of top probabilities to return for each token
978 ///
979 /// **Note:** Reasoning models (GPT-5, o-series) do not support top_logprobs.
980 /// For these models, this parameter will be ignored with a warning.
981 ///
982 /// # Arguments
983 ///
984 /// * `top_logprobs` - Number of top probabilities (range: 0-20)
985 ///
986 /// # Returns
987 ///
988 /// A mutable reference to self for method chaining
989 pub fn top_logprobs(&mut self, top_logprobs: u8) -> &mut Self {
990 let support = self.request_body.model.parameter_support();
991 if !support.top_logprobs {
992 tracing::warn!("Model '{}' does not support top_logprobs parameter. Ignoring.", self.request_body.model);
993 return self;
994 }
995 self.request_body.top_logprobs = Some(top_logprobs);
996 self
997 }
998
999 /// Sets the maximum number of tokens to generate
1000 ///
1001 /// # Arguments
1002 ///
1003 /// * `max_completion_tokens` - Maximum number of tokens
1004 ///
1005 /// # Returns
1006 ///
1007 /// A mutable reference to self for method chaining
1008 pub fn max_completion_tokens(&mut self, max_completion_tokens: u64) -> &mut Self {
1009 self.request_body.max_completion_tokens = Option::from(max_completion_tokens);
1010 self
1011 }
1012
1013 /// Sets the number of responses to generate
1014 ///
1015 /// **Note:** Reasoning models (GPT-5, o-series) only support n=1.
1016 /// For these models, values other than 1 will be ignored with a warning.
1017 ///
1018 /// # Arguments
1019 ///
1020 /// * `n` - Number of responses to generate
1021 ///
1022 /// # Returns
1023 ///
1024 /// A mutable reference to self for method chaining
1025 pub fn n(&mut self, n: u32) -> &mut Self {
1026 let support = self.request_body.model.parameter_support();
1027 if !support.n_multiple && n != 1 {
1028 tracing::warn!("Model '{}' only supports n=1. Ignoring n={}.", self.request_body.model, n);
1029 return self;
1030 }
1031 self.request_body.n = Some(n);
1032 self
1033 }
1034
1035 /// Sets the available modalities for the response
1036 ///
1037 /// # Arguments
1038 ///
1039 /// * `modalities` - List of modalities (e.g., `["text", "audio"]`)
1040 ///
1041 /// # Returns
1042 ///
1043 /// A mutable reference to self for method chaining
1044 pub fn modalities<T: AsRef<str>>(&mut self, modalities: Vec<T>) -> &mut Self {
1045 self.request_body.modalities = Option::from(modalities.into_iter().map(|m| m.as_ref().to_string()).collect::<Vec<String>>());
1046 self
1047 }
1048
1049 /// Sets the presence penalty
1050 ///
1051 /// A parameter that controls the tendency to include new content in the document.
1052 /// Positive values encourage talking about new topics, negative values encourage
1053 /// staying on existing topics.
1054 ///
1055 /// **Note:** Reasoning models (GPT-5, o-series) only support presence_penalty=0.
1056 /// For these models, non-zero values will be ignored with a warning.
1057 ///
1058 /// # Arguments
1059 ///
1060 /// * `presence_penalty` - Presence penalty value (range: -2.0 to 2.0)
1061 ///
1062 /// # Returns
1063 ///
1064 /// A mutable reference to self for method chaining
1065 pub fn presence_penalty(&mut self, presence_penalty: f32) -> &mut Self {
1066 let support = self.request_body.model.parameter_support();
1067 match support.presence_penalty {
1068 ParameterRestriction::FixedValue(fixed) => {
1069 if (presence_penalty as f64 - fixed).abs() > f64::EPSILON {
1070 tracing::warn!(
1071 "Model '{}' only supports presence_penalty={}. Ignoring presence_penalty={}.",
1072 self.request_body.model,
1073 fixed,
1074 presence_penalty
1075 );
1076 return self;
1077 }
1078 }
1079 ParameterRestriction::NotSupported => {
1080 tracing::warn!("Model '{}' does not support presence_penalty parameter. Ignoring.", self.request_body.model);
1081 return self;
1082 }
1083 ParameterRestriction::Any => {}
1084 }
1085 self.request_body.presence_penalty = Some(presence_penalty);
1086 self
1087 }
1088
1089 /// Sets the temperature parameter to control response randomness
1090 ///
1091 /// Higher values (e.g., 1.0) produce more creative and diverse outputs,
1092 /// while lower values (e.g., 0.2) produce more deterministic and consistent outputs.
1093 ///
1094 /// **Note:** Reasoning models (GPT-5, o-series) only support temperature=1.0.
1095 /// For these models, other values will be ignored with a warning.
1096 ///
1097 /// # Arguments
1098 ///
1099 /// * `temperature` - Temperature parameter (range: 0.0 to 2.0)
1100 ///
1101 /// # Returns
1102 ///
1103 /// A mutable reference to self for method chaining
1104 pub fn temperature(&mut self, temperature: f32) -> &mut Self {
1105 let support = self.request_body.model.parameter_support();
1106 match support.temperature {
1107 ParameterRestriction::FixedValue(fixed) => {
1108 if (temperature as f64 - fixed).abs() > f64::EPSILON {
1109 tracing::warn!("Model '{}' only supports temperature={}. Ignoring temperature={}.", self.request_body.model, fixed, temperature);
1110 return self;
1111 }
1112 }
1113 ParameterRestriction::NotSupported => {
1114 tracing::warn!("Model '{}' does not support temperature parameter. Ignoring.", self.request_body.model);
1115 return self;
1116 }
1117 ParameterRestriction::Any => {}
1118 }
1119 self.request_body.temperature = Some(temperature);
1120 self
1121 }
1122
1123 /// Sets structured output using JSON schema
1124 ///
1125 /// Enables receiving responses in a structured JSON format according to the
1126 /// specified JSON schema.
1127 ///
1128 /// # Arguments
1129 ///
1130 /// * `json_schema` - JSON schema defining the response structure
1131 ///
1132 /// # Returns
1133 ///
1134 /// A mutable reference to self for method chaining
1135 pub fn json_schema(&mut self, json_schema: Schema) -> &mut Self {
1136 self.request_body.response_format = Option::from(Format::new(String::from("json_schema"), json_schema));
1137 self
1138 }
1139
1140 /// Sets the tools that can be called by the model
1141 ///
1142 /// Enables function calling by providing a list of tools that the model can choose to call.
1143 /// When tools are provided, the model may generate tool calls instead of or in addition to
1144 /// regular text responses.
1145 ///
1146 /// # Arguments
1147 ///
1148 /// * `tools` - Vector of tools available for the model to use
1149 ///
1150 /// # Returns
1151 ///
1152 /// A mutable reference to self for method chaining
1153 pub fn tools(&mut self, tools: Vec<Tool>) -> &mut Self {
1154 self.request_body.tools = Option::from(tools);
1155 self
1156 }
1157
1158 /// Sets the safety identifier for end-user tracking
1159 ///
1160 /// A stable identifier used to help OpenAI detect users of your application
1161 /// that may be violating usage policies. This enables per-user safety
1162 /// monitoring and abuse detection.
1163 ///
1164 /// # Arguments
1165 ///
1166 /// * `safety_id` - A unique, stable identifier for the end user
1167 /// (recommended: hash of email or internal user ID)
1168 ///
1169 /// # Returns
1170 ///
1171 /// A mutable reference to self for method chaining
1172 ///
1173 /// # Examples
1174 ///
1175 /// ```rust
1176 /// use openai_tools::chat::request::ChatCompletion;
1177 ///
1178 /// let mut chat = ChatCompletion::new();
1179 /// chat.safety_identifier("user_abc123");
1180 /// ```
1181 pub fn safety_identifier<T: AsRef<str>>(&mut self, safety_id: T) -> &mut Self {
1182 self.request_body.safety_identifier = Some(safety_id.as_ref().to_string());
1183 self
1184 }
1185
1186 /// Sets a unique identifier representing your end-user, which can help to monitor and detect abuse
1187 ///
1188 /// A unique identifier representing your end-user, which can help to monitor and detect abuse.
1189 ///
1190 /// # Arguments
1191 ///
1192 /// * `user` - A identifier representing your end-user.
1193 ///
1194 /// # Returns
1195 ///
1196 /// A mutable reference to self for method chaining
1197 ///
1198 /// # Examples
1199 ///
1200 /// ```rust
1201 /// use openai_tools::chat::request::ChatCompletion;
1202 ///
1203 /// let mut chat = ChatCompletion::new();
1204 /// chat.user("abc123");
1205 /// ```
1206 pub fn user<T: AsRef<str>>(&mut self, user: T) -> &mut Self {
1207 self.request_body.user = Some(user.as_ref().to_string());
1208 self
1209 }
1210
1211 /// Gets the current message history
1212 ///
1213 /// # Returns
1214 ///
1215 /// A vector containing the message history
1216 pub fn get_message_history(&self) -> Vec<Message> {
1217 self.request_body.messages.clone()
1218 }
1219
1220 /// Checks if the model is a reasoning model that doesn't support custom temperature
1221 ///
1222 /// Reasoning models (o1, o3, o4 series) only support the default temperature value of 1.0.
1223 /// This method checks if the current model is one of these reasoning models.
1224 ///
1225 /// # Returns
1226 ///
1227 /// `true` if the model is a reasoning model, `false` otherwise
1228 ///
1229 /// # Supported Reasoning Models
1230 ///
1231 /// - `o1`, `o1-pro`, and variants
1232 /// - `o3`, `o3-mini`, and variants
1233 /// - `o4-mini` and variants
1234 fn is_reasoning_model(&self) -> bool {
1235 self.request_body.model.is_reasoning_model()
1236 }
1237
1238 /// Sends the chat completion request to OpenAI API
1239 ///
1240 /// This method validates the request parameters, constructs the HTTP request,
1241 /// and sends it to the OpenAI Chat Completions endpoint.
1242 ///
1243 /// # Returns
1244 ///
1245 /// A `Result` containing the API response on success, or an error on failure.
1246 ///
1247 /// # Errors
1248 ///
1249 /// Returns an error if:
1250 /// - API key is not set
1251 /// - Model ID is not set
1252 /// - Messages are empty
1253 /// - Network request fails
1254 /// - Response parsing fails
1255 ///
1256 /// # Parameter Validation
1257 ///
1258 /// For reasoning models (GPT-5, o-series), certain parameters have restrictions:
1259 /// - `temperature`: only 1.0 supported
1260 /// - `frequency_penalty`: only 0 supported
1261 /// - `presence_penalty`: only 0 supported
1262 /// - `logprobs`, `top_logprobs`, `logit_bias`: not supported
1263 /// - `n`: only 1 supported
1264 ///
1265 /// **Validation occurs at two points:**
1266 /// 1. At setter time (when using `with_model()` constructor) - immediate warning
1267 /// 2. At API call time (fallback) - for cases where model is changed after setting params
1268 ///
1269 /// Unsupported parameter values are ignored with a warning and the request proceeds.
1270 ///
1271 /// # Example
1272 ///
1273 /// ```rust,no_run
1274 /// use openai_tools::chat::request::ChatCompletion;
1275 /// use openai_tools::common::message::Message;
1276 /// use openai_tools::common::role::Role;
1277 ///
1278 /// # #[tokio::main]
1279 /// # async fn main() -> Result<(), Box<dyn std::error::Error>>
1280 /// # {
1281 /// let mut chat = ChatCompletion::new();
1282 /// let messages = vec![Message::from_string(Role::User, "Hello!")];
1283 ///
1284 /// let response = chat
1285 /// .model_id("gpt-4o-mini")
1286 /// .messages(messages)
1287 /// .temperature(1.0)
1288 /// .chat()
1289 /// .await?;
1290 ///
1291 /// println!("{}", response.choices[0].message.content.as_ref().unwrap().text.as_ref().unwrap());
1292 /// # Ok::<(), Box<dyn std::error::Error>>(())
1293 /// # }
1294 /// ```
1295 pub async fn chat(&mut self) -> Result<Response> {
1296 // Validate that messages are set
1297 if self.request_body.messages.is_empty() {
1298 return Err(OpenAIToolError::Error("Messages are not set.".into()));
1299 }
1300
1301 // Handle reasoning models that don't support certain parameters
1302 // See: https://platform.openai.com/docs/guides/reasoning
1303 if self.is_reasoning_model() {
1304 let model = &self.request_body.model;
1305
1306 // Temperature: only default (1.0) is supported
1307 if let Some(temp) = self.request_body.temperature {
1308 if (temp - 1.0).abs() > f32::EPSILON {
1309 tracing::warn!(
1310 "Reasoning model '{}' does not support custom temperature. \
1311 Ignoring temperature={} and using default (1.0).",
1312 model,
1313 temp
1314 );
1315 self.request_body.temperature = None;
1316 }
1317 }
1318
1319 // Frequency penalty: only 0 is supported
1320 if let Some(fp) = self.request_body.frequency_penalty {
1321 if fp.abs() > f32::EPSILON {
1322 tracing::warn!(
1323 "Reasoning model '{}' does not support frequency_penalty. \
1324 Ignoring frequency_penalty={} and using default (0).",
1325 model,
1326 fp
1327 );
1328 self.request_body.frequency_penalty = None;
1329 }
1330 }
1331
1332 // Presence penalty: only 0 is supported
1333 if let Some(pp) = self.request_body.presence_penalty {
1334 if pp.abs() > f32::EPSILON {
1335 tracing::warn!(
1336 "Reasoning model '{}' does not support presence_penalty. \
1337 Ignoring presence_penalty={} and using default (0).",
1338 model,
1339 pp
1340 );
1341 self.request_body.presence_penalty = None;
1342 }
1343 }
1344
1345 // Logprobs: not supported
1346 if self.request_body.logprobs.is_some() {
1347 tracing::warn!("Reasoning model '{}' does not support logprobs. Ignoring logprobs parameter.", model);
1348 self.request_body.logprobs = None;
1349 }
1350
1351 // Top logprobs: not supported
1352 if self.request_body.top_logprobs.is_some() {
1353 tracing::warn!("Reasoning model '{}' does not support top_logprobs. Ignoring top_logprobs parameter.", model);
1354 self.request_body.top_logprobs = None;
1355 }
1356
1357 // Logit bias: not supported
1358 if self.request_body.logit_bias.is_some() {
1359 tracing::warn!("Reasoning model '{}' does not support logit_bias. Ignoring logit_bias parameter.", model);
1360 self.request_body.logit_bias = None;
1361 }
1362
1363 // N: only 1 is supported
1364 if let Some(n) = self.request_body.n {
1365 if n != 1 {
1366 tracing::warn!(
1367 "Reasoning model '{}' does not support n != 1. \
1368 Ignoring n={} and using default (1).",
1369 model,
1370 n
1371 );
1372 self.request_body.n = None;
1373 }
1374 }
1375 }
1376
1377 let body = serde_json::to_string(&self.request_body)?;
1378
1379 let client = create_http_client(self.timeout)?;
1380 let mut headers = request::header::HeaderMap::new();
1381 headers.insert("Content-Type", request::header::HeaderValue::from_static("application/json"));
1382 headers.insert("User-Agent", request::header::HeaderValue::from_static("openai-tools-rust"));
1383
1384 // Apply provider-specific authentication headers
1385 self.auth.apply_headers(&mut headers)?;
1386
1387 if cfg!(debug_assertions) {
1388 // Replace API key with a placeholder in debug mode
1389 let body_for_debug = serde_json::to_string_pretty(&self.request_body).unwrap().replace(self.auth.api_key(), "*************");
1390 tracing::info!("Request body: {}", body_for_debug);
1391 }
1392
1393 // Get the endpoint URL from the auth provider
1394 let endpoint = self.auth.endpoint(CHAT_COMPLETIONS_PATH);
1395
1396 let response = client.post(&endpoint).headers(headers).body(body).send().await.map_err(OpenAIToolError::RequestError)?;
1397 let status = response.status();
1398 let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
1399
1400 if cfg!(debug_assertions) {
1401 tracing::info!("Response content: {}", content);
1402 }
1403
1404 if !status.is_success() {
1405 if let Ok(error_resp) = serde_json::from_str::<ErrorResponse>(&content) {
1406 return Err(OpenAIToolError::Error(error_resp.error.message.unwrap_or_default()));
1407 }
1408 return Err(OpenAIToolError::Error(format!("API error ({}): {}", status, content)));
1409 }
1410
1411 serde_json::from_str::<Response>(&content).map_err(OpenAIToolError::SerdeJsonError)
1412 }
1413
1414 /// Creates a test-only ChatCompletion instance without authentication
1415 ///
1416 /// This is only available in test mode and bypasses API key requirements.
1417 #[cfg(test)]
1418 pub(crate) fn test_new_with_model(model: ChatModel) -> Self {
1419 use crate::common::auth::OpenAIAuth;
1420 Self { auth: AuthProvider::OpenAI(OpenAIAuth::new("test-key")), request_body: Body { model, ..Default::default() }, timeout: None }
1421 }
1422}
1423
1424#[cfg(test)]
1425mod tests {
1426 use super::*;
1427 use crate::common::models::ChatModel;
1428 use std::collections::HashMap;
1429
1430 // =============================================================================
1431 // Standard Model Parameter Tests
1432 // =============================================================================
1433
1434 #[test]
1435 fn test_standard_model_accepts_all_parameters() {
1436 let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt4oMini);
1437
1438 // Standard models should accept all parameters
1439 chat.temperature(0.7);
1440 chat.frequency_penalty(0.5);
1441 chat.presence_penalty(0.5);
1442 chat.logprobs(true);
1443 chat.top_logprobs(5);
1444 chat.n(3);
1445
1446 let logit_bias: HashMap<&str, i32> = [("1234", 10)].iter().cloned().collect();
1447 chat.logit_bias(logit_bias);
1448
1449 assert_eq!(chat.request_body.temperature, Some(0.7));
1450 assert_eq!(chat.request_body.frequency_penalty, Some(0.5));
1451 assert_eq!(chat.request_body.presence_penalty, Some(0.5));
1452 assert_eq!(chat.request_body.logprobs, Some(true));
1453 assert_eq!(chat.request_body.top_logprobs, Some(5));
1454 assert_eq!(chat.request_body.n, Some(3));
1455 assert!(chat.request_body.logit_bias.is_some());
1456 }
1457
1458 #[test]
1459 fn test_gpt4o_accepts_all_parameters() {
1460 let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt4o);
1461
1462 chat.temperature(0.3);
1463 chat.frequency_penalty(-1.0);
1464 chat.presence_penalty(1.5);
1465
1466 assert_eq!(chat.request_body.temperature, Some(0.3));
1467 assert_eq!(chat.request_body.frequency_penalty, Some(-1.0));
1468 assert_eq!(chat.request_body.presence_penalty, Some(1.5));
1469 }
1470
1471 #[test]
1472 fn test_gpt4_1_accepts_all_parameters() {
1473 let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt4_1);
1474
1475 chat.temperature(1.5);
1476 chat.frequency_penalty(0.8);
1477 chat.n(2);
1478
1479 assert_eq!(chat.request_body.temperature, Some(1.5));
1480 assert_eq!(chat.request_body.frequency_penalty, Some(0.8));
1481 assert_eq!(chat.request_body.n, Some(2));
1482 }
1483
1484 // =============================================================================
1485 // O-Series Reasoning Model Tests
1486 // =============================================================================
1487
1488 #[test]
1489 fn test_o1_ignores_non_default_temperature() {
1490 let mut chat = ChatCompletion::test_new_with_model(ChatModel::O1);
1491
1492 // Non-default temperature should be ignored
1493 chat.temperature(0.5);
1494 assert_eq!(chat.request_body.temperature, None);
1495
1496 // Default temperature (1.0) should be accepted
1497 chat.temperature(1.0);
1498 assert_eq!(chat.request_body.temperature, Some(1.0));
1499 }
1500
1501 #[test]
1502 fn test_o3_mini_ignores_non_default_temperature() {
1503 let mut chat = ChatCompletion::test_new_with_model(ChatModel::O3Mini);
1504
1505 chat.temperature(0.3);
1506 assert_eq!(chat.request_body.temperature, None);
1507 }
1508
1509 #[test]
1510 fn test_o4_mini_ignores_non_default_temperature() {
1511 let mut chat = ChatCompletion::test_new_with_model(ChatModel::O4Mini);
1512
1513 chat.temperature(0.7);
1514 assert_eq!(chat.request_body.temperature, None);
1515 }
1516
1517 #[test]
1518 fn test_o1_ignores_frequency_penalty() {
1519 let mut chat = ChatCompletion::test_new_with_model(ChatModel::O1);
1520
1521 // Non-zero frequency_penalty should be ignored
1522 chat.frequency_penalty(0.5);
1523 assert_eq!(chat.request_body.frequency_penalty, None);
1524
1525 // Zero value should be accepted
1526 chat.frequency_penalty(0.0);
1527 assert_eq!(chat.request_body.frequency_penalty, Some(0.0));
1528 }
1529
1530 #[test]
1531 fn test_o3_ignores_presence_penalty() {
1532 let mut chat = ChatCompletion::test_new_with_model(ChatModel::O3);
1533
1534 // Non-zero presence_penalty should be ignored
1535 chat.presence_penalty(0.5);
1536 assert_eq!(chat.request_body.presence_penalty, None);
1537
1538 // Zero value should be accepted
1539 chat.presence_penalty(0.0);
1540 assert_eq!(chat.request_body.presence_penalty, Some(0.0));
1541 }
1542
1543 #[test]
1544 fn test_o1_ignores_logprobs() {
1545 let mut chat = ChatCompletion::test_new_with_model(ChatModel::O1);
1546
1547 chat.logprobs(true);
1548 assert_eq!(chat.request_body.logprobs, None);
1549 }
1550
1551 #[test]
1552 fn test_o3_mini_ignores_top_logprobs() {
1553 let mut chat = ChatCompletion::test_new_with_model(ChatModel::O3Mini);
1554
1555 chat.top_logprobs(5);
1556 assert_eq!(chat.request_body.top_logprobs, None);
1557 }
1558
1559 #[test]
1560 fn test_o1_ignores_logit_bias() {
1561 let mut chat = ChatCompletion::test_new_with_model(ChatModel::O1);
1562
1563 let logit_bias: HashMap<&str, i32> = [("1234", 10)].iter().cloned().collect();
1564 chat.logit_bias(logit_bias);
1565 assert_eq!(chat.request_body.logit_bias, None);
1566 }
1567
1568 #[test]
1569 fn test_o1_ignores_n_greater_than_1() {
1570 let mut chat = ChatCompletion::test_new_with_model(ChatModel::O1);
1571
1572 // n > 1 should be ignored
1573 chat.n(3);
1574 assert_eq!(chat.request_body.n, None);
1575
1576 // n = 1 should be accepted
1577 chat.n(1);
1578 assert_eq!(chat.request_body.n, Some(1));
1579 }
1580
1581 // =============================================================================
1582 // GPT-5 Series Reasoning Model Tests
1583 // =============================================================================
1584
1585 #[test]
1586 fn test_gpt5_2_ignores_non_default_temperature() {
1587 let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt5_2);
1588
1589 chat.temperature(0.5);
1590 assert_eq!(chat.request_body.temperature, None);
1591
1592 chat.temperature(1.0);
1593 assert_eq!(chat.request_body.temperature, Some(1.0));
1594 }
1595
1596 #[test]
1597 fn test_gpt5_1_ignores_non_default_temperature() {
1598 let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt5_1);
1599
1600 chat.temperature(0.3);
1601 assert_eq!(chat.request_body.temperature, None);
1602 }
1603
1604 #[test]
1605 fn test_gpt5_mini_ignores_frequency_penalty() {
1606 let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt5Mini);
1607
1608 chat.frequency_penalty(0.5);
1609 assert_eq!(chat.request_body.frequency_penalty, None);
1610 }
1611
1612 #[test]
1613 fn test_gpt5_2_pro_ignores_presence_penalty() {
1614 let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt5_2Pro);
1615
1616 chat.presence_penalty(0.8);
1617 assert_eq!(chat.request_body.presence_penalty, None);
1618 }
1619
1620 #[test]
1621 fn test_gpt5_1_codex_max_ignores_logprobs() {
1622 let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt5_1CodexMax);
1623
1624 chat.logprobs(true);
1625 assert_eq!(chat.request_body.logprobs, None);
1626 }
1627
1628 #[test]
1629 /// `gpt-5.2-chat-latest` points at the non-reasoning Instant snapshot, so
1630 /// it accepts the standard parameters that the reasoning models reject.
1631 fn test_gpt5_2_chat_latest_honors_n_greater_than_1() {
1632 let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt5_2ChatLatest);
1633
1634 chat.n(5);
1635 assert_eq!(chat.request_body.n, Some(5));
1636
1637 chat.temperature(0.3);
1638 assert_eq!(chat.request_body.temperature, Some(0.3));
1639 }
1640
1641 // =============================================================================
1642 // Multiple Restricted Parameters Tests
1643 // =============================================================================
1644
1645 #[test]
1646 fn test_o1_ignores_all_restricted_parameters_at_once() {
1647 let mut chat = ChatCompletion::test_new_with_model(ChatModel::O1);
1648
1649 // Set all restricted parameters
1650 chat.temperature(0.5);
1651 chat.frequency_penalty(0.5);
1652 chat.presence_penalty(0.5);
1653 chat.logprobs(true);
1654 chat.top_logprobs(5);
1655 chat.n(3);
1656
1657 let logit_bias: HashMap<&str, i32> = [("1234", 10)].iter().cloned().collect();
1658 chat.logit_bias(logit_bias);
1659
1660 // All should be ignored
1661 assert_eq!(chat.request_body.temperature, None);
1662 assert_eq!(chat.request_body.frequency_penalty, None);
1663 assert_eq!(chat.request_body.presence_penalty, None);
1664 assert_eq!(chat.request_body.logprobs, None);
1665 assert_eq!(chat.request_body.top_logprobs, None);
1666 assert_eq!(chat.request_body.n, None);
1667 assert_eq!(chat.request_body.logit_bias, None);
1668 }
1669
1670 #[test]
1671 fn test_gpt5_2_ignores_all_restricted_parameters_at_once() {
1672 let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt5_2);
1673
1674 chat.temperature(0.5);
1675 chat.frequency_penalty(0.5);
1676 chat.presence_penalty(0.5);
1677 chat.logprobs(true);
1678 chat.top_logprobs(5);
1679 chat.n(3);
1680
1681 let logit_bias: HashMap<&str, i32> = [("1234", 10)].iter().cloned().collect();
1682 chat.logit_bias(logit_bias);
1683
1684 assert_eq!(chat.request_body.temperature, None);
1685 assert_eq!(chat.request_body.frequency_penalty, None);
1686 assert_eq!(chat.request_body.presence_penalty, None);
1687 assert_eq!(chat.request_body.logprobs, None);
1688 assert_eq!(chat.request_body.top_logprobs, None);
1689 assert_eq!(chat.request_body.n, None);
1690 assert_eq!(chat.request_body.logit_bias, None);
1691 }
1692
1693 // =============================================================================
1694 // Custom Model Tests
1695 // =============================================================================
1696
1697 #[test]
1698 fn test_custom_gpt5_model_detected_as_reasoning() {
1699 let mut chat = ChatCompletion::test_new_with_model(ChatModel::custom("gpt-5.3-preview"));
1700
1701 // Custom GPT-5 models should be treated as reasoning models
1702 chat.temperature(0.5);
1703 assert_eq!(chat.request_body.temperature, None);
1704 }
1705
1706 #[test]
1707 fn test_custom_o1_model_detected_as_reasoning() {
1708 let mut chat = ChatCompletion::test_new_with_model(ChatModel::custom("o1-pro-2025-01-15"));
1709
1710 // Custom o1-series models should be treated as reasoning models
1711 chat.temperature(0.5);
1712 assert_eq!(chat.request_body.temperature, None);
1713 }
1714
1715 #[test]
1716 fn test_custom_o3_model_detected_as_reasoning() {
1717 let mut chat = ChatCompletion::test_new_with_model(ChatModel::custom("o3-high"));
1718
1719 // Custom o3-series models should be treated as reasoning models
1720 chat.temperature(0.5);
1721 assert_eq!(chat.request_body.temperature, None);
1722 }
1723
1724 #[test]
1725 fn test_custom_o4_model_detected_as_reasoning() {
1726 let mut chat = ChatCompletion::test_new_with_model(ChatModel::custom("o4-mini-preview"));
1727
1728 // Custom o4-series models should be treated as reasoning models
1729 chat.temperature(0.5);
1730 assert_eq!(chat.request_body.temperature, None);
1731 }
1732
1733 #[test]
1734 fn test_custom_standard_model_accepts_all_parameters() {
1735 let mut chat = ChatCompletion::test_new_with_model(ChatModel::custom("ft:gpt-4o-mini:org::123"));
1736
1737 // Fine-tuned standard models should accept all parameters
1738 chat.temperature(0.7);
1739 chat.frequency_penalty(0.5);
1740 chat.n(2);
1741
1742 assert_eq!(chat.request_body.temperature, Some(0.7));
1743 assert_eq!(chat.request_body.frequency_penalty, Some(0.5));
1744 assert_eq!(chat.request_body.n, Some(2));
1745 }
1746
1747 // =============================================================================
1748 // Parameter Boundary Tests
1749 // =============================================================================
1750
1751 #[test]
1752 fn test_temperature_boundary_values() {
1753 let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt4oMini);
1754
1755 // Minimum value
1756 chat.temperature(0.0);
1757 assert_eq!(chat.request_body.temperature, Some(0.0));
1758
1759 // Maximum value
1760 chat.temperature(2.0);
1761 assert_eq!(chat.request_body.temperature, Some(2.0));
1762 }
1763
1764 #[test]
1765 fn test_frequency_penalty_boundary_values() {
1766 let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt4oMini);
1767
1768 // Minimum value
1769 chat.frequency_penalty(-2.0);
1770 assert_eq!(chat.request_body.frequency_penalty, Some(-2.0));
1771
1772 // Maximum value
1773 chat.frequency_penalty(2.0);
1774 assert_eq!(chat.request_body.frequency_penalty, Some(2.0));
1775 }
1776
1777 #[test]
1778 fn test_presence_penalty_boundary_values() {
1779 let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt4oMini);
1780
1781 // Minimum value
1782 chat.presence_penalty(-2.0);
1783 assert_eq!(chat.request_body.presence_penalty, Some(-2.0));
1784
1785 // Maximum value
1786 chat.presence_penalty(2.0);
1787 assert_eq!(chat.request_body.presence_penalty, Some(2.0));
1788 }
1789
1790 // =============================================================================
1791 // Model-Specific Unrestricted Parameters Tests
1792 // =============================================================================
1793
1794 #[test]
1795 fn test_max_completion_tokens_accepted_by_all_models() {
1796 // Standard model
1797 let mut chat_standard = ChatCompletion::test_new_with_model(ChatModel::Gpt4oMini);
1798 chat_standard.max_completion_tokens(1000);
1799 assert_eq!(chat_standard.request_body.max_completion_tokens, Some(1000));
1800
1801 // Reasoning model
1802 let mut chat_reasoning = ChatCompletion::test_new_with_model(ChatModel::O1);
1803 chat_reasoning.max_completion_tokens(2000);
1804 assert_eq!(chat_reasoning.request_body.max_completion_tokens, Some(2000));
1805
1806 // GPT-5 model
1807 let mut chat_gpt5 = ChatCompletion::test_new_with_model(ChatModel::Gpt5_2);
1808 chat_gpt5.max_completion_tokens(3000);
1809 assert_eq!(chat_gpt5.request_body.max_completion_tokens, Some(3000));
1810 }
1811
1812 #[test]
1813 fn test_store_accepted_by_all_models() {
1814 let mut chat_standard = ChatCompletion::test_new_with_model(ChatModel::Gpt4oMini);
1815 chat_standard.store(true);
1816 assert_eq!(chat_standard.request_body.store, Some(true));
1817
1818 let mut chat_reasoning = ChatCompletion::test_new_with_model(ChatModel::O1);
1819 chat_reasoning.store(false);
1820 assert_eq!(chat_reasoning.request_body.store, Some(false));
1821 }
1822
1823 // =============================================================================
1824 // Chat API Content Serialization Tests
1825 // =============================================================================
1826
1827 #[test]
1828 fn test_chat_text_content_serialization() {
1829 use crate::common::message::Content;
1830
1831 let content = Content::from_text("Hello, world!");
1832 let wrapper = ChatContentRef(&content);
1833 let json = serde_json::to_value(&wrapper).unwrap();
1834
1835 assert_eq!(json["type"], "text");
1836 assert_eq!(json["text"], "Hello, world!");
1837 assert!(json.get("image_url").is_none());
1838 }
1839
1840 #[test]
1841 fn test_chat_image_content_serialization() {
1842 use crate::common::message::Content;
1843
1844 let content = Content::from_image_url("https://example.com/image.png");
1845 let wrapper = ChatContentRef(&content);
1846 let json = serde_json::to_value(&wrapper).unwrap();
1847
1848 assert_eq!(json["type"], "image_url");
1849 assert_eq!(json["image_url"]["url"], "https://example.com/image.png");
1850 }
1851
1852 #[test]
1853 fn test_chat_multimodal_message_serialization() {
1854 use crate::common::message::{Content, Message};
1855 use crate::common::role::Role;
1856
1857 let contents = vec![Content::from_text("What's in this image?"), Content::from_image_url("https://example.com/image.png")];
1858 let message = Message::from_message_array(Role::User, contents);
1859 let wrapper = ChatMessageRef(&message);
1860 let json = serde_json::to_value(&wrapper).unwrap();
1861
1862 assert_eq!(json["role"], "user");
1863 let content_arr = json["content"].as_array().unwrap();
1864 assert_eq!(content_arr.len(), 2);
1865
1866 // First element: text
1867 assert_eq!(content_arr[0]["type"], "text");
1868 assert_eq!(content_arr[0]["text"], "What's in this image?");
1869
1870 // Second element: image_url with nested object
1871 assert_eq!(content_arr[1]["type"], "image_url");
1872 assert_eq!(content_arr[1]["image_url"]["url"], "https://example.com/image.png");
1873 }
1874
1875 #[test]
1876 fn test_chat_single_text_message_serialization() {
1877 use crate::common::message::Message;
1878 use crate::common::role::Role;
1879
1880 let message = Message::from_string(Role::User, "Hello!");
1881 let wrapper = ChatMessageRef(&message);
1882 let json = serde_json::to_value(&wrapper).unwrap();
1883
1884 assert_eq!(json["role"], "user");
1885 // Single text content should be serialized as a plain string, not an array
1886 assert_eq!(json["content"], "Hello!");
1887 }
1888
1889 #[test]
1890 fn test_chat_body_messages_serialization() {
1891 use crate::common::message::{Content, Message};
1892 use crate::common::role::Role;
1893
1894 let messages = vec![
1895 Message::from_string(Role::System, "You are a helpful assistant."),
1896 Message::from_message_array(
1897 Role::User,
1898 vec![Content::from_text("Describe this image"), Content::from_image_url("https://example.com/photo.jpg")],
1899 ),
1900 ];
1901
1902 let body = Body { model: ChatModel::Gpt4oMini, messages, ..Default::default() };
1903
1904 let json = serde_json::to_value(&body).unwrap();
1905 let msgs = json["messages"].as_array().unwrap();
1906
1907 // System message: plain string content
1908 assert_eq!(msgs[0]["role"], "system");
1909 assert_eq!(msgs[0]["content"], "You are a helpful assistant.");
1910
1911 // User multimodal message: array content with Chat API types
1912 assert_eq!(msgs[1]["role"], "user");
1913 let content_arr = msgs[1]["content"].as_array().unwrap();
1914 assert_eq!(content_arr[0]["type"], "text");
1915 assert_eq!(content_arr[1]["type"], "image_url");
1916 assert_eq!(content_arr[1]["image_url"]["url"], "https://example.com/photo.jpg");
1917 }
1918
1919 #[test]
1920 fn test_safety_identifier() {
1921 let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt4oMini);
1922 chat.safety_identifier("user_abc123");
1923 assert_eq!(chat.request_body.safety_identifier, Some("user_abc123".to_string()));
1924
1925 // Verify serialization
1926 let json = serde_json::to_value(&chat.request_body).unwrap();
1927 assert_eq!(json["safety_identifier"], "user_abc123");
1928 }
1929
1930 #[test]
1931 fn test_user() {
1932 let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt4oMini);
1933 chat.user("abc123");
1934 assert_eq!(chat.request_body.user, Some("abc123".to_string()));
1935
1936 // Verify serialization
1937 let json = serde_json::to_value(&chat.request_body).unwrap();
1938 assert_eq!(json["user"], "abc123");
1939 }
1940
1941 #[test]
1942 fn test_safety_identifier_not_serialized_when_none() {
1943 let chat = ChatCompletion::test_new_with_model(ChatModel::Gpt4oMini);
1944 let json = serde_json::to_value(&chat.request_body).unwrap();
1945 assert!(json.get("safety_identifier").is_none());
1946 }
1947}