Skip to main content

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}
393
394/// OpenAI Chat Completions API client
395///
396/// This structure manages interactions with the OpenAI Chat Completions API.
397/// It handles API key management, request parameter configuration, and API calls.
398///
399/// # Example
400///
401/// ```rust
402/// use openai_tools::chat::request::ChatCompletion;
403/// use openai_tools::common::message::Message;
404/// use openai_tools::common::role::Role;
405///
406/// # #[tokio::main]
407/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
408/// let mut chat = ChatCompletion::new();
409/// let messages = vec![Message::from_string(Role::User, "Hello!")];
410///
411/// let response = chat
412///     .model_id("gpt-4o-mini")
413///     .messages(messages)
414///     .temperature(1.0)
415///     .chat()
416///     .await?;
417/// # Ok::<(), Box<dyn std::error::Error>>(())
418/// # }
419/// ```
420/// Default API path for Chat Completions
421const CHAT_COMPLETIONS_PATH: &str = "chat/completions";
422
423/// OpenAI Chat Completions API client
424///
425/// This structure manages interactions with the OpenAI Chat Completions API
426/// and Azure OpenAI API. It handles authentication, request parameter
427/// configuration, and API calls.
428///
429/// # Providers
430///
431/// The client supports two providers:
432/// - **OpenAI**: Standard OpenAI API (default)
433/// - **Azure**: Azure OpenAI Service
434///
435/// # Examples
436///
437/// ## OpenAI (existing behavior - unchanged)
438///
439/// ```rust,no_run
440/// use openai_tools::chat::request::ChatCompletion;
441/// use openai_tools::common::message::Message;
442/// use openai_tools::common::role::Role;
443///
444/// # #[tokio::main]
445/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
446/// let mut chat = ChatCompletion::new();
447/// let messages = vec![Message::from_string(Role::User, "Hello!")];
448///
449/// let response = chat
450///     .model_id("gpt-4o-mini")
451///     .messages(messages)
452///     .chat()
453///     .await?;
454/// # Ok(())
455/// # }
456/// ```
457///
458/// ## Azure OpenAI
459///
460/// ```rust,no_run
461/// use openai_tools::chat::request::ChatCompletion;
462/// use openai_tools::common::message::Message;
463/// use openai_tools::common::role::Role;
464///
465/// # #[tokio::main]
466/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
467/// // From environment variables
468/// let mut chat = ChatCompletion::azure()?;
469///
470/// let messages = vec![Message::from_string(Role::User, "Hello!")];
471/// let response = chat.messages(messages).chat().await?;
472/// # Ok(())
473/// # }
474/// ```
475#[derive(Debug, Clone)]
476pub struct ChatCompletion {
477    /// Authentication provider (OpenAI or Azure)
478    auth: AuthProvider,
479    /// The request body containing all parameters for the API call
480    pub(crate) request_body: Body,
481    /// Optional request timeout duration
482    timeout: Option<Duration>,
483}
484
485impl Default for ChatCompletion {
486    fn default() -> Self {
487        Self::new()
488    }
489}
490
491impl ChatCompletion {
492    /// Creates a new ChatCompletion instance for OpenAI API
493    ///
494    /// Loads the API key from the `OPENAI_API_KEY` environment variable.
495    /// If a `.env` file exists, it will also be loaded.
496    ///
497    /// # Panics
498    ///
499    /// Panics if the `OPENAI_API_KEY` environment variable is not set.
500    ///
501    /// # Returns
502    ///
503    /// A new ChatCompletion instance configured for OpenAI API
504    ///
505    /// # Example
506    ///
507    /// ```rust,no_run
508    /// use openai_tools::chat::request::ChatCompletion;
509    ///
510    /// let mut chat = ChatCompletion::new();
511    /// ```
512    pub fn new() -> Self {
513        let auth = AuthProvider::openai_from_env().map_err(|e| OpenAIToolError::Error(format!("Failed to load OpenAI auth: {}", e))).unwrap();
514        Self { auth, request_body: Body::default(), timeout: None }
515    }
516
517    /// Creates a new ChatCompletion instance with a specified model
518    ///
519    /// This is the recommended constructor as it enables parameter validation
520    /// at setter time. When you set parameters like `temperature()`, the model's
521    /// parameter support is checked and warnings are logged for unsupported values.
522    ///
523    /// # Arguments
524    ///
525    /// * `model` - The model to use for chat completion
526    ///
527    /// # Panics
528    ///
529    /// Panics if the `OPENAI_API_KEY` environment variable is not set.
530    ///
531    /// # Returns
532    ///
533    /// A new ChatCompletion instance with the specified model
534    ///
535    /// # Example
536    ///
537    /// ```rust,no_run
538    /// use openai_tools::chat::request::ChatCompletion;
539    /// use openai_tools::common::models::ChatModel;
540    ///
541    /// // Recommended: specify model at creation time
542    /// let mut chat = ChatCompletion::with_model(ChatModel::Gpt4oMini);
543    ///
544    /// // For reasoning models, unsupported parameters are validated at setter time
545    /// let mut reasoning_chat = ChatCompletion::with_model(ChatModel::O3Mini);
546    /// reasoning_chat.temperature(0.5); // Warning logged, value ignored
547    /// ```
548    pub fn with_model(model: ChatModel) -> Self {
549        let auth = AuthProvider::openai_from_env().map_err(|e| OpenAIToolError::Error(format!("Failed to load OpenAI auth: {}", e))).unwrap();
550        Self { auth, request_body: Body { model, ..Default::default() }, timeout: None }
551    }
552
553    /// Creates a new ChatCompletion instance with a custom authentication provider
554    ///
555    /// Use this to explicitly configure OpenAI or Azure authentication.
556    ///
557    /// # Arguments
558    ///
559    /// * `auth` - The authentication provider
560    ///
561    /// # Returns
562    ///
563    /// A new ChatCompletion instance with the specified auth provider
564    ///
565    /// # Example
566    ///
567    /// ```rust
568    /// use openai_tools::chat::request::ChatCompletion;
569    /// use openai_tools::common::auth::{AuthProvider, AzureAuth};
570    ///
571    /// // Explicit Azure configuration with complete base URL
572    /// let auth = AuthProvider::Azure(
573    ///     AzureAuth::new(
574    ///         "api-key",
575    ///         "https://my-resource.openai.azure.com/openai/deployments/gpt-4o?api-version=2024-08-01-preview"
576    ///     )
577    /// );
578    /// let mut chat = ChatCompletion::with_auth(auth);
579    /// ```
580    pub fn with_auth(auth: AuthProvider) -> Self {
581        Self { auth, request_body: Body::default(), timeout: None }
582    }
583
584    /// Creates a new ChatCompletion instance for Azure OpenAI API
585    ///
586    /// Loads configuration from Azure-specific environment variables.
587    ///
588    /// # Returns
589    ///
590    /// `Result<ChatCompletion>` - Configured for Azure or error if env vars missing
591    ///
592    /// # Environment Variables
593    ///
594    /// | Variable | Required | Description |
595    /// |----------|----------|-------------|
596    /// | `AZURE_OPENAI_API_KEY` | Yes | Azure API key |
597    /// | `AZURE_OPENAI_BASE_URL` | Yes | Complete endpoint URL including deployment, API path, and api-version |
598    ///
599    /// # Example
600    ///
601    /// ```rust,no_run
602    /// use openai_tools::chat::request::ChatCompletion;
603    ///
604    /// // With environment variables:
605    /// // AZURE_OPENAI_API_KEY=xxx
606    /// // AZURE_OPENAI_BASE_URL=https://my-resource.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-08-01-preview
607    /// let mut chat = ChatCompletion::azure()?;
608    /// # Ok::<(), openai_tools::common::errors::OpenAIToolError>(())
609    /// ```
610    pub fn azure() -> Result<Self> {
611        let auth = AuthProvider::azure_from_env()?;
612        Ok(Self { auth, request_body: Body::default(), timeout: None })
613    }
614
615    /// Creates a new ChatCompletion instance by auto-detecting the provider
616    ///
617    /// Tries Azure first (if AZURE_OPENAI_API_KEY is set), then falls back to OpenAI.
618    ///
619    /// # Returns
620    ///
621    /// `Result<ChatCompletion>` - Auto-configured client or error
622    ///
623    /// # Example
624    ///
625    /// ```rust,no_run
626    /// use openai_tools::chat::request::ChatCompletion;
627    ///
628    /// // Uses Azure if AZURE_OPENAI_API_KEY is set, otherwise OpenAI
629    /// let mut chat = ChatCompletion::detect_provider()?;
630    /// # Ok::<(), openai_tools::common::errors::OpenAIToolError>(())
631    /// ```
632    pub fn detect_provider() -> Result<Self> {
633        let auth = AuthProvider::from_env()?;
634        Ok(Self { auth, request_body: Body::default(), timeout: None })
635    }
636
637    /// Creates a new ChatCompletion instance with URL-based provider detection
638    ///
639    /// Analyzes the URL pattern to determine the provider:
640    /// - URLs containing `.openai.azure.com` → Azure
641    /// - All other URLs → OpenAI-compatible
642    ///
643    /// # Arguments
644    ///
645    /// * `base_url` - The complete base URL for API requests
646    /// * `api_key` - The API key or token
647    ///
648    /// # Returns
649    ///
650    /// `ChatCompletion` - Configured client
651    ///
652    /// # Example
653    ///
654    /// ```rust
655    /// use openai_tools::chat::request::ChatCompletion;
656    ///
657    /// // OpenAI-compatible API (e.g., local Ollama)
658    /// let chat = ChatCompletion::with_url(
659    ///     "http://localhost:11434/v1",
660    ///     "ollama",
661    /// );
662    ///
663    /// // Azure OpenAI (complete base URL)
664    /// let azure_chat = ChatCompletion::with_url(
665    ///     "https://my-resource.openai.azure.com/openai/deployments/gpt-4o?api-version=2024-08-01-preview",
666    ///     "azure-key",
667    /// );
668    /// ```
669    pub fn with_url<S: Into<String>>(base_url: S, api_key: S) -> Self {
670        let auth = AuthProvider::from_url_with_key(base_url, api_key);
671        Self { auth, request_body: Body::default(), timeout: None }
672    }
673
674    /// Creates a new ChatCompletion instance from URL using environment variables
675    ///
676    /// Analyzes the URL pattern to determine the provider, then loads
677    /// credentials from the appropriate environment variables.
678    ///
679    /// # Arguments
680    ///
681    /// * `base_url` - The complete base URL for API requests
682    ///
683    /// # Environment Variables
684    ///
685    /// For Azure URLs (`*.openai.azure.com`):
686    /// - `AZURE_OPENAI_API_KEY` (required)
687    ///
688    /// For other URLs:
689    /// - `OPENAI_API_KEY` (required)
690    ///
691    /// # Returns
692    ///
693    /// `Result<ChatCompletion>` - Configured client or error
694    ///
695    /// # Example
696    ///
697    /// ```rust,no_run
698    /// use openai_tools::chat::request::ChatCompletion;
699    ///
700    /// // Uses OPENAI_API_KEY from environment
701    /// let chat = ChatCompletion::from_url("https://api.openai.com/v1")?;
702    ///
703    /// // Uses AZURE_OPENAI_API_KEY from environment (complete base URL)
704    /// let azure = ChatCompletion::from_url(
705    ///     "https://my-resource.openai.azure.com/openai/deployments/gpt-4o?api-version=2024-08-01-preview"
706    /// )?;
707    /// # Ok::<(), openai_tools::common::errors::OpenAIToolError>(())
708    /// ```
709    pub fn from_url<S: Into<String>>(base_url: S) -> Result<Self> {
710        let auth = AuthProvider::from_url(base_url)?;
711        Ok(Self { auth, request_body: Body::default(), timeout: None })
712    }
713
714    /// Returns the authentication provider
715    ///
716    /// # Returns
717    ///
718    /// Reference to the authentication provider
719    pub fn auth(&self) -> &AuthProvider {
720        &self.auth
721    }
722
723    /// Sets a custom API endpoint URL (OpenAI only)
724    ///
725    /// Use this to point to alternative OpenAI-compatible APIs (e.g., proxy servers).
726    /// For Azure, use `azure()` or `with_auth()` instead.
727    ///
728    /// # Arguments
729    ///
730    /// * `url` - The base URL (e.g., "https://my-proxy.example.com/v1")
731    ///
732    /// # Returns
733    ///
734    /// A mutable reference to self for method chaining
735    ///
736    /// # Note
737    ///
738    /// This method only works with OpenAI authentication. For Azure, the endpoint
739    /// is constructed from resource name and deployment name.
740    ///
741    /// # Example
742    ///
743    /// ```rust,no_run
744    /// use openai_tools::chat::request::ChatCompletion;
745    ///
746    /// let mut chat = ChatCompletion::new();
747    /// chat.base_url("https://my-proxy.example.com/v1");
748    /// ```
749    pub fn base_url<T: AsRef<str>>(&mut self, url: T) -> &mut Self {
750        // Only modify if OpenAI provider
751        if let AuthProvider::OpenAI(ref openai_auth) = self.auth {
752            let new_auth = OpenAIAuth::new(openai_auth.api_key()).with_base_url(url.as_ref());
753            self.auth = AuthProvider::OpenAI(new_auth);
754        } else {
755            tracing::warn!("base_url() is only supported for OpenAI provider. Use azure() or with_auth() for Azure.");
756        }
757        self
758    }
759
760    /// Sets the model to use for chat completion.
761    ///
762    /// # Arguments
763    ///
764    /// * `model` - The model to use (e.g., `ChatModel::Gpt4oMini`, `ChatModel::Gpt4o`)
765    ///
766    /// # Returns
767    ///
768    /// A mutable reference to self for method chaining
769    ///
770    /// # Example
771    ///
772    /// ```rust,no_run
773    /// use openai_tools::chat::request::ChatCompletion;
774    /// use openai_tools::common::models::ChatModel;
775    ///
776    /// let mut chat = ChatCompletion::new();
777    /// chat.model(ChatModel::Gpt4oMini);
778    /// ```
779    pub fn model(&mut self, model: ChatModel) -> &mut Self {
780        self.request_body.model = model;
781        self
782    }
783
784    /// Sets the model using a string ID (for backward compatibility).
785    ///
786    /// Prefer using [`model`] with `ChatModel` enum for type safety.
787    ///
788    /// # Arguments
789    ///
790    /// * `model_id` - OpenAI model ID string (e.g., "gpt-4o-mini")
791    ///
792    /// # Returns
793    ///
794    /// A mutable reference to self for method chaining
795    ///
796    /// # Example
797    ///
798    /// ```rust,no_run
799    /// use openai_tools::chat::request::ChatCompletion;
800    ///
801    /// let mut chat = ChatCompletion::new();
802    /// chat.model_id("gpt-4o-mini");
803    /// ```
804    #[deprecated(since = "0.2.0", note = "Use `model(ChatModel)` instead for type safety")]
805    pub fn model_id<T: AsRef<str>>(&mut self, model_id: T) -> &mut Self {
806        self.request_body.model = ChatModel::from(model_id.as_ref());
807        self
808    }
809
810    /// Sets the request timeout duration
811    ///
812    /// # Arguments
813    ///
814    /// * `timeout` - The maximum time to wait for a response
815    ///
816    /// # Returns
817    ///
818    /// A mutable reference to self for method chaining
819    ///
820    /// # Example
821    ///
822    /// ```rust,no_run
823    /// use std::time::Duration;
824    /// use openai_tools::chat::request::ChatCompletion;
825    ///
826    /// let mut chat = ChatCompletion::new();
827    /// chat.model_id("gpt-4o-mini")
828    ///     .timeout(Duration::from_secs(30));
829    /// ```
830    pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
831        self.timeout = Some(timeout);
832        self
833    }
834
835    /// Sets the chat message history
836    ///
837    /// # Arguments
838    ///
839    /// * `messages` - Vector of chat messages representing the conversation history
840    ///
841    /// # Returns
842    ///
843    /// A mutable reference to self for method chaining
844    pub fn messages(&mut self, messages: Vec<Message>) -> &mut Self {
845        self.request_body.messages = messages;
846        self
847    }
848
849    /// Adds a single message to the conversation history
850    ///
851    /// This method appends a new message to the existing conversation history.
852    /// It's useful for building conversations incrementally.
853    ///
854    /// # Arguments
855    ///
856    /// * `message` - The message to add to the conversation
857    ///
858    /// # Returns
859    ///
860    /// A mutable reference to self for method chaining
861    ///
862    /// # Examples
863    ///
864    /// ```rust,no_run
865    /// use openai_tools::chat::request::ChatCompletion;
866    /// use openai_tools::common::message::Message;
867    /// use openai_tools::common::role::Role;
868    ///
869    /// let mut chat = ChatCompletion::new();
870    /// chat.add_message(Message::from_string(Role::User, "Hello!"))
871    ///     .add_message(Message::from_string(Role::Assistant, "Hi there!"))
872    ///     .add_message(Message::from_string(Role::User, "How are you?"));
873    /// ```
874    pub fn add_message(&mut self, message: Message) -> &mut Self {
875        self.request_body.messages.push(message);
876        self
877    }
878    /// Sets whether to store the request and response at OpenAI
879    ///
880    /// # Arguments
881    ///
882    /// * `store` - `true` to store, `false` to not store
883    ///
884    /// # Returns
885    ///
886    /// A mutable reference to self for method chaining
887    pub fn store(&mut self, store: bool) -> &mut Self {
888        self.request_body.store = Option::from(store);
889        self
890    }
891
892    /// Sets the frequency penalty
893    ///
894    /// A parameter that penalizes based on word frequency to reduce repetition.
895    /// Positive values decrease repetition, negative values increase it.
896    ///
897    /// **Note:** Reasoning models (GPT-5, o-series) only support frequency_penalty=0.
898    /// For these models, non-zero values will be ignored with a warning.
899    ///
900    /// # Arguments
901    ///
902    /// * `frequency_penalty` - Frequency penalty value (range: -2.0 to 2.0)
903    ///
904    /// # Returns
905    ///
906    /// A mutable reference to self for method chaining
907    pub fn frequency_penalty(&mut self, frequency_penalty: f32) -> &mut Self {
908        let support = self.request_body.model.parameter_support();
909        match support.frequency_penalty {
910            ParameterRestriction::FixedValue(fixed) => {
911                if (frequency_penalty as f64 - fixed).abs() > f64::EPSILON {
912                    tracing::warn!(
913                        "Model '{}' only supports frequency_penalty={}. Ignoring frequency_penalty={}.",
914                        self.request_body.model,
915                        fixed,
916                        frequency_penalty
917                    );
918                    return self;
919                }
920            }
921            ParameterRestriction::NotSupported => {
922                tracing::warn!("Model '{}' does not support frequency_penalty parameter. Ignoring.", self.request_body.model);
923                return self;
924            }
925            ParameterRestriction::Any => {}
926        }
927        self.request_body.frequency_penalty = Some(frequency_penalty);
928        self
929    }
930
931    /// Sets logit bias to adjust the probability of specific tokens
932    ///
933    /// **Note:** Reasoning models (GPT-5, o-series) do not support logit_bias.
934    /// For these models, this parameter will be ignored with a warning.
935    ///
936    /// # Arguments
937    ///
938    /// * `logit_bias` - A map of token IDs to adjustment values
939    ///
940    /// # Returns
941    ///
942    /// A mutable reference to self for method chaining
943    pub fn logit_bias<T: AsRef<str>>(&mut self, logit_bias: HashMap<T, i32>) -> &mut Self {
944        let support = self.request_body.model.parameter_support();
945        if !support.logit_bias {
946            tracing::warn!("Model '{}' does not support logit_bias parameter. Ignoring.", self.request_body.model);
947            return self;
948        }
949        self.request_body.logit_bias = Some(logit_bias.into_iter().map(|(k, v)| (k.as_ref().to_string(), v)).collect::<HashMap<String, i32>>());
950        self
951    }
952
953    /// Sets whether to include probability information for each token
954    ///
955    /// **Note:** Reasoning models (GPT-5, o-series) do not support logprobs.
956    /// For these models, this parameter will be ignored with a warning.
957    ///
958    /// # Arguments
959    ///
960    /// * `logprobs` - `true` to include probability information
961    ///
962    /// # Returns
963    ///
964    /// A mutable reference to self for method chaining
965    pub fn logprobs(&mut self, logprobs: bool) -> &mut Self {
966        let support = self.request_body.model.parameter_support();
967        if !support.logprobs {
968            tracing::warn!("Model '{}' does not support logprobs parameter. Ignoring.", self.request_body.model);
969            return self;
970        }
971        self.request_body.logprobs = Some(logprobs);
972        self
973    }
974
975    /// Sets the number of top probabilities to return for each token
976    ///
977    /// **Note:** Reasoning models (GPT-5, o-series) do not support top_logprobs.
978    /// For these models, this parameter will be ignored with a warning.
979    ///
980    /// # Arguments
981    ///
982    /// * `top_logprobs` - Number of top probabilities (range: 0-20)
983    ///
984    /// # Returns
985    ///
986    /// A mutable reference to self for method chaining
987    pub fn top_logprobs(&mut self, top_logprobs: u8) -> &mut Self {
988        let support = self.request_body.model.parameter_support();
989        if !support.top_logprobs {
990            tracing::warn!("Model '{}' does not support top_logprobs parameter. Ignoring.", self.request_body.model);
991            return self;
992        }
993        self.request_body.top_logprobs = Some(top_logprobs);
994        self
995    }
996
997    /// Sets the maximum number of tokens to generate
998    ///
999    /// # Arguments
1000    ///
1001    /// * `max_completion_tokens` - Maximum number of tokens
1002    ///
1003    /// # Returns
1004    ///
1005    /// A mutable reference to self for method chaining
1006    pub fn max_completion_tokens(&mut self, max_completion_tokens: u64) -> &mut Self {
1007        self.request_body.max_completion_tokens = Option::from(max_completion_tokens);
1008        self
1009    }
1010
1011    /// Sets the number of responses to generate
1012    ///
1013    /// **Note:** Reasoning models (GPT-5, o-series) only support n=1.
1014    /// For these models, values other than 1 will be ignored with a warning.
1015    ///
1016    /// # Arguments
1017    ///
1018    /// * `n` - Number of responses to generate
1019    ///
1020    /// # Returns
1021    ///
1022    /// A mutable reference to self for method chaining
1023    pub fn n(&mut self, n: u32) -> &mut Self {
1024        let support = self.request_body.model.parameter_support();
1025        if !support.n_multiple && n != 1 {
1026            tracing::warn!("Model '{}' only supports n=1. Ignoring n={}.", self.request_body.model, n);
1027            return self;
1028        }
1029        self.request_body.n = Some(n);
1030        self
1031    }
1032
1033    /// Sets the available modalities for the response
1034    ///
1035    /// # Arguments
1036    ///
1037    /// * `modalities` - List of modalities (e.g., `["text", "audio"]`)
1038    ///
1039    /// # Returns
1040    ///
1041    /// A mutable reference to self for method chaining
1042    pub fn modalities<T: AsRef<str>>(&mut self, modalities: Vec<T>) -> &mut Self {
1043        self.request_body.modalities = Option::from(modalities.into_iter().map(|m| m.as_ref().to_string()).collect::<Vec<String>>());
1044        self
1045    }
1046
1047    /// Sets the presence penalty
1048    ///
1049    /// A parameter that controls the tendency to include new content in the document.
1050    /// Positive values encourage talking about new topics, negative values encourage
1051    /// staying on existing topics.
1052    ///
1053    /// **Note:** Reasoning models (GPT-5, o-series) only support presence_penalty=0.
1054    /// For these models, non-zero values will be ignored with a warning.
1055    ///
1056    /// # Arguments
1057    ///
1058    /// * `presence_penalty` - Presence penalty value (range: -2.0 to 2.0)
1059    ///
1060    /// # Returns
1061    ///
1062    /// A mutable reference to self for method chaining
1063    pub fn presence_penalty(&mut self, presence_penalty: f32) -> &mut Self {
1064        let support = self.request_body.model.parameter_support();
1065        match support.presence_penalty {
1066            ParameterRestriction::FixedValue(fixed) => {
1067                if (presence_penalty as f64 - fixed).abs() > f64::EPSILON {
1068                    tracing::warn!(
1069                        "Model '{}' only supports presence_penalty={}. Ignoring presence_penalty={}.",
1070                        self.request_body.model,
1071                        fixed,
1072                        presence_penalty
1073                    );
1074                    return self;
1075                }
1076            }
1077            ParameterRestriction::NotSupported => {
1078                tracing::warn!("Model '{}' does not support presence_penalty parameter. Ignoring.", self.request_body.model);
1079                return self;
1080            }
1081            ParameterRestriction::Any => {}
1082        }
1083        self.request_body.presence_penalty = Some(presence_penalty);
1084        self
1085    }
1086
1087    /// Sets the temperature parameter to control response randomness
1088    ///
1089    /// Higher values (e.g., 1.0) produce more creative and diverse outputs,
1090    /// while lower values (e.g., 0.2) produce more deterministic and consistent outputs.
1091    ///
1092    /// **Note:** Reasoning models (GPT-5, o-series) only support temperature=1.0.
1093    /// For these models, other values will be ignored with a warning.
1094    ///
1095    /// # Arguments
1096    ///
1097    /// * `temperature` - Temperature parameter (range: 0.0 to 2.0)
1098    ///
1099    /// # Returns
1100    ///
1101    /// A mutable reference to self for method chaining
1102    pub fn temperature(&mut self, temperature: f32) -> &mut Self {
1103        let support = self.request_body.model.parameter_support();
1104        match support.temperature {
1105            ParameterRestriction::FixedValue(fixed) => {
1106                if (temperature as f64 - fixed).abs() > f64::EPSILON {
1107                    tracing::warn!("Model '{}' only supports temperature={}. Ignoring temperature={}.", self.request_body.model, fixed, temperature);
1108                    return self;
1109                }
1110            }
1111            ParameterRestriction::NotSupported => {
1112                tracing::warn!("Model '{}' does not support temperature parameter. Ignoring.", self.request_body.model);
1113                return self;
1114            }
1115            ParameterRestriction::Any => {}
1116        }
1117        self.request_body.temperature = Some(temperature);
1118        self
1119    }
1120
1121    /// Sets structured output using JSON schema
1122    ///
1123    /// Enables receiving responses in a structured JSON format according to the
1124    /// specified JSON schema.
1125    ///
1126    /// # Arguments
1127    ///
1128    /// * `json_schema` - JSON schema defining the response structure
1129    ///
1130    /// # Returns
1131    ///
1132    /// A mutable reference to self for method chaining
1133    pub fn json_schema(&mut self, json_schema: Schema) -> &mut Self {
1134        self.request_body.response_format = Option::from(Format::new(String::from("json_schema"), json_schema));
1135        self
1136    }
1137
1138    /// Sets the tools that can be called by the model
1139    ///
1140    /// Enables function calling by providing a list of tools that the model can choose to call.
1141    /// When tools are provided, the model may generate tool calls instead of or in addition to
1142    /// regular text responses.
1143    ///
1144    /// # Arguments
1145    ///
1146    /// * `tools` - Vector of tools available for the model to use
1147    ///
1148    /// # Returns
1149    ///
1150    /// A mutable reference to self for method chaining
1151    pub fn tools(&mut self, tools: Vec<Tool>) -> &mut Self {
1152        self.request_body.tools = Option::from(tools);
1153        self
1154    }
1155
1156    /// Sets the safety identifier for end-user tracking
1157    ///
1158    /// A stable identifier used to help OpenAI detect users of your application
1159    /// that may be violating usage policies. This enables per-user safety
1160    /// monitoring and abuse detection.
1161    ///
1162    /// # Arguments
1163    ///
1164    /// * `safety_id` - A unique, stable identifier for the end user
1165    ///   (recommended: hash of email or internal user ID)
1166    ///
1167    /// # Returns
1168    ///
1169    /// A mutable reference to self for method chaining
1170    ///
1171    /// # Examples
1172    ///
1173    /// ```rust
1174    /// use openai_tools::chat::request::ChatCompletion;
1175    ///
1176    /// let mut chat = ChatCompletion::new();
1177    /// chat.safety_identifier("user_abc123");
1178    /// ```
1179    pub fn safety_identifier<T: AsRef<str>>(&mut self, safety_id: T) -> &mut Self {
1180        self.request_body.safety_identifier = Some(safety_id.as_ref().to_string());
1181        self
1182    }
1183
1184    /// Gets the current message history
1185    ///
1186    /// # Returns
1187    ///
1188    /// A vector containing the message history
1189    pub fn get_message_history(&self) -> Vec<Message> {
1190        self.request_body.messages.clone()
1191    }
1192
1193    /// Checks if the model is a reasoning model that doesn't support custom temperature
1194    ///
1195    /// Reasoning models (o1, o3, o4 series) only support the default temperature value of 1.0.
1196    /// This method checks if the current model is one of these reasoning models.
1197    ///
1198    /// # Returns
1199    ///
1200    /// `true` if the model is a reasoning model, `false` otherwise
1201    ///
1202    /// # Supported Reasoning Models
1203    ///
1204    /// - `o1`, `o1-pro`, and variants
1205    /// - `o3`, `o3-mini`, and variants
1206    /// - `o4-mini` and variants
1207    fn is_reasoning_model(&self) -> bool {
1208        self.request_body.model.is_reasoning_model()
1209    }
1210
1211    /// Sends the chat completion request to OpenAI API
1212    ///
1213    /// This method validates the request parameters, constructs the HTTP request,
1214    /// and sends it to the OpenAI Chat Completions endpoint.
1215    ///
1216    /// # Returns
1217    ///
1218    /// A `Result` containing the API response on success, or an error on failure.
1219    ///
1220    /// # Errors
1221    ///
1222    /// Returns an error if:
1223    /// - API key is not set
1224    /// - Model ID is not set
1225    /// - Messages are empty
1226    /// - Network request fails
1227    /// - Response parsing fails
1228    ///
1229    /// # Parameter Validation
1230    ///
1231    /// For reasoning models (GPT-5, o-series), certain parameters have restrictions:
1232    /// - `temperature`: only 1.0 supported
1233    /// - `frequency_penalty`: only 0 supported
1234    /// - `presence_penalty`: only 0 supported
1235    /// - `logprobs`, `top_logprobs`, `logit_bias`: not supported
1236    /// - `n`: only 1 supported
1237    ///
1238    /// **Validation occurs at two points:**
1239    /// 1. At setter time (when using `with_model()` constructor) - immediate warning
1240    /// 2. At API call time (fallback) - for cases where model is changed after setting params
1241    ///
1242    /// Unsupported parameter values are ignored with a warning and the request proceeds.
1243    ///
1244    /// # Example
1245    ///
1246    /// ```rust,no_run
1247    /// use openai_tools::chat::request::ChatCompletion;
1248    /// use openai_tools::common::message::Message;
1249    /// use openai_tools::common::role::Role;
1250    ///
1251    /// # #[tokio::main]
1252    /// # async fn main() -> Result<(), Box<dyn std::error::Error>>
1253    /// # {
1254    /// let mut chat = ChatCompletion::new();
1255    /// let messages = vec![Message::from_string(Role::User, "Hello!")];
1256    ///
1257    /// let response = chat
1258    ///     .model_id("gpt-4o-mini")
1259    ///     .messages(messages)
1260    ///     .temperature(1.0)
1261    ///     .chat()
1262    ///     .await?;
1263    ///
1264    /// println!("{}", response.choices[0].message.content.as_ref().unwrap().text.as_ref().unwrap());
1265    /// # Ok::<(), Box<dyn std::error::Error>>(())
1266    /// # }
1267    /// ```
1268    pub async fn chat(&mut self) -> Result<Response> {
1269        // Validate that messages are set
1270        if self.request_body.messages.is_empty() {
1271            return Err(OpenAIToolError::Error("Messages are not set.".into()));
1272        }
1273
1274        // Handle reasoning models that don't support certain parameters
1275        // See: https://platform.openai.com/docs/guides/reasoning
1276        if self.is_reasoning_model() {
1277            let model = &self.request_body.model;
1278
1279            // Temperature: only default (1.0) is supported
1280            if let Some(temp) = self.request_body.temperature {
1281                if (temp - 1.0).abs() > f32::EPSILON {
1282                    tracing::warn!(
1283                        "Reasoning model '{}' does not support custom temperature. \
1284                         Ignoring temperature={} and using default (1.0).",
1285                        model,
1286                        temp
1287                    );
1288                    self.request_body.temperature = None;
1289                }
1290            }
1291
1292            // Frequency penalty: only 0 is supported
1293            if let Some(fp) = self.request_body.frequency_penalty {
1294                if fp.abs() > f32::EPSILON {
1295                    tracing::warn!(
1296                        "Reasoning model '{}' does not support frequency_penalty. \
1297                         Ignoring frequency_penalty={} and using default (0).",
1298                        model,
1299                        fp
1300                    );
1301                    self.request_body.frequency_penalty = None;
1302                }
1303            }
1304
1305            // Presence penalty: only 0 is supported
1306            if let Some(pp) = self.request_body.presence_penalty {
1307                if pp.abs() > f32::EPSILON {
1308                    tracing::warn!(
1309                        "Reasoning model '{}' does not support presence_penalty. \
1310                         Ignoring presence_penalty={} and using default (0).",
1311                        model,
1312                        pp
1313                    );
1314                    self.request_body.presence_penalty = None;
1315                }
1316            }
1317
1318            // Logprobs: not supported
1319            if self.request_body.logprobs.is_some() {
1320                tracing::warn!("Reasoning model '{}' does not support logprobs. Ignoring logprobs parameter.", model);
1321                self.request_body.logprobs = None;
1322            }
1323
1324            // Top logprobs: not supported
1325            if self.request_body.top_logprobs.is_some() {
1326                tracing::warn!("Reasoning model '{}' does not support top_logprobs. Ignoring top_logprobs parameter.", model);
1327                self.request_body.top_logprobs = None;
1328            }
1329
1330            // Logit bias: not supported
1331            if self.request_body.logit_bias.is_some() {
1332                tracing::warn!("Reasoning model '{}' does not support logit_bias. Ignoring logit_bias parameter.", model);
1333                self.request_body.logit_bias = None;
1334            }
1335
1336            // N: only 1 is supported
1337            if let Some(n) = self.request_body.n {
1338                if n != 1 {
1339                    tracing::warn!(
1340                        "Reasoning model '{}' does not support n != 1. \
1341                         Ignoring n={} and using default (1).",
1342                        model,
1343                        n
1344                    );
1345                    self.request_body.n = None;
1346                }
1347            }
1348        }
1349
1350        let body = serde_json::to_string(&self.request_body)?;
1351
1352        let client = create_http_client(self.timeout)?;
1353        let mut headers = request::header::HeaderMap::new();
1354        headers.insert("Content-Type", request::header::HeaderValue::from_static("application/json"));
1355        headers.insert("User-Agent", request::header::HeaderValue::from_static("openai-tools-rust"));
1356
1357        // Apply provider-specific authentication headers
1358        self.auth.apply_headers(&mut headers)?;
1359
1360        if cfg!(debug_assertions) {
1361            // Replace API key with a placeholder in debug mode
1362            let body_for_debug = serde_json::to_string_pretty(&self.request_body).unwrap().replace(self.auth.api_key(), "*************");
1363            tracing::info!("Request body: {}", body_for_debug);
1364        }
1365
1366        // Get the endpoint URL from the auth provider
1367        let endpoint = self.auth.endpoint(CHAT_COMPLETIONS_PATH);
1368
1369        let response = client.post(&endpoint).headers(headers).body(body).send().await.map_err(OpenAIToolError::RequestError)?;
1370        let status = response.status();
1371        let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
1372
1373        if cfg!(debug_assertions) {
1374            tracing::info!("Response content: {}", content);
1375        }
1376
1377        if !status.is_success() {
1378            if let Ok(error_resp) = serde_json::from_str::<ErrorResponse>(&content) {
1379                return Err(OpenAIToolError::Error(error_resp.error.message.unwrap_or_default()));
1380            }
1381            return Err(OpenAIToolError::Error(format!("API error ({}): {}", status, content)));
1382        }
1383
1384        serde_json::from_str::<Response>(&content).map_err(OpenAIToolError::SerdeJsonError)
1385    }
1386
1387    /// Creates a test-only ChatCompletion instance without authentication
1388    ///
1389    /// This is only available in test mode and bypasses API key requirements.
1390    #[cfg(test)]
1391    pub(crate) fn test_new_with_model(model: ChatModel) -> Self {
1392        use crate::common::auth::OpenAIAuth;
1393        Self { auth: AuthProvider::OpenAI(OpenAIAuth::new("test-key")), request_body: Body { model, ..Default::default() }, timeout: None }
1394    }
1395}
1396
1397#[cfg(test)]
1398mod tests {
1399    use super::*;
1400    use crate::common::models::ChatModel;
1401    use std::collections::HashMap;
1402
1403    // =============================================================================
1404    // Standard Model Parameter Tests
1405    // =============================================================================
1406
1407    #[test]
1408    fn test_standard_model_accepts_all_parameters() {
1409        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt4oMini);
1410
1411        // Standard models should accept all parameters
1412        chat.temperature(0.7);
1413        chat.frequency_penalty(0.5);
1414        chat.presence_penalty(0.5);
1415        chat.logprobs(true);
1416        chat.top_logprobs(5);
1417        chat.n(3);
1418
1419        let logit_bias: HashMap<&str, i32> = [("1234", 10)].iter().cloned().collect();
1420        chat.logit_bias(logit_bias);
1421
1422        assert_eq!(chat.request_body.temperature, Some(0.7));
1423        assert_eq!(chat.request_body.frequency_penalty, Some(0.5));
1424        assert_eq!(chat.request_body.presence_penalty, Some(0.5));
1425        assert_eq!(chat.request_body.logprobs, Some(true));
1426        assert_eq!(chat.request_body.top_logprobs, Some(5));
1427        assert_eq!(chat.request_body.n, Some(3));
1428        assert!(chat.request_body.logit_bias.is_some());
1429    }
1430
1431    #[test]
1432    fn test_gpt4o_accepts_all_parameters() {
1433        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt4o);
1434
1435        chat.temperature(0.3);
1436        chat.frequency_penalty(-1.0);
1437        chat.presence_penalty(1.5);
1438
1439        assert_eq!(chat.request_body.temperature, Some(0.3));
1440        assert_eq!(chat.request_body.frequency_penalty, Some(-1.0));
1441        assert_eq!(chat.request_body.presence_penalty, Some(1.5));
1442    }
1443
1444    #[test]
1445    fn test_gpt4_1_accepts_all_parameters() {
1446        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt4_1);
1447
1448        chat.temperature(1.5);
1449        chat.frequency_penalty(0.8);
1450        chat.n(2);
1451
1452        assert_eq!(chat.request_body.temperature, Some(1.5));
1453        assert_eq!(chat.request_body.frequency_penalty, Some(0.8));
1454        assert_eq!(chat.request_body.n, Some(2));
1455    }
1456
1457    // =============================================================================
1458    // O-Series Reasoning Model Tests
1459    // =============================================================================
1460
1461    #[test]
1462    fn test_o1_ignores_non_default_temperature() {
1463        let mut chat = ChatCompletion::test_new_with_model(ChatModel::O1);
1464
1465        // Non-default temperature should be ignored
1466        chat.temperature(0.5);
1467        assert_eq!(chat.request_body.temperature, None);
1468
1469        // Default temperature (1.0) should be accepted
1470        chat.temperature(1.0);
1471        assert_eq!(chat.request_body.temperature, Some(1.0));
1472    }
1473
1474    #[test]
1475    fn test_o3_mini_ignores_non_default_temperature() {
1476        let mut chat = ChatCompletion::test_new_with_model(ChatModel::O3Mini);
1477
1478        chat.temperature(0.3);
1479        assert_eq!(chat.request_body.temperature, None);
1480    }
1481
1482    #[test]
1483    fn test_o4_mini_ignores_non_default_temperature() {
1484        let mut chat = ChatCompletion::test_new_with_model(ChatModel::O4Mini);
1485
1486        chat.temperature(0.7);
1487        assert_eq!(chat.request_body.temperature, None);
1488    }
1489
1490    #[test]
1491    fn test_o1_ignores_frequency_penalty() {
1492        let mut chat = ChatCompletion::test_new_with_model(ChatModel::O1);
1493
1494        // Non-zero frequency_penalty should be ignored
1495        chat.frequency_penalty(0.5);
1496        assert_eq!(chat.request_body.frequency_penalty, None);
1497
1498        // Zero value should be accepted
1499        chat.frequency_penalty(0.0);
1500        assert_eq!(chat.request_body.frequency_penalty, Some(0.0));
1501    }
1502
1503    #[test]
1504    fn test_o3_ignores_presence_penalty() {
1505        let mut chat = ChatCompletion::test_new_with_model(ChatModel::O3);
1506
1507        // Non-zero presence_penalty should be ignored
1508        chat.presence_penalty(0.5);
1509        assert_eq!(chat.request_body.presence_penalty, None);
1510
1511        // Zero value should be accepted
1512        chat.presence_penalty(0.0);
1513        assert_eq!(chat.request_body.presence_penalty, Some(0.0));
1514    }
1515
1516    #[test]
1517    fn test_o1_ignores_logprobs() {
1518        let mut chat = ChatCompletion::test_new_with_model(ChatModel::O1);
1519
1520        chat.logprobs(true);
1521        assert_eq!(chat.request_body.logprobs, None);
1522    }
1523
1524    #[test]
1525    fn test_o3_mini_ignores_top_logprobs() {
1526        let mut chat = ChatCompletion::test_new_with_model(ChatModel::O3Mini);
1527
1528        chat.top_logprobs(5);
1529        assert_eq!(chat.request_body.top_logprobs, None);
1530    }
1531
1532    #[test]
1533    fn test_o1_ignores_logit_bias() {
1534        let mut chat = ChatCompletion::test_new_with_model(ChatModel::O1);
1535
1536        let logit_bias: HashMap<&str, i32> = [("1234", 10)].iter().cloned().collect();
1537        chat.logit_bias(logit_bias);
1538        assert_eq!(chat.request_body.logit_bias, None);
1539    }
1540
1541    #[test]
1542    fn test_o1_ignores_n_greater_than_1() {
1543        let mut chat = ChatCompletion::test_new_with_model(ChatModel::O1);
1544
1545        // n > 1 should be ignored
1546        chat.n(3);
1547        assert_eq!(chat.request_body.n, None);
1548
1549        // n = 1 should be accepted
1550        chat.n(1);
1551        assert_eq!(chat.request_body.n, Some(1));
1552    }
1553
1554    // =============================================================================
1555    // GPT-5 Series Reasoning Model Tests
1556    // =============================================================================
1557
1558    #[test]
1559    fn test_gpt5_2_ignores_non_default_temperature() {
1560        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt5_2);
1561
1562        chat.temperature(0.5);
1563        assert_eq!(chat.request_body.temperature, None);
1564
1565        chat.temperature(1.0);
1566        assert_eq!(chat.request_body.temperature, Some(1.0));
1567    }
1568
1569    #[test]
1570    fn test_gpt5_1_ignores_non_default_temperature() {
1571        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt5_1);
1572
1573        chat.temperature(0.3);
1574        assert_eq!(chat.request_body.temperature, None);
1575    }
1576
1577    #[test]
1578    fn test_gpt5_mini_ignores_frequency_penalty() {
1579        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt5Mini);
1580
1581        chat.frequency_penalty(0.5);
1582        assert_eq!(chat.request_body.frequency_penalty, None);
1583    }
1584
1585    #[test]
1586    fn test_gpt5_2_pro_ignores_presence_penalty() {
1587        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt5_2Pro);
1588
1589        chat.presence_penalty(0.8);
1590        assert_eq!(chat.request_body.presence_penalty, None);
1591    }
1592
1593    #[test]
1594    fn test_gpt5_1_codex_max_ignores_logprobs() {
1595        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt5_1CodexMax);
1596
1597        chat.logprobs(true);
1598        assert_eq!(chat.request_body.logprobs, None);
1599    }
1600
1601    #[test]
1602    fn test_gpt5_2_chat_latest_ignores_n_greater_than_1() {
1603        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt5_2ChatLatest);
1604
1605        chat.n(5);
1606        assert_eq!(chat.request_body.n, None);
1607    }
1608
1609    // =============================================================================
1610    // Multiple Restricted Parameters Tests
1611    // =============================================================================
1612
1613    #[test]
1614    fn test_o1_ignores_all_restricted_parameters_at_once() {
1615        let mut chat = ChatCompletion::test_new_with_model(ChatModel::O1);
1616
1617        // Set all restricted parameters
1618        chat.temperature(0.5);
1619        chat.frequency_penalty(0.5);
1620        chat.presence_penalty(0.5);
1621        chat.logprobs(true);
1622        chat.top_logprobs(5);
1623        chat.n(3);
1624
1625        let logit_bias: HashMap<&str, i32> = [("1234", 10)].iter().cloned().collect();
1626        chat.logit_bias(logit_bias);
1627
1628        // All should be ignored
1629        assert_eq!(chat.request_body.temperature, None);
1630        assert_eq!(chat.request_body.frequency_penalty, None);
1631        assert_eq!(chat.request_body.presence_penalty, None);
1632        assert_eq!(chat.request_body.logprobs, None);
1633        assert_eq!(chat.request_body.top_logprobs, None);
1634        assert_eq!(chat.request_body.n, None);
1635        assert_eq!(chat.request_body.logit_bias, None);
1636    }
1637
1638    #[test]
1639    fn test_gpt5_2_ignores_all_restricted_parameters_at_once() {
1640        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt5_2);
1641
1642        chat.temperature(0.5);
1643        chat.frequency_penalty(0.5);
1644        chat.presence_penalty(0.5);
1645        chat.logprobs(true);
1646        chat.top_logprobs(5);
1647        chat.n(3);
1648
1649        let logit_bias: HashMap<&str, i32> = [("1234", 10)].iter().cloned().collect();
1650        chat.logit_bias(logit_bias);
1651
1652        assert_eq!(chat.request_body.temperature, None);
1653        assert_eq!(chat.request_body.frequency_penalty, None);
1654        assert_eq!(chat.request_body.presence_penalty, None);
1655        assert_eq!(chat.request_body.logprobs, None);
1656        assert_eq!(chat.request_body.top_logprobs, None);
1657        assert_eq!(chat.request_body.n, None);
1658        assert_eq!(chat.request_body.logit_bias, None);
1659    }
1660
1661    // =============================================================================
1662    // Custom Model Tests
1663    // =============================================================================
1664
1665    #[test]
1666    fn test_custom_gpt5_model_detected_as_reasoning() {
1667        let mut chat = ChatCompletion::test_new_with_model(ChatModel::custom("gpt-5.3-preview"));
1668
1669        // Custom GPT-5 models should be treated as reasoning models
1670        chat.temperature(0.5);
1671        assert_eq!(chat.request_body.temperature, None);
1672    }
1673
1674    #[test]
1675    fn test_custom_o1_model_detected_as_reasoning() {
1676        let mut chat = ChatCompletion::test_new_with_model(ChatModel::custom("o1-pro-2025-01-15"));
1677
1678        // Custom o1-series models should be treated as reasoning models
1679        chat.temperature(0.5);
1680        assert_eq!(chat.request_body.temperature, None);
1681    }
1682
1683    #[test]
1684    fn test_custom_o3_model_detected_as_reasoning() {
1685        let mut chat = ChatCompletion::test_new_with_model(ChatModel::custom("o3-high"));
1686
1687        // Custom o3-series models should be treated as reasoning models
1688        chat.temperature(0.5);
1689        assert_eq!(chat.request_body.temperature, None);
1690    }
1691
1692    #[test]
1693    fn test_custom_o4_model_detected_as_reasoning() {
1694        let mut chat = ChatCompletion::test_new_with_model(ChatModel::custom("o4-mini-preview"));
1695
1696        // Custom o4-series models should be treated as reasoning models
1697        chat.temperature(0.5);
1698        assert_eq!(chat.request_body.temperature, None);
1699    }
1700
1701    #[test]
1702    fn test_custom_standard_model_accepts_all_parameters() {
1703        let mut chat = ChatCompletion::test_new_with_model(ChatModel::custom("ft:gpt-4o-mini:org::123"));
1704
1705        // Fine-tuned standard models should accept all parameters
1706        chat.temperature(0.7);
1707        chat.frequency_penalty(0.5);
1708        chat.n(2);
1709
1710        assert_eq!(chat.request_body.temperature, Some(0.7));
1711        assert_eq!(chat.request_body.frequency_penalty, Some(0.5));
1712        assert_eq!(chat.request_body.n, Some(2));
1713    }
1714
1715    // =============================================================================
1716    // Parameter Boundary Tests
1717    // =============================================================================
1718
1719    #[test]
1720    fn test_temperature_boundary_values() {
1721        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt4oMini);
1722
1723        // Minimum value
1724        chat.temperature(0.0);
1725        assert_eq!(chat.request_body.temperature, Some(0.0));
1726
1727        // Maximum value
1728        chat.temperature(2.0);
1729        assert_eq!(chat.request_body.temperature, Some(2.0));
1730    }
1731
1732    #[test]
1733    fn test_frequency_penalty_boundary_values() {
1734        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt4oMini);
1735
1736        // Minimum value
1737        chat.frequency_penalty(-2.0);
1738        assert_eq!(chat.request_body.frequency_penalty, Some(-2.0));
1739
1740        // Maximum value
1741        chat.frequency_penalty(2.0);
1742        assert_eq!(chat.request_body.frequency_penalty, Some(2.0));
1743    }
1744
1745    #[test]
1746    fn test_presence_penalty_boundary_values() {
1747        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt4oMini);
1748
1749        // Minimum value
1750        chat.presence_penalty(-2.0);
1751        assert_eq!(chat.request_body.presence_penalty, Some(-2.0));
1752
1753        // Maximum value
1754        chat.presence_penalty(2.0);
1755        assert_eq!(chat.request_body.presence_penalty, Some(2.0));
1756    }
1757
1758    // =============================================================================
1759    // Model-Specific Unrestricted Parameters Tests
1760    // =============================================================================
1761
1762    #[test]
1763    fn test_max_completion_tokens_accepted_by_all_models() {
1764        // Standard model
1765        let mut chat_standard = ChatCompletion::test_new_with_model(ChatModel::Gpt4oMini);
1766        chat_standard.max_completion_tokens(1000);
1767        assert_eq!(chat_standard.request_body.max_completion_tokens, Some(1000));
1768
1769        // Reasoning model
1770        let mut chat_reasoning = ChatCompletion::test_new_with_model(ChatModel::O1);
1771        chat_reasoning.max_completion_tokens(2000);
1772        assert_eq!(chat_reasoning.request_body.max_completion_tokens, Some(2000));
1773
1774        // GPT-5 model
1775        let mut chat_gpt5 = ChatCompletion::test_new_with_model(ChatModel::Gpt5_2);
1776        chat_gpt5.max_completion_tokens(3000);
1777        assert_eq!(chat_gpt5.request_body.max_completion_tokens, Some(3000));
1778    }
1779
1780    #[test]
1781    fn test_store_accepted_by_all_models() {
1782        let mut chat_standard = ChatCompletion::test_new_with_model(ChatModel::Gpt4oMini);
1783        chat_standard.store(true);
1784        assert_eq!(chat_standard.request_body.store, Some(true));
1785
1786        let mut chat_reasoning = ChatCompletion::test_new_with_model(ChatModel::O1);
1787        chat_reasoning.store(false);
1788        assert_eq!(chat_reasoning.request_body.store, Some(false));
1789    }
1790
1791    // =============================================================================
1792    // Chat API Content Serialization Tests
1793    // =============================================================================
1794
1795    #[test]
1796    fn test_chat_text_content_serialization() {
1797        use crate::common::message::Content;
1798
1799        let content = Content::from_text("Hello, world!");
1800        let wrapper = ChatContentRef(&content);
1801        let json = serde_json::to_value(&wrapper).unwrap();
1802
1803        assert_eq!(json["type"], "text");
1804        assert_eq!(json["text"], "Hello, world!");
1805        assert!(json.get("image_url").is_none());
1806    }
1807
1808    #[test]
1809    fn test_chat_image_content_serialization() {
1810        use crate::common::message::Content;
1811
1812        let content = Content::from_image_url("https://example.com/image.png");
1813        let wrapper = ChatContentRef(&content);
1814        let json = serde_json::to_value(&wrapper).unwrap();
1815
1816        assert_eq!(json["type"], "image_url");
1817        assert_eq!(json["image_url"]["url"], "https://example.com/image.png");
1818    }
1819
1820    #[test]
1821    fn test_chat_multimodal_message_serialization() {
1822        use crate::common::message::{Content, Message};
1823        use crate::common::role::Role;
1824
1825        let contents = vec![Content::from_text("What's in this image?"), Content::from_image_url("https://example.com/image.png")];
1826        let message = Message::from_message_array(Role::User, contents);
1827        let wrapper = ChatMessageRef(&message);
1828        let json = serde_json::to_value(&wrapper).unwrap();
1829
1830        assert_eq!(json["role"], "user");
1831        let content_arr = json["content"].as_array().unwrap();
1832        assert_eq!(content_arr.len(), 2);
1833
1834        // First element: text
1835        assert_eq!(content_arr[0]["type"], "text");
1836        assert_eq!(content_arr[0]["text"], "What's in this image?");
1837
1838        // Second element: image_url with nested object
1839        assert_eq!(content_arr[1]["type"], "image_url");
1840        assert_eq!(content_arr[1]["image_url"]["url"], "https://example.com/image.png");
1841    }
1842
1843    #[test]
1844    fn test_chat_single_text_message_serialization() {
1845        use crate::common::message::Message;
1846        use crate::common::role::Role;
1847
1848        let message = Message::from_string(Role::User, "Hello!");
1849        let wrapper = ChatMessageRef(&message);
1850        let json = serde_json::to_value(&wrapper).unwrap();
1851
1852        assert_eq!(json["role"], "user");
1853        // Single text content should be serialized as a plain string, not an array
1854        assert_eq!(json["content"], "Hello!");
1855    }
1856
1857    #[test]
1858    fn test_chat_body_messages_serialization() {
1859        use crate::common::message::{Content, Message};
1860        use crate::common::role::Role;
1861
1862        let messages = vec![
1863            Message::from_string(Role::System, "You are a helpful assistant."),
1864            Message::from_message_array(
1865                Role::User,
1866                vec![Content::from_text("Describe this image"), Content::from_image_url("https://example.com/photo.jpg")],
1867            ),
1868        ];
1869
1870        let body = Body { model: ChatModel::Gpt4oMini, messages, ..Default::default() };
1871
1872        let json = serde_json::to_value(&body).unwrap();
1873        let msgs = json["messages"].as_array().unwrap();
1874
1875        // System message: plain string content
1876        assert_eq!(msgs[0]["role"], "system");
1877        assert_eq!(msgs[0]["content"], "You are a helpful assistant.");
1878
1879        // User multimodal message: array content with Chat API types
1880        assert_eq!(msgs[1]["role"], "user");
1881        let content_arr = msgs[1]["content"].as_array().unwrap();
1882        assert_eq!(content_arr[0]["type"], "text");
1883        assert_eq!(content_arr[1]["type"], "image_url");
1884        assert_eq!(content_arr[1]["image_url"]["url"], "https://example.com/photo.jpg");
1885    }
1886
1887    #[test]
1888    fn test_safety_identifier() {
1889        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt4oMini);
1890        chat.safety_identifier("user_abc123");
1891        assert_eq!(chat.request_body.safety_identifier, Some("user_abc123".to_string()));
1892
1893        // Verify serialization
1894        let json = serde_json::to_value(&chat.request_body).unwrap();
1895        assert_eq!(json["safety_identifier"], "user_abc123");
1896    }
1897
1898    #[test]
1899    fn test_safety_identifier_not_serialized_when_none() {
1900        let chat = ChatCompletion::test_new_with_model(ChatModel::Gpt4oMini);
1901        let json = serde_json::to_value(&chat.request_body).unwrap();
1902        assert!(json.get("safety_identifier").is_none());
1903    }
1904}