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    #[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}