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`](Self::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:** Search models (`gpt-5-search-api`, `gpt-4o-search-preview`, ...)
1016    /// only support n=1; for them, values other than 1 are ignored with a
1017    /// warning. Reasoning models (GPT-5, GPT-6, o-series) accept any `n`.
1018    ///
1019    /// # Arguments
1020    ///
1021    /// * `n` - Number of responses to generate
1022    ///
1023    /// # Returns
1024    ///
1025    /// A mutable reference to self for method chaining
1026    pub fn n(&mut self, n: u32) -> &mut Self {
1027        let support = self.request_body.model.parameter_support();
1028        if !support.n_multiple && n != 1 {
1029            tracing::warn!("Model '{}' only supports n=1. Ignoring n={}.", self.request_body.model, n);
1030            return self;
1031        }
1032        self.request_body.n = Some(n);
1033        self
1034    }
1035
1036    /// Sets the available modalities for the response
1037    ///
1038    /// # Arguments
1039    ///
1040    /// * `modalities` - List of modalities (e.g., `["text", "audio"]`)
1041    ///
1042    /// # Returns
1043    ///
1044    /// A mutable reference to self for method chaining
1045    pub fn modalities<T: AsRef<str>>(&mut self, modalities: Vec<T>) -> &mut Self {
1046        self.request_body.modalities = Option::from(modalities.into_iter().map(|m| m.as_ref().to_string()).collect::<Vec<String>>());
1047        self
1048    }
1049
1050    /// Sets the presence penalty
1051    ///
1052    /// A parameter that controls the tendency to include new content in the document.
1053    /// Positive values encourage talking about new topics, negative values encourage
1054    /// staying on existing topics.
1055    ///
1056    /// **Note:** Reasoning models (GPT-5, o-series) only support presence_penalty=0.
1057    /// For these models, non-zero values will be ignored with a warning.
1058    ///
1059    /// # Arguments
1060    ///
1061    /// * `presence_penalty` - Presence penalty value (range: -2.0 to 2.0)
1062    ///
1063    /// # Returns
1064    ///
1065    /// A mutable reference to self for method chaining
1066    pub fn presence_penalty(&mut self, presence_penalty: f32) -> &mut Self {
1067        let support = self.request_body.model.parameter_support();
1068        match support.presence_penalty {
1069            ParameterRestriction::FixedValue(fixed) => {
1070                if (presence_penalty as f64 - fixed).abs() > f64::EPSILON {
1071                    tracing::warn!(
1072                        "Model '{}' only supports presence_penalty={}. Ignoring presence_penalty={}.",
1073                        self.request_body.model,
1074                        fixed,
1075                        presence_penalty
1076                    );
1077                    return self;
1078                }
1079            }
1080            ParameterRestriction::NotSupported => {
1081                tracing::warn!("Model '{}' does not support presence_penalty parameter. Ignoring.", self.request_body.model);
1082                return self;
1083            }
1084            ParameterRestriction::Any => {}
1085        }
1086        self.request_body.presence_penalty = Some(presence_penalty);
1087        self
1088    }
1089
1090    /// Sets the temperature parameter to control response randomness
1091    ///
1092    /// Higher values (e.g., 1.0) produce more creative and diverse outputs,
1093    /// while lower values (e.g., 0.2) produce more deterministic and consistent outputs.
1094    ///
1095    /// **Note:** Reasoning models (GPT-5, o-series) only support temperature=1.0.
1096    /// For these models, other values will be ignored with a warning.
1097    ///
1098    /// # Arguments
1099    ///
1100    /// * `temperature` - Temperature parameter (range: 0.0 to 2.0)
1101    ///
1102    /// # Returns
1103    ///
1104    /// A mutable reference to self for method chaining
1105    pub fn temperature(&mut self, temperature: f32) -> &mut Self {
1106        let support = self.request_body.model.parameter_support();
1107        match support.temperature {
1108            ParameterRestriction::FixedValue(fixed) => {
1109                if (temperature as f64 - fixed).abs() > f64::EPSILON {
1110                    tracing::warn!("Model '{}' only supports temperature={}. Ignoring temperature={}.", self.request_body.model, fixed, temperature);
1111                    return self;
1112                }
1113            }
1114            ParameterRestriction::NotSupported => {
1115                tracing::warn!("Model '{}' does not support temperature parameter. Ignoring.", self.request_body.model);
1116                return self;
1117            }
1118            ParameterRestriction::Any => {}
1119        }
1120        self.request_body.temperature = Some(temperature);
1121        self
1122    }
1123
1124    /// Sets structured output using JSON schema
1125    ///
1126    /// Enables receiving responses in a structured JSON format according to the
1127    /// specified JSON schema.
1128    ///
1129    /// # Arguments
1130    ///
1131    /// * `json_schema` - JSON schema defining the response structure
1132    ///
1133    /// # Returns
1134    ///
1135    /// A mutable reference to self for method chaining
1136    pub fn json_schema(&mut self, json_schema: Schema) -> &mut Self {
1137        self.request_body.response_format = Option::from(Format::new(String::from("json_schema"), json_schema));
1138        self
1139    }
1140
1141    /// Sets the tools that can be called by the model
1142    ///
1143    /// Enables function calling by providing a list of tools that the model can choose to call.
1144    /// When tools are provided, the model may generate tool calls instead of or in addition to
1145    /// regular text responses.
1146    ///
1147    /// # Arguments
1148    ///
1149    /// * `tools` - Vector of tools available for the model to use
1150    ///
1151    /// # Returns
1152    ///
1153    /// A mutable reference to self for method chaining
1154    pub fn tools(&mut self, tools: Vec<Tool>) -> &mut Self {
1155        self.request_body.tools = Option::from(tools);
1156        self
1157    }
1158
1159    /// Sets the safety identifier for end-user tracking
1160    ///
1161    /// A stable identifier used to help OpenAI detect users of your application
1162    /// that may be violating usage policies. This enables per-user safety
1163    /// monitoring and abuse detection.
1164    ///
1165    /// # Arguments
1166    ///
1167    /// * `safety_id` - A unique, stable identifier for the end user
1168    ///   (recommended: hash of email or internal user ID)
1169    ///
1170    /// # Returns
1171    ///
1172    /// A mutable reference to self for method chaining
1173    ///
1174    /// # Examples
1175    ///
1176    /// ```rust
1177    /// use openai_tools::chat::request::ChatCompletion;
1178    ///
1179    /// let mut chat = ChatCompletion::new();
1180    /// chat.safety_identifier("user_abc123");
1181    /// ```
1182    pub fn safety_identifier<T: AsRef<str>>(&mut self, safety_id: T) -> &mut Self {
1183        self.request_body.safety_identifier = Some(safety_id.as_ref().to_string());
1184        self
1185    }
1186
1187    /// Sets a unique identifier representing your end-user, which can help to monitor and detect abuse
1188    ///
1189    /// A unique identifier representing your end-user, which can help to monitor and detect abuse.
1190    ///
1191    /// # Arguments
1192    ///
1193    /// * `user` - A identifier representing your end-user.
1194    ///
1195    /// # Returns
1196    ///
1197    /// A mutable reference to self for method chaining
1198    ///
1199    /// # Examples
1200    ///
1201    /// ```rust
1202    /// use openai_tools::chat::request::ChatCompletion;
1203    ///
1204    /// let mut chat = ChatCompletion::new();
1205    /// chat.user("abc123");
1206    /// ```
1207    pub fn user<T: AsRef<str>>(&mut self, user: T) -> &mut Self {
1208        self.request_body.user = Some(user.as_ref().to_string());
1209        self
1210    }
1211
1212    /// Gets the current message history
1213    ///
1214    /// # Returns
1215    ///
1216    /// A vector containing the message history
1217    pub fn get_message_history(&self) -> Vec<Message> {
1218        self.request_body.messages.clone()
1219    }
1220
1221    /// Checks if the model is a reasoning model that doesn't support custom temperature
1222    ///
1223    /// Reasoning models (o1, o3, o4 series) only support the default temperature value of 1.0.
1224    /// This method checks if the current model is one of these reasoning models.
1225    ///
1226    /// # Returns
1227    ///
1228    /// `true` if the model is a reasoning model, `false` otherwise
1229    ///
1230    /// # Supported Reasoning Models
1231    ///
1232    /// - `o1`, `o1-pro`, and variants
1233    /// - `o3`, `o3-mini`, and variants
1234    /// - `o4-mini` and variants
1235    fn is_reasoning_model(&self) -> bool {
1236        self.request_body.model.is_reasoning_model()
1237    }
1238
1239    /// Sends the chat completion request to OpenAI API
1240    ///
1241    /// This method validates the request parameters, constructs the HTTP request,
1242    /// and sends it to the OpenAI Chat Completions endpoint.
1243    ///
1244    /// # Returns
1245    ///
1246    /// A `Result` containing the API response on success, or an error on failure.
1247    ///
1248    /// # Errors
1249    ///
1250    /// Returns an error if:
1251    /// - API key is not set
1252    /// - Model ID is not set
1253    /// - Messages are empty
1254    /// - Network request fails
1255    /// - Response parsing fails
1256    ///
1257    /// # Parameter Validation
1258    ///
1259    /// For reasoning models (GPT-5, o-series), certain parameters have restrictions:
1260    /// - `temperature`: only 1.0 supported
1261    /// - `frequency_penalty`: only 0 supported
1262    /// - `presence_penalty`: only 0 supported
1263    /// - `logprobs`, `top_logprobs`, `logit_bias`: not supported
1264    /// - `n`: only 1 supported
1265    ///
1266    /// **Validation occurs at two points:**
1267    /// 1. At setter time (when using `with_model()` constructor) - immediate warning
1268    /// 2. At API call time (fallback) - for cases where model is changed after setting params
1269    ///
1270    /// Unsupported parameter values are ignored with a warning and the request proceeds.
1271    ///
1272    /// # Example
1273    ///
1274    /// ```rust,no_run
1275    /// use openai_tools::chat::request::ChatCompletion;
1276    /// use openai_tools::common::message::Message;
1277    /// use openai_tools::common::role::Role;
1278    ///
1279    /// # #[tokio::main]
1280    /// # async fn main() -> Result<(), Box<dyn std::error::Error>>
1281    /// # {
1282    /// let mut chat = ChatCompletion::new();
1283    /// let messages = vec![Message::from_string(Role::User, "Hello!")];
1284    ///
1285    /// let response = chat
1286    ///     .model_id("gpt-4o-mini")
1287    ///     .messages(messages)
1288    ///     .temperature(1.0)
1289    ///     .chat()
1290    ///     .await?;
1291    ///
1292    /// println!("{}", response.choices[0].message.content.as_ref().unwrap().text.as_ref().unwrap());
1293    /// # Ok::<(), Box<dyn std::error::Error>>(())
1294    /// # }
1295    /// ```
1296    pub async fn chat(&mut self) -> Result<Response> {
1297        // Validate that messages are set
1298        if self.request_body.messages.is_empty() {
1299            return Err(OpenAIToolError::Error("Messages are not set.".into()));
1300        }
1301
1302        // Handle reasoning models that don't support certain parameters
1303        // See: https://platform.openai.com/docs/guides/reasoning
1304        if self.is_reasoning_model() {
1305            let model = &self.request_body.model;
1306
1307            // Temperature: only default (1.0) is supported
1308            if let Some(temp) = self.request_body.temperature {
1309                if (temp - 1.0).abs() > f32::EPSILON {
1310                    tracing::warn!(
1311                        "Reasoning model '{}' does not support custom temperature. \
1312                         Ignoring temperature={} and using default (1.0).",
1313                        model,
1314                        temp
1315                    );
1316                    self.request_body.temperature = None;
1317                }
1318            }
1319
1320            // Frequency penalty: only 0 is supported
1321            if let Some(fp) = self.request_body.frequency_penalty {
1322                if fp.abs() > f32::EPSILON {
1323                    tracing::warn!(
1324                        "Reasoning model '{}' does not support frequency_penalty. \
1325                         Ignoring frequency_penalty={} and using default (0).",
1326                        model,
1327                        fp
1328                    );
1329                    self.request_body.frequency_penalty = None;
1330                }
1331            }
1332
1333            // Presence penalty: only 0 is supported
1334            if let Some(pp) = self.request_body.presence_penalty {
1335                if pp.abs() > f32::EPSILON {
1336                    tracing::warn!(
1337                        "Reasoning model '{}' does not support presence_penalty. \
1338                         Ignoring presence_penalty={} and using default (0).",
1339                        model,
1340                        pp
1341                    );
1342                    self.request_body.presence_penalty = None;
1343                }
1344            }
1345
1346            // Logprobs: not supported
1347            if self.request_body.logprobs.is_some() {
1348                tracing::warn!("Reasoning model '{}' does not support logprobs. Ignoring logprobs parameter.", model);
1349                self.request_body.logprobs = None;
1350            }
1351
1352            // Top logprobs: not supported
1353            if self.request_body.top_logprobs.is_some() {
1354                tracing::warn!("Reasoning model '{}' does not support top_logprobs. Ignoring top_logprobs parameter.", model);
1355                self.request_body.top_logprobs = None;
1356            }
1357
1358            // Logit bias: not supported
1359            if self.request_body.logit_bias.is_some() {
1360                tracing::warn!("Reasoning model '{}' does not support logit_bias. Ignoring logit_bias parameter.", model);
1361                self.request_body.logit_bias = None;
1362            }
1363        }
1364
1365        // N: reasoning models accept any value; search models only support 1.
1366        // Checked here as well as in `n()` because the model may change after `n()` was called.
1367        if let Some(n) = self.request_body.n {
1368            if n != 1 && !self.request_body.model.parameter_support().n_multiple {
1369                tracing::warn!("Model '{}' only supports n=1. Ignoring n={}.", self.request_body.model, n);
1370                self.request_body.n = None;
1371            }
1372        }
1373
1374        let body = serde_json::to_string(&self.request_body)?;
1375
1376        let client = create_http_client(self.timeout)?;
1377        let mut headers = request::header::HeaderMap::new();
1378        headers.insert("Content-Type", request::header::HeaderValue::from_static("application/json"));
1379        headers.insert("User-Agent", request::header::HeaderValue::from_static("openai-tools-rust"));
1380
1381        // Apply provider-specific authentication headers
1382        self.auth.apply_headers(&mut headers)?;
1383
1384        if cfg!(debug_assertions) {
1385            // Replace API key with a placeholder in debug mode
1386            let body_for_debug = serde_json::to_string_pretty(&self.request_body).unwrap().replace(self.auth.api_key(), "*************");
1387            tracing::info!("Request body: {}", body_for_debug);
1388        }
1389
1390        // Get the endpoint URL from the auth provider
1391        let endpoint = self.auth.endpoint(CHAT_COMPLETIONS_PATH);
1392
1393        let response = client.post(&endpoint).headers(headers).body(body).send().await.map_err(OpenAIToolError::RequestError)?;
1394        let status = response.status();
1395        let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
1396
1397        if cfg!(debug_assertions) {
1398            tracing::info!("Response content: {}", content);
1399        }
1400
1401        if !status.is_success() {
1402            if let Ok(error_resp) = serde_json::from_str::<ErrorResponse>(&content) {
1403                return Err(OpenAIToolError::Error(error_resp.error.message.unwrap_or_default()));
1404            }
1405            return Err(OpenAIToolError::Error(format!("API error ({}): {}", status, content)));
1406        }
1407
1408        serde_json::from_str::<Response>(&content).map_err(OpenAIToolError::SerdeJsonError)
1409    }
1410
1411    /// Creates a test-only ChatCompletion instance without authentication
1412    ///
1413    /// This is only available in test mode and bypasses API key requirements.
1414    #[cfg(test)]
1415    pub(crate) fn test_new_with_model(model: ChatModel) -> Self {
1416        use crate::common::auth::OpenAIAuth;
1417        Self { auth: AuthProvider::OpenAI(OpenAIAuth::new("test-key")), request_body: Body { model, ..Default::default() }, timeout: None }
1418    }
1419}
1420
1421#[cfg(test)]
1422mod tests {
1423    use super::*;
1424    use crate::common::models::ChatModel;
1425    use std::collections::HashMap;
1426
1427    // =============================================================================
1428    // Standard Model Parameter Tests
1429    // =============================================================================
1430
1431    #[test]
1432    fn test_standard_model_accepts_all_parameters() {
1433        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt4oMini);
1434
1435        // Standard models should accept all parameters
1436        chat.temperature(0.7);
1437        chat.frequency_penalty(0.5);
1438        chat.presence_penalty(0.5);
1439        chat.logprobs(true);
1440        chat.top_logprobs(5);
1441        chat.n(3);
1442
1443        let logit_bias: HashMap<&str, i32> = [("1234", 10)].iter().cloned().collect();
1444        chat.logit_bias(logit_bias);
1445
1446        assert_eq!(chat.request_body.temperature, Some(0.7));
1447        assert_eq!(chat.request_body.frequency_penalty, Some(0.5));
1448        assert_eq!(chat.request_body.presence_penalty, Some(0.5));
1449        assert_eq!(chat.request_body.logprobs, Some(true));
1450        assert_eq!(chat.request_body.top_logprobs, Some(5));
1451        assert_eq!(chat.request_body.n, Some(3));
1452        assert!(chat.request_body.logit_bias.is_some());
1453    }
1454
1455    #[test]
1456    fn test_gpt4o_accepts_all_parameters() {
1457        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt4o);
1458
1459        chat.temperature(0.3);
1460        chat.frequency_penalty(-1.0);
1461        chat.presence_penalty(1.5);
1462
1463        assert_eq!(chat.request_body.temperature, Some(0.3));
1464        assert_eq!(chat.request_body.frequency_penalty, Some(-1.0));
1465        assert_eq!(chat.request_body.presence_penalty, Some(1.5));
1466    }
1467
1468    #[test]
1469    fn test_gpt4_1_accepts_all_parameters() {
1470        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt4_1);
1471
1472        chat.temperature(1.5);
1473        chat.frequency_penalty(0.8);
1474        chat.n(2);
1475
1476        assert_eq!(chat.request_body.temperature, Some(1.5));
1477        assert_eq!(chat.request_body.frequency_penalty, Some(0.8));
1478        assert_eq!(chat.request_body.n, Some(2));
1479    }
1480
1481    // =============================================================================
1482    // O-Series Reasoning Model Tests
1483    // =============================================================================
1484
1485    #[test]
1486    fn test_o1_ignores_non_default_temperature() {
1487        let mut chat = ChatCompletion::test_new_with_model(ChatModel::O1);
1488
1489        // Non-default temperature should be ignored
1490        chat.temperature(0.5);
1491        assert_eq!(chat.request_body.temperature, None);
1492
1493        // Default temperature (1.0) should be accepted
1494        chat.temperature(1.0);
1495        assert_eq!(chat.request_body.temperature, Some(1.0));
1496    }
1497
1498    #[test]
1499    fn test_o3_mini_ignores_non_default_temperature() {
1500        let mut chat = ChatCompletion::test_new_with_model(ChatModel::O3Mini);
1501
1502        chat.temperature(0.3);
1503        assert_eq!(chat.request_body.temperature, None);
1504    }
1505
1506    #[test]
1507    fn test_o4_mini_ignores_non_default_temperature() {
1508        let mut chat = ChatCompletion::test_new_with_model(ChatModel::O4Mini);
1509
1510        chat.temperature(0.7);
1511        assert_eq!(chat.request_body.temperature, None);
1512    }
1513
1514    #[test]
1515    fn test_o1_ignores_frequency_penalty() {
1516        let mut chat = ChatCompletion::test_new_with_model(ChatModel::O1);
1517
1518        // Non-zero frequency_penalty should be ignored
1519        chat.frequency_penalty(0.5);
1520        assert_eq!(chat.request_body.frequency_penalty, None);
1521
1522        // Zero value should be accepted
1523        chat.frequency_penalty(0.0);
1524        assert_eq!(chat.request_body.frequency_penalty, Some(0.0));
1525    }
1526
1527    #[test]
1528    fn test_o3_ignores_presence_penalty() {
1529        let mut chat = ChatCompletion::test_new_with_model(ChatModel::O3);
1530
1531        // Non-zero presence_penalty should be ignored
1532        chat.presence_penalty(0.5);
1533        assert_eq!(chat.request_body.presence_penalty, None);
1534
1535        // Zero value should be accepted
1536        chat.presence_penalty(0.0);
1537        assert_eq!(chat.request_body.presence_penalty, Some(0.0));
1538    }
1539
1540    #[test]
1541    fn test_o1_ignores_logprobs() {
1542        let mut chat = ChatCompletion::test_new_with_model(ChatModel::O1);
1543
1544        chat.logprobs(true);
1545        assert_eq!(chat.request_body.logprobs, None);
1546    }
1547
1548    #[test]
1549    fn test_o3_mini_ignores_top_logprobs() {
1550        let mut chat = ChatCompletion::test_new_with_model(ChatModel::O3Mini);
1551
1552        chat.top_logprobs(5);
1553        assert_eq!(chat.request_body.top_logprobs, None);
1554    }
1555
1556    #[test]
1557    fn test_o1_ignores_logit_bias() {
1558        let mut chat = ChatCompletion::test_new_with_model(ChatModel::O1);
1559
1560        let logit_bias: HashMap<&str, i32> = [("1234", 10)].iter().cloned().collect();
1561        chat.logit_bias(logit_bias);
1562        assert_eq!(chat.request_body.logit_bias, None);
1563    }
1564
1565    #[test]
1566    fn test_o1_accepts_n_greater_than_1() {
1567        let mut chat = ChatCompletion::test_new_with_model(ChatModel::O1);
1568
1569        // The API returns n choices for reasoning models, so n > 1 is kept
1570        chat.n(3);
1571        assert_eq!(chat.request_body.n, Some(3));
1572    }
1573
1574    #[test]
1575    fn test_search_model_ignores_n_greater_than_1() {
1576        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt5SearchApi);
1577
1578        // Search models reject n > 1 ("Model incompatible request argument supplied: n")
1579        chat.n(3);
1580        assert_eq!(chat.request_body.n, None);
1581
1582        // n = 1 should be accepted
1583        chat.n(1);
1584        assert_eq!(chat.request_body.n, Some(1));
1585    }
1586
1587    // =============================================================================
1588    // GPT-5 Series Reasoning Model Tests
1589    // =============================================================================
1590
1591    #[test]
1592    fn test_gpt5_2_ignores_non_default_temperature() {
1593        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt5_2);
1594
1595        chat.temperature(0.5);
1596        assert_eq!(chat.request_body.temperature, None);
1597
1598        chat.temperature(1.0);
1599        assert_eq!(chat.request_body.temperature, Some(1.0));
1600    }
1601
1602    #[test]
1603    fn test_gpt5_1_ignores_non_default_temperature() {
1604        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt5_1);
1605
1606        chat.temperature(0.3);
1607        assert_eq!(chat.request_body.temperature, None);
1608    }
1609
1610    #[test]
1611    fn test_gpt5_mini_ignores_frequency_penalty() {
1612        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt5Mini);
1613
1614        chat.frequency_penalty(0.5);
1615        assert_eq!(chat.request_body.frequency_penalty, None);
1616    }
1617
1618    #[test]
1619    fn test_gpt5_2_pro_ignores_presence_penalty() {
1620        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt5_2Pro);
1621
1622        chat.presence_penalty(0.8);
1623        assert_eq!(chat.request_body.presence_penalty, None);
1624    }
1625
1626    #[test]
1627    fn test_gpt5_1_codex_max_ignores_logprobs() {
1628        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt5_1CodexMax);
1629
1630        chat.logprobs(true);
1631        assert_eq!(chat.request_body.logprobs, None);
1632    }
1633
1634    #[test]
1635    /// `gpt-5.2-chat-latest` points at the non-reasoning Instant snapshot, so
1636    /// it accepts the standard parameters that the reasoning models reject.
1637    fn test_gpt5_2_chat_latest_honors_n_greater_than_1() {
1638        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt5_2ChatLatest);
1639
1640        chat.n(5);
1641        assert_eq!(chat.request_body.n, Some(5));
1642
1643        chat.temperature(0.3);
1644        assert_eq!(chat.request_body.temperature, Some(0.3));
1645    }
1646
1647    // =============================================================================
1648    // Multiple Restricted Parameters Tests
1649    // =============================================================================
1650
1651    #[test]
1652    fn test_o1_ignores_all_restricted_parameters_at_once() {
1653        let mut chat = ChatCompletion::test_new_with_model(ChatModel::O1);
1654
1655        // Set all restricted parameters
1656        chat.temperature(0.5);
1657        chat.frequency_penalty(0.5);
1658        chat.presence_penalty(0.5);
1659        chat.logprobs(true);
1660        chat.top_logprobs(5);
1661        chat.n(3);
1662
1663        let logit_bias: HashMap<&str, i32> = [("1234", 10)].iter().cloned().collect();
1664        chat.logit_bias(logit_bias);
1665
1666        // All restricted parameters should be ignored; n is not restricted
1667        assert_eq!(chat.request_body.temperature, None);
1668        assert_eq!(chat.request_body.frequency_penalty, None);
1669        assert_eq!(chat.request_body.presence_penalty, None);
1670        assert_eq!(chat.request_body.logprobs, None);
1671        assert_eq!(chat.request_body.top_logprobs, None);
1672        assert_eq!(chat.request_body.n, Some(3));
1673        assert_eq!(chat.request_body.logit_bias, None);
1674    }
1675
1676    #[test]
1677    fn test_gpt5_2_ignores_all_restricted_parameters_at_once() {
1678        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt5_2);
1679
1680        chat.temperature(0.5);
1681        chat.frequency_penalty(0.5);
1682        chat.presence_penalty(0.5);
1683        chat.logprobs(true);
1684        chat.top_logprobs(5);
1685        chat.n(3);
1686
1687        let logit_bias: HashMap<&str, i32> = [("1234", 10)].iter().cloned().collect();
1688        chat.logit_bias(logit_bias);
1689
1690        assert_eq!(chat.request_body.temperature, None);
1691        assert_eq!(chat.request_body.frequency_penalty, None);
1692        assert_eq!(chat.request_body.presence_penalty, None);
1693        assert_eq!(chat.request_body.logprobs, None);
1694        assert_eq!(chat.request_body.top_logprobs, None);
1695        assert_eq!(chat.request_body.n, Some(3));
1696        assert_eq!(chat.request_body.logit_bias, None);
1697    }
1698
1699    // =============================================================================
1700    // Custom Model Tests
1701    // =============================================================================
1702
1703    #[test]
1704    fn test_custom_gpt5_model_detected_as_reasoning() {
1705        let mut chat = ChatCompletion::test_new_with_model(ChatModel::custom("gpt-5.3-preview"));
1706
1707        // Custom GPT-5 models should be treated as reasoning models
1708        chat.temperature(0.5);
1709        assert_eq!(chat.request_body.temperature, None);
1710    }
1711
1712    #[test]
1713    fn test_custom_o1_model_detected_as_reasoning() {
1714        let mut chat = ChatCompletion::test_new_with_model(ChatModel::custom("o1-pro-2025-01-15"));
1715
1716        // Custom o1-series models should be treated as reasoning models
1717        chat.temperature(0.5);
1718        assert_eq!(chat.request_body.temperature, None);
1719    }
1720
1721    #[test]
1722    fn test_custom_o3_model_detected_as_reasoning() {
1723        let mut chat = ChatCompletion::test_new_with_model(ChatModel::custom("o3-high"));
1724
1725        // Custom o3-series models should be treated as reasoning models
1726        chat.temperature(0.5);
1727        assert_eq!(chat.request_body.temperature, None);
1728    }
1729
1730    #[test]
1731    fn test_custom_o4_model_detected_as_reasoning() {
1732        let mut chat = ChatCompletion::test_new_with_model(ChatModel::custom("o4-mini-preview"));
1733
1734        // Custom o4-series models should be treated as reasoning models
1735        chat.temperature(0.5);
1736        assert_eq!(chat.request_body.temperature, None);
1737    }
1738
1739    #[test]
1740    fn test_custom_standard_model_accepts_all_parameters() {
1741        let mut chat = ChatCompletion::test_new_with_model(ChatModel::custom("ft:gpt-4o-mini:org::123"));
1742
1743        // Fine-tuned standard models should accept all parameters
1744        chat.temperature(0.7);
1745        chat.frequency_penalty(0.5);
1746        chat.n(2);
1747
1748        assert_eq!(chat.request_body.temperature, Some(0.7));
1749        assert_eq!(chat.request_body.frequency_penalty, Some(0.5));
1750        assert_eq!(chat.request_body.n, Some(2));
1751    }
1752
1753    // =============================================================================
1754    // Parameter Boundary Tests
1755    // =============================================================================
1756
1757    #[test]
1758    fn test_temperature_boundary_values() {
1759        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt4oMini);
1760
1761        // Minimum value
1762        chat.temperature(0.0);
1763        assert_eq!(chat.request_body.temperature, Some(0.0));
1764
1765        // Maximum value
1766        chat.temperature(2.0);
1767        assert_eq!(chat.request_body.temperature, Some(2.0));
1768    }
1769
1770    #[test]
1771    fn test_frequency_penalty_boundary_values() {
1772        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt4oMini);
1773
1774        // Minimum value
1775        chat.frequency_penalty(-2.0);
1776        assert_eq!(chat.request_body.frequency_penalty, Some(-2.0));
1777
1778        // Maximum value
1779        chat.frequency_penalty(2.0);
1780        assert_eq!(chat.request_body.frequency_penalty, Some(2.0));
1781    }
1782
1783    #[test]
1784    fn test_presence_penalty_boundary_values() {
1785        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt4oMini);
1786
1787        // Minimum value
1788        chat.presence_penalty(-2.0);
1789        assert_eq!(chat.request_body.presence_penalty, Some(-2.0));
1790
1791        // Maximum value
1792        chat.presence_penalty(2.0);
1793        assert_eq!(chat.request_body.presence_penalty, Some(2.0));
1794    }
1795
1796    // =============================================================================
1797    // Model-Specific Unrestricted Parameters Tests
1798    // =============================================================================
1799
1800    #[test]
1801    fn test_max_completion_tokens_accepted_by_all_models() {
1802        // Standard model
1803        let mut chat_standard = ChatCompletion::test_new_with_model(ChatModel::Gpt4oMini);
1804        chat_standard.max_completion_tokens(1000);
1805        assert_eq!(chat_standard.request_body.max_completion_tokens, Some(1000));
1806
1807        // Reasoning model
1808        let mut chat_reasoning = ChatCompletion::test_new_with_model(ChatModel::O1);
1809        chat_reasoning.max_completion_tokens(2000);
1810        assert_eq!(chat_reasoning.request_body.max_completion_tokens, Some(2000));
1811
1812        // GPT-5 model
1813        let mut chat_gpt5 = ChatCompletion::test_new_with_model(ChatModel::Gpt5_2);
1814        chat_gpt5.max_completion_tokens(3000);
1815        assert_eq!(chat_gpt5.request_body.max_completion_tokens, Some(3000));
1816    }
1817
1818    #[test]
1819    fn test_store_accepted_by_all_models() {
1820        let mut chat_standard = ChatCompletion::test_new_with_model(ChatModel::Gpt4oMini);
1821        chat_standard.store(true);
1822        assert_eq!(chat_standard.request_body.store, Some(true));
1823
1824        let mut chat_reasoning = ChatCompletion::test_new_with_model(ChatModel::O1);
1825        chat_reasoning.store(false);
1826        assert_eq!(chat_reasoning.request_body.store, Some(false));
1827    }
1828
1829    // =============================================================================
1830    // Chat API Content Serialization Tests
1831    // =============================================================================
1832
1833    #[test]
1834    fn test_chat_text_content_serialization() {
1835        use crate::common::message::Content;
1836
1837        let content = Content::from_text("Hello, world!");
1838        let wrapper = ChatContentRef(&content);
1839        let json = serde_json::to_value(&wrapper).unwrap();
1840
1841        assert_eq!(json["type"], "text");
1842        assert_eq!(json["text"], "Hello, world!");
1843        assert!(json.get("image_url").is_none());
1844    }
1845
1846    #[test]
1847    fn test_chat_image_content_serialization() {
1848        use crate::common::message::Content;
1849
1850        let content = Content::from_image_url("https://example.com/image.png");
1851        let wrapper = ChatContentRef(&content);
1852        let json = serde_json::to_value(&wrapper).unwrap();
1853
1854        assert_eq!(json["type"], "image_url");
1855        assert_eq!(json["image_url"]["url"], "https://example.com/image.png");
1856    }
1857
1858    #[test]
1859    fn test_chat_multimodal_message_serialization() {
1860        use crate::common::message::{Content, Message};
1861        use crate::common::role::Role;
1862
1863        let contents = vec![Content::from_text("What's in this image?"), Content::from_image_url("https://example.com/image.png")];
1864        let message = Message::from_message_array(Role::User, contents);
1865        let wrapper = ChatMessageRef(&message);
1866        let json = serde_json::to_value(&wrapper).unwrap();
1867
1868        assert_eq!(json["role"], "user");
1869        let content_arr = json["content"].as_array().unwrap();
1870        assert_eq!(content_arr.len(), 2);
1871
1872        // First element: text
1873        assert_eq!(content_arr[0]["type"], "text");
1874        assert_eq!(content_arr[0]["text"], "What's in this image?");
1875
1876        // Second element: image_url with nested object
1877        assert_eq!(content_arr[1]["type"], "image_url");
1878        assert_eq!(content_arr[1]["image_url"]["url"], "https://example.com/image.png");
1879    }
1880
1881    #[test]
1882    fn test_chat_single_text_message_serialization() {
1883        use crate::common::message::Message;
1884        use crate::common::role::Role;
1885
1886        let message = Message::from_string(Role::User, "Hello!");
1887        let wrapper = ChatMessageRef(&message);
1888        let json = serde_json::to_value(&wrapper).unwrap();
1889
1890        assert_eq!(json["role"], "user");
1891        // Single text content should be serialized as a plain string, not an array
1892        assert_eq!(json["content"], "Hello!");
1893    }
1894
1895    #[test]
1896    fn test_chat_body_messages_serialization() {
1897        use crate::common::message::{Content, Message};
1898        use crate::common::role::Role;
1899
1900        let messages = vec![
1901            Message::from_string(Role::System, "You are a helpful assistant."),
1902            Message::from_message_array(
1903                Role::User,
1904                vec![Content::from_text("Describe this image"), Content::from_image_url("https://example.com/photo.jpg")],
1905            ),
1906        ];
1907
1908        let body = Body { model: ChatModel::Gpt4oMini, messages, ..Default::default() };
1909
1910        let json = serde_json::to_value(&body).unwrap();
1911        let msgs = json["messages"].as_array().unwrap();
1912
1913        // System message: plain string content
1914        assert_eq!(msgs[0]["role"], "system");
1915        assert_eq!(msgs[0]["content"], "You are a helpful assistant.");
1916
1917        // User multimodal message: array content with Chat API types
1918        assert_eq!(msgs[1]["role"], "user");
1919        let content_arr = msgs[1]["content"].as_array().unwrap();
1920        assert_eq!(content_arr[0]["type"], "text");
1921        assert_eq!(content_arr[1]["type"], "image_url");
1922        assert_eq!(content_arr[1]["image_url"]["url"], "https://example.com/photo.jpg");
1923    }
1924
1925    #[test]
1926    fn test_safety_identifier() {
1927        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt4oMini);
1928        chat.safety_identifier("user_abc123");
1929        assert_eq!(chat.request_body.safety_identifier, Some("user_abc123".to_string()));
1930
1931        // Verify serialization
1932        let json = serde_json::to_value(&chat.request_body).unwrap();
1933        assert_eq!(json["safety_identifier"], "user_abc123");
1934    }
1935
1936    #[test]
1937    fn test_user() {
1938        let mut chat = ChatCompletion::test_new_with_model(ChatModel::Gpt4oMini);
1939        chat.user("abc123");
1940        assert_eq!(chat.request_body.user, Some("abc123".to_string()));
1941
1942        // Verify serialization
1943        let json = serde_json::to_value(&chat.request_body).unwrap();
1944        assert_eq!(json["user"], "abc123");
1945    }
1946
1947    #[test]
1948    fn test_safety_identifier_not_serialized_when_none() {
1949        let chat = ChatCompletion::test_new_with_model(ChatModel::Gpt4oMini);
1950        let json = serde_json::to_value(&chat.request_body).unwrap();
1951        assert!(json.get("safety_identifier").is_none());
1952    }
1953}