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