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}