Skip to main content

aither_core/llm/
model.rs

1//! AI language model configuration and profiling types.
2//!
3//! This module provides types for configuring AI language models, including
4//! parameters for model behavior, pricing information, and capability profiles.
5//!
6//! # Examples
7//!
8//! ## Creating a model profile
9//!
10//! ```rust,ignore
11//! use aither::llm::model::{Profile, Ability, Pricing};
12//!
13//! let mut pricing = Pricing::default();
14//!
15//! pricing.prompt = 0.01; // $0.01 per 1K prompt tokens
16//! pricing.completion = 0.03; // $0.03 per 1K completion tokens
17//! pricing.request = 0.01; // $0.01 per request
18//! pricing.image = 0.1; // $0.1 per image
19//! pricing.web_search = 0.05; // $0.05 per web search
20//! pricing.internal_reasoning = 0.003; // $0.003 per internal reasoning
21//! pricing.input_cache_read = 0.0005; // $0.0005 per input cache read
22//! pricing.input_cache_write = 0.001; // $0.001 per input cache write
23//!
24//! let profile = Profile::new("gpt-4", "GPT-4 model", 8192)
25//!     .with_ability(Ability::ToolUse)
26//!     .with_ability(Ability::Vision)
27//!     .with_pricing(pricing);
28//! ```
29//!
30//! ## Configuring model parameters
31//!
32//! ```rust,ignore
33//! use aither::llm::model::Parameters;
34//!
35//! let params = Parameters::default()
36//!     .temperature(0.7)
37//!     .top_p(0.9)
38//!     .max_tokens(1000)
39//!     .seed(42);
40//! ```
41
42use alloc::{string::String, vec::Vec};
43use schemars::Schema;
44use serde_json::Value;
45
46/// Parameters for configuring the behavior of a language model.
47///
48/// This struct contains various parameters that can be used to control
49/// how a language model generates responses. All parameters are optional
50/// and use the builder pattern for easy configuration.
51///
52/// # Examples
53///
54/// ```rust,ignore
55/// use aither::llm::model::Parameters;
56///
57/// let params = Parameters::default()
58///     .temperature(0.7)
59///     .top_p(0.9)
60///     .max_tokens(1000)
61///     .seed(42);
62/// ```
63#[derive(Debug, Default, Clone, PartialEq)]
64#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
65#[allow(clippy::struct_excessive_bools)]
66pub struct Parameters {
67    /// Sampling temperature.
68    ///
69    /// Controls randomness in generation. Higher values (e.g., 1.0) make output more random,
70    /// lower values (e.g., 0.1) make it more deterministic.
71    pub temperature: Option<f32>,
72    /// Nucleus sampling probability.
73    ///
74    /// Only consider tokens with cumulative probability up to this value.
75    /// Typical values are between 0.9 and 1.0.
76    pub top_p: Option<f32>,
77    /// Top-k sampling parameter.
78    ///
79    /// Only consider the k most likely tokens at each step.
80    pub top_k: Option<u32>,
81    /// Frequency penalty to reduce repetition.
82    ///
83    /// Positive values penalize tokens that have already appeared.
84    pub frequency_penalty: Option<f32>,
85    /// Presence penalty to encourage new tokens.
86    ///
87    /// Positive values encourage the model to talk about new topics.
88    pub presence_penalty: Option<f32>,
89    /// Repetition penalty to penalize repeated tokens.
90    ///
91    /// Values > 1.0 discourage repetition, values < 1.0 encourage it.
92    ///
93    /// Local inference only: this maps onto a llama.cpp sampler and none of the
94    /// hosted provider APIs accept it, so setting it has no effect on them.
95    pub repetition_penalty: Option<f32>,
96    /// Minimum probability for nucleus sampling.
97    ///
98    /// Alternative to `top_p` that sets a minimum threshold for token probabilities.
99    ///
100    /// Local inference only, like [`Self::repetition_penalty`].
101    pub min_p: Option<f32>,
102    /// Random seed for reproducibility.
103    ///
104    /// Use the same seed to get deterministic outputs.
105    pub seed: Option<u32>,
106    /// Maximum number of tokens to generate.
107    ///
108    /// Limits the length of the generated response.
109    pub max_tokens: Option<u32>,
110    /// Biases for specific logits.
111    ///
112    /// Each tuple contains a token string and its bias value.
113    pub logit_bias: Option<Vec<(String, f32)>>,
114    /// Whether to return log probabilities.
115    ///
116    /// When true, the model returns probability information for tokens.
117    pub logprobs: Option<bool>,
118    /// Number of top log probabilities to return.
119    ///
120    /// Only used when logprobs is true.
121    pub top_logprobs: Option<u8>,
122    /// Stop sequences to end generation.
123    ///
124    /// Generation stops when any of these strings are encountered.
125    pub stop: Option<Vec<String>>,
126    /// Tool choice policy for the model.
127    ///
128    /// Controls whether tools are allowed, required, or constrained to a specific tool.
129    pub tool_choice: ToolChoice,
130
131    /// Whether the model may request several tools in a single turn.
132    ///
133    /// `None` leaves the decision to the provider's own default. Set it only to
134    /// override that default — forcing `false` serializes an agent loop that
135    /// could otherwise fan its tool calls out concurrently.
136    pub parallel_tool_calls: Option<bool>,
137
138    /// Whether the provider should answer with a streaming response.
139    ///
140    /// `None` leaves the decision to the provider's own default — providers
141    /// that offer both modes stream by default through
142    /// [`LanguageModel::respond`](crate::LanguageModel::respond). `Some(false)`
143    /// asks for a single buffered response, which callers use for requests
144    /// that exist only to touch the provider (for example a prompt-cache
145    /// keep-alive), never to produce visible output.
146    pub stream: Option<bool>,
147
148    /// Preferred reasoning effort when supported.
149    pub reasoning_effort: Option<ReasoningEffort>,
150
151    /// Whether the provider should include reasoning summaries in the response stream.
152    pub include_reasoning: bool,
153
154    /// Whether to enable structured outputs.
155    ///
156    /// When true, the model will attempt to return outputs in a structured format (e.g., JSON).
157    pub structured_outputs: bool,
158
159    /// The expected response format schema.
160    ///
161    /// When set, the model will attempt to return outputs matching this schema.
162    pub response_format: Option<Schema>,
163    /// Whether to enable native Search tool for grounding.
164    pub websearch: bool,
165    /// Whether to enable native Code Execution tool.
166    pub code_execution: bool,
167    /// Provider-native tools that are not portable across API families.
168    #[cfg_attr(
169        feature = "serde",
170        serde(default, skip_serializing_if = "NativeTools::is_empty")
171    )]
172    pub native_tools: NativeTools,
173    /// Provider-specific prompt cache controls.
174    #[cfg_attr(
175        feature = "serde",
176        serde(default, skip_serializing_if = "CacheOptions::is_empty")
177    )]
178    pub cache: CacheOptions,
179}
180
181macro_rules! impl_with_methods {
182    (
183        impl $ty:ty {
184            $($field:ident : $field_ty:ty),* $(,)?
185        }
186    ) => {
187        impl $ty {
188            $(
189                /// Sets the parameter value using a builder pattern.
190                ///
191                /// # Arguments
192                ///
193                /// * `value` - The value to set for this parameter
194                #[allow(clippy::missing_const_for_fn)]
195                #[must_use] pub fn $field(mut self, value: $field_ty) -> Self {
196                    self.$field = Some(value);
197                    self
198                }
199            )*
200        }
201    };
202}
203
204impl_with_methods! {
205    impl Parameters {
206        temperature: f32,
207        top_p: f32,
208        top_k: u32,
209        frequency_penalty: f32,
210        presence_penalty: f32,
211        repetition_penalty: f32,
212        min_p: f32,
213        seed: u32,
214        max_tokens: u32,
215        logit_bias: Vec<(String, f32)>,
216        logprobs: bool,
217        top_logprobs: u8,
218        stop: Vec<String>,
219        parallel_tool_calls: bool,
220        stream: bool,
221    }
222}
223
224impl Parameters {
225    /// Sets whether providers should include reasoning summaries.
226    #[must_use]
227    pub const fn include_reasoning(mut self, include: bool) -> Self {
228        self.include_reasoning = include;
229        self
230    }
231
232    /// Sets the preferred reasoning effort.
233    #[must_use]
234    pub const fn reasoning_effort(mut self, effort: ReasoningEffort) -> Self {
235        self.reasoning_effort = Some(effort);
236        self
237    }
238
239    /// Sets whether to enable native Google Search tool.
240    #[must_use]
241    pub const fn websearch(mut self, enabled: bool) -> Self {
242        self.websearch = enabled;
243        self
244    }
245
246    /// Sets whether to enable native Code Execution tool.
247    #[must_use]
248    pub const fn code_execution(mut self, enabled: bool) -> Self {
249        self.code_execution = enabled;
250        self
251    }
252
253    /// Sets provider-native tools.
254    #[must_use]
255    pub fn native_tools(mut self, tools: NativeTools) -> Self {
256        self.native_tools = tools;
257        self
258    }
259
260    /// Whether a provider-hosted web search tool is enabled for the request.
261    ///
262    /// Checks the portable [`Self::websearch`] flag plus every provider-native
263    /// equivalent (`OpenAI` `web_search`, Gemini `google_search`, Claude
264    /// `web_search`), so callers can decide whether a client-side search tool
265    /// is redundant regardless of which provider serves the request.
266    #[must_use]
267    pub const fn hosted_web_search(&self) -> bool {
268        self.websearch
269            || self.native_tools.openai.web_search.is_some()
270            || self.native_tools.gemini.google_search
271            || self.native_tools.claude.web_search
272    }
273
274    /// Whether a provider-hosted URL fetch tool is enabled for the request.
275    ///
276    /// Covers Claude `web_fetch` and Gemini `url_context`; `OpenAI` has no
277    /// standalone fetch tool (its `web_search` reads pages internally).
278    #[must_use]
279    pub const fn hosted_web_fetch(&self) -> bool {
280        self.native_tools.claude.web_fetch || self.native_tools.gemini.url_context
281    }
282
283    /// Sets `OpenAI` Responses-native tools.
284    #[must_use]
285    pub fn openai_tools(mut self, tools: OpenAINativeTools) -> Self {
286        self.native_tools.openai = tools;
287        self
288    }
289
290    /// Sets Gemini-native tools.
291    #[must_use]
292    pub const fn gemini_tools(mut self, tools: GeminiNativeTools) -> Self {
293        self.native_tools.gemini = tools;
294        self
295    }
296
297    /// Sets Claude-native tools.
298    #[must_use]
299    pub const fn claude_tools(mut self, tools: ClaudeNativeTools) -> Self {
300        self.native_tools.claude = tools;
301        self
302    }
303
304    /// Sets the OpenAI-compatible prompt cache key.
305    #[must_use]
306    pub fn prompt_cache_key(mut self, key: impl Into<String>) -> Self {
307        let cache = self
308            .cache
309            .openai
310            .get_or_insert_with(OpenAIPromptCache::default);
311        cache.key = Some(key.into());
312        self
313    }
314
315    /// Sets the OpenAI-compatible prompt cache retention policy.
316    #[must_use]
317    pub fn prompt_cache_retention(mut self, retention: OpenAIPromptCacheRetention) -> Self {
318        let cache = self
319            .cache
320            .openai
321            .get_or_insert_with(OpenAIPromptCache::default);
322        cache.retention = Some(retention);
323        self
324    }
325
326    /// Sets Claude prompt caching options.
327    #[must_use]
328    pub const fn claude_prompt_cache(mut self, cache: ClaudePromptCache) -> Self {
329        self.cache.claude = Some(cache);
330        self
331    }
332
333    /// Sets Claude prompt caching with automatic top-level cache control.
334    #[must_use]
335    pub const fn claude_prompt_cache_automatic(mut self, ttl: ClaudePromptCacheTtl) -> Self {
336        self.cache.claude = Some(ClaudePromptCache::automatic(ttl));
337        self
338    }
339
340    /// Sets Claude prompt caching with explicit block-level cache breakpoints.
341    #[must_use]
342    pub const fn claude_prompt_cache_explicit(
343        mut self,
344        ttl: ClaudePromptCacheTtl,
345        breakpoints: ClaudeExplicitCacheBreakpoints,
346    ) -> Self {
347        self.cache.claude = Some(ClaudePromptCache::explicit(ttl, breakpoints));
348        self
349    }
350
351    /// Sets Claude prompt caching with automatic and explicit breakpoint modes combined.
352    #[must_use]
353    pub const fn claude_prompt_cache_automatic_with_explicit(
354        mut self,
355        ttl: ClaudePromptCacheTtl,
356        breakpoints: ClaudeExplicitCacheBreakpoints,
357    ) -> Self {
358        self.cache.claude = Some(ClaudePromptCache::automatic_with_explicit(ttl, breakpoints));
359        self
360    }
361
362    /// Sets the Gemini cached content resource name.
363    #[must_use]
364    pub fn gemini_cached_content(mut self, cached_content: impl Into<String>) -> Self {
365        self.cache.gemini = Some(GeminiPromptCache::new(cached_content));
366        self
367    }
368
369    /// Clears all provider-specific cache options.
370    #[must_use]
371    pub fn without_cache(mut self) -> Self {
372        self.cache = CacheOptions {
373            openai: None,
374            claude: None,
375            gemini: None,
376        };
377        self
378    }
379
380    /// Sets the tool choice policy.
381    #[must_use]
382    pub fn tool_choice(mut self, choice: ToolChoice) -> Self {
383        self.tool_choice = choice;
384        self
385    }
386}
387
388/// Provider-native tool configuration.
389#[derive(Debug, Clone, PartialEq, Eq, Default)]
390#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
391pub struct NativeTools {
392    /// `OpenAI` Responses API hosted tools.
393    #[cfg_attr(
394        feature = "serde",
395        serde(default, skip_serializing_if = "OpenAINativeTools::is_empty")
396    )]
397    pub openai: OpenAINativeTools,
398    /// Gemini hosted and client-side tools.
399    #[cfg_attr(
400        feature = "serde",
401        serde(default, skip_serializing_if = "GeminiNativeTools::is_empty")
402    )]
403    pub gemini: GeminiNativeTools,
404    /// Claude Anthropic-defined and server tools.
405    #[cfg_attr(
406        feature = "serde",
407        serde(default, skip_serializing_if = "ClaudeNativeTools::is_empty")
408    )]
409    pub claude: ClaudeNativeTools,
410}
411
412impl NativeTools {
413    /// Returns true when no provider-native tools are configured.
414    #[must_use]
415    pub const fn is_empty(&self) -> bool {
416        self.openai.is_empty() && self.gemini.is_empty() && self.claude.is_empty()
417    }
418}
419
420/// `OpenAI` Responses API native tools.
421#[derive(Debug, Clone, PartialEq, Eq, Default)]
422#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
423pub struct OpenAINativeTools {
424    /// Hosted web search.
425    #[cfg_attr(
426        feature = "serde",
427        serde(default, skip_serializing_if = "Option::is_none")
428    )]
429    pub web_search: Option<OpenAIWebSearchTool>,
430    /// Hosted file search over vector stores.
431    #[cfg_attr(
432        feature = "serde",
433        serde(default, skip_serializing_if = "Vec::is_empty")
434    )]
435    pub file_search: Vec<OpenAIFileSearchTool>,
436    /// Hosted code interpreter.
437    #[cfg_attr(
438        feature = "serde",
439        serde(default, skip_serializing_if = "Option::is_none")
440    )]
441    pub code_interpreter: Option<OpenAICodeInterpreterTool>,
442    /// Hosted image generation.
443    #[cfg_attr(
444        feature = "serde",
445        serde(default, skip_serializing_if = "Option::is_none")
446    )]
447    pub image_generation: Option<OpenAIImageGenerationTool>,
448    /// Remote MCP servers.
449    #[cfg_attr(
450        feature = "serde",
451        serde(default, skip_serializing_if = "Vec::is_empty")
452    )]
453    pub mcp: Vec<OpenAIMcpTool>,
454    /// Computer use preview.
455    #[cfg_attr(
456        feature = "serde",
457        serde(default, skip_serializing_if = "Option::is_none")
458    )]
459    pub computer_use: Option<OpenAIComputerUseTool>,
460}
461
462impl OpenAINativeTools {
463    /// Returns true when no `OpenAI` native tools are configured.
464    #[must_use]
465    pub const fn is_empty(&self) -> bool {
466        self.web_search.is_none()
467            && self.file_search.is_empty()
468            && self.code_interpreter.is_none()
469            && self.image_generation.is_none()
470            && self.mcp.is_empty()
471            && self.computer_use.is_none()
472    }
473
474    /// Enables hosted web search with default API settings.
475    #[must_use]
476    pub fn with_web_search(mut self, tool: OpenAIWebSearchTool) -> Self {
477        self.web_search = Some(tool);
478        self
479    }
480
481    /// Enables hosted web search with default API settings.
482    #[must_use]
483    pub fn enable_web_search(mut self) -> Self {
484        self.web_search = Some(OpenAIWebSearchTool::default());
485        self
486    }
487
488    /// Adds a file search tool.
489    #[must_use]
490    pub fn with_file_search(mut self, tool: OpenAIFileSearchTool) -> Self {
491        self.file_search.push(tool);
492        self
493    }
494
495    /// Enables hosted code interpreter.
496    #[must_use]
497    pub fn with_code_interpreter(mut self, tool: OpenAICodeInterpreterTool) -> Self {
498        self.code_interpreter = Some(tool);
499        self
500    }
501
502    /// Enables hosted image generation.
503    #[must_use]
504    pub const fn with_image_generation(mut self, tool: OpenAIImageGenerationTool) -> Self {
505        self.image_generation = Some(tool);
506        self
507    }
508
509    /// Adds a remote MCP server.
510    #[must_use]
511    pub fn with_mcp(mut self, tool: OpenAIMcpTool) -> Self {
512        self.mcp.push(tool);
513        self
514    }
515
516    /// Enables computer use preview.
517    #[must_use]
518    pub fn with_computer_use(mut self, tool: OpenAIComputerUseTool) -> Self {
519        self.computer_use = Some(tool);
520        self
521    }
522}
523
524/// `OpenAI` hosted web search configuration.
525#[derive(Debug, Clone, PartialEq, Eq, Default)]
526#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
527pub struct OpenAIWebSearchTool {
528    /// Whether live web access is allowed. `None` uses `OpenAI`'s default.
529    #[cfg_attr(
530        feature = "serde",
531        serde(default, skip_serializing_if = "Option::is_none")
532    )]
533    pub external_web_access: Option<bool>,
534    /// Official filter object, such as `allowed_domains`.
535    #[cfg_attr(
536        feature = "serde",
537        serde(default, skip_serializing_if = "Option::is_none")
538    )]
539    pub filters: Option<Value>,
540    /// Official user location object.
541    #[cfg_attr(
542        feature = "serde",
543        serde(default, skip_serializing_if = "Option::is_none")
544    )]
545    pub user_location: Option<Value>,
546}
547
548impl OpenAIWebSearchTool {
549    /// Sets whether live web access is allowed.
550    #[must_use]
551    pub const fn external_web_access(mut self, allowed: bool) -> Self {
552        self.external_web_access = Some(allowed);
553        self
554    }
555
556    /// Sets the official filter object.
557    #[must_use]
558    pub fn filters(mut self, filters: Value) -> Self {
559        self.filters = Some(filters);
560        self
561    }
562
563    /// Sets the official user location object.
564    #[must_use]
565    pub fn user_location(mut self, location: Value) -> Self {
566        self.user_location = Some(location);
567        self
568    }
569}
570
571/// `OpenAI` hosted file search configuration.
572#[derive(Debug, Clone, PartialEq, Eq)]
573#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
574pub struct OpenAIFileSearchTool {
575    /// Vector stores to search.
576    pub vector_store_ids: Vec<String>,
577    /// Maximum number of search results.
578    #[cfg_attr(
579        feature = "serde",
580        serde(default, skip_serializing_if = "Option::is_none")
581    )]
582    pub max_num_results: Option<u32>,
583    /// Whether to request `file_search_call.results` in the response include list.
584    pub include_results: bool,
585    /// Official filter object.
586    #[cfg_attr(
587        feature = "serde",
588        serde(default, skip_serializing_if = "Option::is_none")
589    )]
590    pub filters: Option<Value>,
591}
592
593impl OpenAIFileSearchTool {
594    /// Creates file search over the provided vector stores.
595    #[must_use]
596    pub fn new(vector_store_ids: impl Into<Vec<String>>) -> Self {
597        Self {
598            vector_store_ids: vector_store_ids.into(),
599            max_num_results: None,
600            include_results: false,
601            filters: None,
602        }
603    }
604
605    /// Sets maximum result count.
606    #[must_use]
607    pub const fn max_num_results(mut self, value: u32) -> Self {
608        self.max_num_results = Some(value);
609        self
610    }
611
612    /// Requests file search result inclusion.
613    #[must_use]
614    pub const fn include_results(mut self, include: bool) -> Self {
615        self.include_results = include;
616        self
617    }
618
619    /// Sets the official filter object.
620    #[must_use]
621    pub fn filters(mut self, filters: Value) -> Self {
622        self.filters = Some(filters);
623        self
624    }
625}
626
627/// `OpenAI` code interpreter configuration.
628#[derive(Debug, Clone, PartialEq, Eq)]
629#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
630pub struct OpenAICodeInterpreterTool {
631    /// Container selection.
632    pub container: OpenAICodeInterpreterContainer,
633}
634
635impl Default for OpenAICodeInterpreterTool {
636    fn default() -> Self {
637        Self {
638            container: OpenAICodeInterpreterContainer::Auto(OpenAIAutoContainer::default()),
639        }
640    }
641}
642
643impl OpenAICodeInterpreterTool {
644    /// Uses an automatically managed container.
645    #[must_use]
646    pub const fn auto() -> Self {
647        Self {
648            container: OpenAICodeInterpreterContainer::Auto(OpenAIAutoContainer::new()),
649        }
650    }
651
652    /// Uses an existing container ID.
653    #[must_use]
654    pub fn existing(container_id: impl Into<String>) -> Self {
655        Self {
656            container: OpenAICodeInterpreterContainer::Existing(container_id.into()),
657        }
658    }
659}
660
661/// `OpenAI` code interpreter container selection.
662#[derive(Debug, Clone, PartialEq, Eq)]
663#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
664pub enum OpenAICodeInterpreterContainer {
665    /// Automatically create or reuse a container.
666    Auto(OpenAIAutoContainer),
667    /// Use an existing container ID.
668    Existing(String),
669}
670
671/// `OpenAI` automatically managed code interpreter container.
672#[allow(clippy::struct_excessive_bools)]
673#[derive(Debug, Clone, PartialEq, Eq, Default)]
674#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
675pub struct OpenAIAutoContainer {
676    /// Memory tier such as `1g`, `4g`, `16g`, or `64g`.
677    #[cfg_attr(
678        feature = "serde",
679        serde(default, skip_serializing_if = "Option::is_none")
680    )]
681    pub memory_limit: Option<String>,
682    /// File IDs to preload into the container.
683    #[cfg_attr(
684        feature = "serde",
685        serde(default, skip_serializing_if = "Vec::is_empty")
686    )]
687    pub file_ids: Vec<String>,
688}
689
690impl OpenAIAutoContainer {
691    /// Creates an auto container with default API settings.
692    #[must_use]
693    pub const fn new() -> Self {
694        Self {
695            memory_limit: None,
696            file_ids: Vec::new(),
697        }
698    }
699
700    /// Sets the memory tier.
701    #[must_use]
702    pub fn memory_limit(mut self, memory_limit: impl Into<String>) -> Self {
703        self.memory_limit = Some(memory_limit.into());
704        self
705    }
706
707    /// Preloads file IDs.
708    #[must_use]
709    pub fn file_ids(mut self, file_ids: impl Into<Vec<String>>) -> Self {
710        self.file_ids = file_ids.into();
711        self
712    }
713}
714
715/// `OpenAI` image generation tool configuration.
716#[derive(Debug, Clone, PartialEq, Eq, Default)]
717#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
718pub struct OpenAIImageGenerationTool {
719    /// Number of partial image events to stream.
720    #[cfg_attr(
721        feature = "serde",
722        serde(default, skip_serializing_if = "Option::is_none")
723    )]
724    pub partial_images: Option<u8>,
725}
726
727impl OpenAIImageGenerationTool {
728    /// Sets partial image count.
729    #[must_use]
730    pub const fn partial_images(mut self, count: u8) -> Self {
731        self.partial_images = Some(count);
732        self
733    }
734}
735
736/// `OpenAI` remote MCP server configuration.
737#[derive(Debug, Clone, PartialEq, Eq)]
738#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
739pub struct OpenAIMcpTool {
740    /// Label for the server.
741    pub server_label: String,
742    /// Server URL.
743    pub server_url: String,
744    /// Approval policy, such as `never` or `always`.
745    #[cfg_attr(
746        feature = "serde",
747        serde(default, skip_serializing_if = "Option::is_none")
748    )]
749    pub require_approval: Option<String>,
750    /// Optional allowlist of tool names.
751    #[cfg_attr(
752        feature = "serde",
753        serde(default, skip_serializing_if = "Vec::is_empty")
754    )]
755    pub allowed_tools: Vec<String>,
756}
757
758impl OpenAIMcpTool {
759    /// Creates a remote MCP server tool.
760    #[must_use]
761    pub fn new(server_label: impl Into<String>, server_url: impl Into<String>) -> Self {
762        Self {
763            server_label: server_label.into(),
764            server_url: server_url.into(),
765            require_approval: None,
766            allowed_tools: Vec::new(),
767        }
768    }
769
770    /// Sets approval policy.
771    #[must_use]
772    pub fn require_approval(mut self, policy: impl Into<String>) -> Self {
773        self.require_approval = Some(policy.into());
774        self
775    }
776
777    /// Sets allowed tools.
778    #[must_use]
779    pub fn allowed_tools(mut self, tools: impl Into<Vec<String>>) -> Self {
780        self.allowed_tools = tools.into();
781        self
782    }
783}
784
785/// `OpenAI` computer use preview tool configuration.
786#[derive(Debug, Clone, PartialEq, Eq)]
787#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
788pub struct OpenAIComputerUseTool {
789    /// Display width in pixels.
790    pub display_width: u32,
791    /// Display height in pixels.
792    pub display_height: u32,
793    /// Environment such as `browser`.
794    pub environment: String,
795}
796
797impl OpenAIComputerUseTool {
798    /// Creates a computer use preview tool.
799    #[must_use]
800    pub fn new(display_width: u32, display_height: u32, environment: impl Into<String>) -> Self {
801        Self {
802            display_width,
803            display_height,
804            environment: environment.into(),
805        }
806    }
807}
808
809/// Gemini-native tools.
810#[derive(Debug, Clone, PartialEq, Eq, Default)]
811#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
812pub struct GeminiNativeTools {
813    /// Grounding with Google Search.
814    pub google_search: bool,
815    /// Built-in code execution.
816    pub code_execution: bool,
817    /// URL Context tool.
818    pub url_context: bool,
819}
820
821impl GeminiNativeTools {
822    /// Returns true when no Gemini-native tools are configured.
823    #[must_use]
824    pub const fn is_empty(&self) -> bool {
825        !self.google_search && !self.code_execution && !self.url_context
826    }
827
828    /// Enables Grounding with Google Search.
829    #[must_use]
830    pub const fn google_search(mut self, enabled: bool) -> Self {
831        self.google_search = enabled;
832        self
833    }
834
835    /// Enables Code Execution.
836    #[must_use]
837    pub const fn code_execution(mut self, enabled: bool) -> Self {
838        self.code_execution = enabled;
839        self
840    }
841
842    /// Enables URL Context.
843    #[must_use]
844    pub const fn url_context(mut self, enabled: bool) -> Self {
845        self.url_context = enabled;
846        self
847    }
848}
849
850/// Claude-native tools.
851#[allow(clippy::struct_excessive_bools)]
852#[derive(Debug, Clone, PartialEq, Eq, Default)]
853#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
854pub struct ClaudeNativeTools {
855    /// Server-side web search.
856    pub web_search: bool,
857    /// Server-side web fetch.
858    pub web_fetch: bool,
859    /// Server-side code execution.
860    pub code_execution: bool,
861    /// Client-side bash tool.
862    pub bash: bool,
863    /// Client-side text editor tool.
864    #[cfg_attr(
865        feature = "serde",
866        serde(default, skip_serializing_if = "Option::is_none")
867    )]
868    pub text_editor: Option<ClaudeTextEditorTool>,
869}
870
871impl ClaudeNativeTools {
872    /// Returns true when no Claude-native tools are configured.
873    #[must_use]
874    pub const fn is_empty(&self) -> bool {
875        !self.web_search
876            && !self.web_fetch
877            && !self.code_execution
878            && !self.bash
879            && self.text_editor.is_none()
880    }
881
882    /// Enables server-side web search.
883    #[must_use]
884    pub const fn web_search(mut self, enabled: bool) -> Self {
885        self.web_search = enabled;
886        self
887    }
888
889    /// Enables server-side web fetch.
890    #[must_use]
891    pub const fn web_fetch(mut self, enabled: bool) -> Self {
892        self.web_fetch = enabled;
893        self
894    }
895
896    /// Enables server-side code execution.
897    #[must_use]
898    pub const fn code_execution(mut self, enabled: bool) -> Self {
899        self.code_execution = enabled;
900        self
901    }
902
903    /// Enables client-side bash.
904    #[must_use]
905    pub const fn bash(mut self, enabled: bool) -> Self {
906        self.bash = enabled;
907        self
908    }
909
910    /// Enables client-side text editor.
911    #[must_use]
912    pub const fn text_editor(mut self, tool: ClaudeTextEditorTool) -> Self {
913        self.text_editor = Some(tool);
914        self
915    }
916}
917
918/// Claude text editor tool configuration.
919#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
920#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
921pub struct ClaudeTextEditorTool {
922    /// Optional maximum characters returned by the `view` command.
923    pub max_characters: Option<u32>,
924}
925
926impl ClaudeTextEditorTool {
927    /// Sets maximum characters for view results.
928    #[must_use]
929    pub const fn max_characters(mut self, value: u32) -> Self {
930        self.max_characters = Some(value);
931        self
932    }
933}
934
935/// Tool choice policy for tool calling.
936#[derive(Debug, Clone, PartialEq, Eq)]
937#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
938#[serde(rename_all = "lowercase")]
939#[derive(Default)]
940pub enum ToolChoice {
941    /// Let the model decide whether to call tools.
942    #[default]
943    Auto,
944    /// Disallow tool calls.
945    None,
946    /// Require a tool call (any available tool).
947    Required,
948    /// Constrain the model to a specific tool.
949    Exact(String),
950}
951
952/// Effort levels available for reasoning-focused models.
953///
954/// The ladder is ordered from least to most reasoning, and spans the union of
955/// what the providers accept. **No provider accepts every level**, and the
956/// supported set varies by model within a provider, so each provider crate maps
957/// this to its own wire vocabulary and rejects a level its API does not have —
958/// rather than silently clamping to the nearest one, which would bill the
959/// caller for a depth they did not ask for.
960///
961/// Deliberately carries no `as_str`: the wire spelling is not shared. `Minimal`
962/// is `"minimal"` on `OpenAI` and Gemini and does not exist on Claude; `XHigh`
963/// and `Max` exist on Claude and `OpenAI` but not Gemini.
964#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
965#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
966#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
967pub enum ReasoningEffort {
968    /// No reasoning at all: answer directly.
969    ///
970    /// On Claude this disables thinking, which the API rejects on models whose
971    /// thinking is always on.
972    None,
973    /// The least reasoning a model will do while still reasoning.
974    Minimal,
975    /// Shallow reasoning, for latency-sensitive work.
976    Low,
977    /// Balanced reasoning depth.
978    Medium,
979    /// Thorough reasoning. The default on current Claude models.
980    High,
981    /// Beyond `High`, where the extra depth pays for itself.
982    XHigh,
983    /// The most reasoning available, for quality-first workloads.
984    Max,
985}
986
987/// Provider-specific prompt cache controls.
988#[derive(Debug, Clone, PartialEq, Eq, Default)]
989#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
990pub struct CacheOptions {
991    /// OpenAI-compatible prompt cache controls (`OpenAI`, `Copilot`).
992    #[cfg_attr(
993        feature = "serde",
994        serde(default, skip_serializing_if = "Option::is_none")
995    )]
996    pub openai: Option<OpenAIPromptCache>,
997    /// Anthropic Claude prompt cache controls.
998    #[cfg_attr(
999        feature = "serde",
1000        serde(default, skip_serializing_if = "Option::is_none")
1001    )]
1002    pub claude: Option<ClaudePromptCache>,
1003    /// Gemini cached content reference.
1004    #[cfg_attr(
1005        feature = "serde",
1006        serde(default, skip_serializing_if = "Option::is_none")
1007    )]
1008    pub gemini: Option<GeminiPromptCache>,
1009}
1010
1011impl CacheOptions {
1012    /// Returns true when no provider cache options are set.
1013    #[must_use]
1014    pub const fn is_empty(&self) -> bool {
1015        self.openai.is_none() && self.claude.is_none() && self.gemini.is_none()
1016    }
1017}
1018
1019/// OpenAI-compatible prompt cache controls.
1020#[derive(Debug, Clone, PartialEq, Eq, Default)]
1021#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1022pub struct OpenAIPromptCache {
1023    /// Stable key used to route related prompts to the same cache shard.
1024    #[cfg_attr(
1025        feature = "serde",
1026        serde(default, skip_serializing_if = "Option::is_none")
1027    )]
1028    pub key: Option<String>,
1029    /// Retention policy for prompt cache entries.
1030    #[cfg_attr(
1031        feature = "serde",
1032        serde(default, skip_serializing_if = "Option::is_none")
1033    )]
1034    pub retention: Option<OpenAIPromptCacheRetention>,
1035}
1036
1037/// `OpenAI` prompt cache retention policies.
1038#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1039#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1040#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
1041pub enum OpenAIPromptCacheRetention {
1042    /// Keep cache entries in-memory (default `OpenAI` retention mode).
1043    InMemory,
1044    /// Keep cache entries for 24 hours.
1045    #[cfg_attr(feature = "serde", serde(rename = "24h"))]
1046    Hours24,
1047}
1048
1049impl OpenAIPromptCacheRetention {
1050    /// Returns the API value expected by OpenAI-compatible endpoints.
1051    #[must_use]
1052    pub const fn as_str(self) -> &'static str {
1053        match self {
1054            Self::InMemory => "in-memory",
1055            Self::Hours24 => "24h",
1056        }
1057    }
1058}
1059
1060/// Claude prompt cache control for the Messages API.
1061#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1062#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1063pub struct ClaudePromptCache {
1064    /// Requested cache TTL policy.
1065    pub ttl: ClaudePromptCacheTtl,
1066    /// Cache application strategy.
1067    pub strategy: ClaudePromptCacheStrategy,
1068}
1069
1070impl ClaudePromptCache {
1071    /// Build a cache configuration with the chosen TTL.
1072    #[must_use]
1073    pub const fn new(ttl: ClaudePromptCacheTtl) -> Self {
1074        Self {
1075            ttl,
1076            strategy: ClaudePromptCacheStrategy::Automatic,
1077        }
1078    }
1079
1080    /// Build an automatic top-level cache configuration.
1081    #[must_use]
1082    pub const fn automatic(ttl: ClaudePromptCacheTtl) -> Self {
1083        Self::new(ttl)
1084    }
1085
1086    /// Build an explicit block-level cache configuration.
1087    #[must_use]
1088    pub const fn explicit(
1089        ttl: ClaudePromptCacheTtl,
1090        breakpoints: ClaudeExplicitCacheBreakpoints,
1091    ) -> Self {
1092        Self {
1093            ttl,
1094            strategy: ClaudePromptCacheStrategy::Explicit(breakpoints),
1095        }
1096    }
1097
1098    /// Build a cache configuration that combines automatic and explicit breakpoints.
1099    #[must_use]
1100    pub const fn automatic_with_explicit(
1101        ttl: ClaudePromptCacheTtl,
1102        breakpoints: ClaudeExplicitCacheBreakpoints,
1103    ) -> Self {
1104        Self {
1105            ttl,
1106            strategy: ClaudePromptCacheStrategy::AutomaticAndExplicit(breakpoints),
1107        }
1108    }
1109
1110    /// Override the cache strategy.
1111    #[must_use]
1112    pub const fn with_strategy(mut self, strategy: ClaudePromptCacheStrategy) -> Self {
1113        self.strategy = strategy;
1114        self
1115    }
1116}
1117
1118/// Claude prompt cache TTL values.
1119#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1120#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1121pub enum ClaudePromptCacheTtl {
1122    /// Short-lived cache (5 minutes).
1123    #[default]
1124    FiveMinutes,
1125    /// Extended cache (1 hour).
1126    OneHour,
1127}
1128
1129impl ClaudePromptCacheTtl {
1130    /// Returns the API value expected by Claude.
1131    #[must_use]
1132    pub const fn as_str(self) -> &'static str {
1133        match self {
1134            Self::FiveMinutes => "5m",
1135            Self::OneHour => "1h",
1136        }
1137    }
1138}
1139
1140/// Claude prompt cache strategy.
1141#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1142#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1143pub enum ClaudePromptCacheStrategy {
1144    /// Add top-level `cache_control` (automatic caching).
1145    #[default]
1146    Automatic,
1147    /// Place `cache_control` on selected cacheable blocks.
1148    Explicit(ClaudeExplicitCacheBreakpoints),
1149    /// Use top-level automatic caching and explicit breakpoints together.
1150    AutomaticAndExplicit(ClaudeExplicitCacheBreakpoints),
1151}
1152
1153/// Explicit Claude cache breakpoint.
1154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1155#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1156pub struct ClaudeExplicitCacheBreakpoint {
1157    /// Block target where cache control should be applied.
1158    pub target: ClaudeCacheBreakpointTarget,
1159    /// Optional TTL override for this breakpoint.
1160    ///
1161    /// When omitted, request-level `ClaudePromptCache::ttl` is used.
1162    pub ttl: Option<ClaudePromptCacheTtl>,
1163}
1164
1165impl ClaudeExplicitCacheBreakpoint {
1166    /// Creates a breakpoint for the given target using request-level default TTL.
1167    #[must_use]
1168    pub const fn new(target: ClaudeCacheBreakpointTarget) -> Self {
1169        Self { target, ttl: None }
1170    }
1171
1172    /// Overrides TTL for this breakpoint.
1173    #[must_use]
1174    pub const fn with_ttl(mut self, ttl: ClaudePromptCacheTtl) -> Self {
1175        self.ttl = Some(ttl);
1176        self
1177    }
1178
1179    /// Returns the effective TTL for this breakpoint.
1180    #[must_use]
1181    pub const fn effective_ttl(self, default_ttl: ClaudePromptCacheTtl) -> ClaudePromptCacheTtl {
1182        match self.ttl {
1183            Some(ttl) => ttl,
1184            None => default_ttl,
1185        }
1186    }
1187}
1188
1189/// Explicit Claude cache breakpoint placement target.
1190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1191#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1192pub enum ClaudeCacheBreakpointTarget {
1193    /// The last tool definition.
1194    LastTool,
1195    /// A specific tool definition by index.
1196    Tool(usize),
1197    /// The last non-empty system text block.
1198    LastSystem,
1199    /// A specific system text block by index.
1200    System(usize),
1201    /// The last cacheable content block in messages.
1202    LastMessage,
1203    /// A specific message content block by message and block indices.
1204    Message {
1205        /// Message index in `messages`.
1206        message_index: usize,
1207        /// Content block index within the message.
1208        block_index: usize,
1209    },
1210}
1211
1212/// Up to four explicit Claude cache breakpoints.
1213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1214#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1215pub struct ClaudeExplicitCacheBreakpoints {
1216    /// First explicit breakpoint.
1217    pub first: ClaudeExplicitCacheBreakpoint,
1218    /// Optional second explicit breakpoint.
1219    pub second: Option<ClaudeExplicitCacheBreakpoint>,
1220    /// Optional third explicit breakpoint.
1221    pub third: Option<ClaudeExplicitCacheBreakpoint>,
1222    /// Optional fourth explicit breakpoint.
1223    pub fourth: Option<ClaudeExplicitCacheBreakpoint>,
1224}
1225
1226impl ClaudeExplicitCacheBreakpoints {
1227    /// Creates an explicit breakpoint set with one mandatory breakpoint.
1228    #[must_use]
1229    pub const fn new(first: ClaudeExplicitCacheBreakpoint) -> Self {
1230        Self {
1231            first,
1232            second: None,
1233            third: None,
1234            fourth: None,
1235        }
1236    }
1237
1238    /// Sets the second breakpoint.
1239    #[must_use]
1240    pub const fn with_second(mut self, second: ClaudeExplicitCacheBreakpoint) -> Self {
1241        self.second = Some(second);
1242        self
1243    }
1244
1245    /// Sets the third breakpoint.
1246    #[must_use]
1247    pub const fn with_third(mut self, third: ClaudeExplicitCacheBreakpoint) -> Self {
1248        self.third = Some(third);
1249        self
1250    }
1251
1252    /// Sets the fourth breakpoint.
1253    #[must_use]
1254    pub const fn with_fourth(mut self, fourth: ClaudeExplicitCacheBreakpoint) -> Self {
1255        self.fourth = Some(fourth);
1256        self
1257    }
1258
1259    /// Returns all configured breakpoints in declared order.
1260    pub fn iter(self) -> impl Iterator<Item = ClaudeExplicitCacheBreakpoint> {
1261        [Some(self.first), self.second, self.third, self.fourth]
1262            .into_iter()
1263            .flatten()
1264    }
1265
1266    /// Number of configured breakpoints.
1267    #[must_use]
1268    pub const fn count(&self) -> usize {
1269        1 + self.second.is_some() as usize
1270            + self.third.is_some() as usize
1271            + self.fourth.is_some() as usize
1272    }
1273
1274    /// Returns true when all four breakpoint slots are in use.
1275    #[must_use]
1276    pub const fn is_full(&self) -> bool {
1277        self.fourth.is_some()
1278    }
1279
1280    /// Cache only the last cacheable message content block.
1281    #[must_use]
1282    pub const fn messages_only() -> Self {
1283        Self::new(ClaudeExplicitCacheBreakpoint::new(
1284            ClaudeCacheBreakpointTarget::LastMessage,
1285        ))
1286    }
1287
1288    /// Cache tool definitions, system blocks, and message blocks.
1289    #[must_use]
1290    pub const fn all() -> Self {
1291        Self::new(ClaudeExplicitCacheBreakpoint::new(
1292            ClaudeCacheBreakpointTarget::LastTool,
1293        ))
1294        .with_second(ClaudeExplicitCacheBreakpoint::new(
1295            ClaudeCacheBreakpointTarget::LastSystem,
1296        ))
1297        .with_third(ClaudeExplicitCacheBreakpoint::new(
1298            ClaudeCacheBreakpointTarget::LastMessage,
1299        ))
1300    }
1301}
1302
1303impl Default for ClaudeExplicitCacheBreakpoints {
1304    fn default() -> Self {
1305        Self::messages_only()
1306    }
1307}
1308
1309/// Gemini cached content reference.
1310#[derive(Debug, Clone, PartialEq, Eq)]
1311#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1312pub struct GeminiPromptCache {
1313    /// Resource name returned by Gemini cache creation APIs.
1314    pub cached_content: String,
1315}
1316
1317impl GeminiPromptCache {
1318    /// Creates a Gemini cached content reference.
1319    #[must_use]
1320    pub fn new(cached_content: impl Into<String>) -> Self {
1321        Self {
1322            cached_content: cached_content.into(),
1323        }
1324    }
1325}
1326
1327/// Represents a language model's profile, including its name, description, abilities, context length, and optional pricing.
1328///
1329/// A model profile provides comprehensive information about a language model's
1330/// capabilities, limitations, and pricing structure. This allows applications
1331/// to make informed decisions about which model to use for specific tasks.
1332///
1333/// # Examples
1334///
1335/// ```rust,ignore
1336/// use aither::llm::model::{Profile, Ability, Pricing};
1337///
1338/// let profile = Profile::new("gpt-4", "GPT-4 Turbo", 128000)
1339///     .with_ability(Ability::ToolUse)
1340///     .with_ability(Ability::Vision);
1341/// ```
1342#[derive(Debug, Clone, PartialEq, PartialOrd)]
1343#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1344#[non_exhaustive]
1345pub struct Profile {
1346    /// The name of the model.
1347    pub name: String,
1348    /// The author of the model.
1349    pub author: String,
1350    /// The slug of the model.
1351    pub slug: String,
1352    /// A description of the model.
1353    pub description: String,
1354    /// The abilities supported by the model.
1355    pub abilities: Vec<Ability>,
1356    /// The maximum context length supported by the model.
1357    pub context_length: u32,
1358    /// Optional pricing information for the model.
1359    pub pricing: Option<Pricing>,
1360}
1361
1362/// Pricing information for a model's various capabilities (unit: USD).
1363///
1364/// This struct contains detailed pricing information for different aspects
1365/// of model usage. All prices are in USD and typically represent costs
1366/// per unit (token, request, image, etc.).
1367///
1368/// # Examples
1369///
1370/// ```rust,ignore
1371/// use aither::llm::model::Pricing;
1372///
1373/// let mut pricing = Pricing::default();
1374///
1375/// pricing.prompt = 0.01; // $0.01 per 1K prompt tokens
1376/// pricing.completion = 0.03; // $0.03 per 1K completion tokens
1377/// pricing.image = 0.25; // $0.25 per image
1378/// pricing.web_search = 0.005; // $0.005 per search
1379/// ```
1380#[derive(Debug, Clone, PartialEq, PartialOrd, Default)]
1381#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1382#[non_exhaustive]
1383pub struct Pricing {
1384    /// Price per prompt token.
1385    pub prompt: f64,
1386    /// Price per completion token.
1387    pub completion: f64,
1388    /// Price per request.
1389    pub request: f64,
1390    /// Price per image processed.
1391    pub image: f64,
1392    /// Price per web search.
1393    pub web_search: f64,
1394    /// Price for internal reasoning.
1395    pub internal_reasoning: f64,
1396    /// Price for reading from input cache.
1397    pub input_cache_read: f64,
1398    /// Price for writing to input cache.
1399    pub input_cache_write: f64,
1400}
1401
1402/// Indicates which parameters are supported by a model.
1403///
1404/// This struct is used to communicate which configuration parameters
1405/// a specific model supports, allowing applications to adjust their
1406/// requests accordingly.
1407///
1408/// # Examples
1409///
1410/// ```rust,ignore
1411/// use aither::llm::model::SupportedParameters;
1412///
1413/// let mut support = SupportedParameters::default();
1414///
1415/// support.temperature = true;
1416/// support.max_tokens = true;
1417/// support.top_p = true;
1418/// support.stop = true;
1419/// support.seed = true;
1420/// ```
1421#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
1422#[allow(clippy::struct_excessive_bools)]
1423#[non_exhaustive]
1424pub struct SupportedParameters {
1425    /// Whether `max_tokens` is supported.
1426    pub max_tokens: bool,
1427    /// Whether temperature is supported.
1428    pub temperature: bool,
1429    /// Whether `top_p` is supported.
1430    pub top_p: bool,
1431    /// Whether reasoning is supported.
1432    pub reasoning: bool,
1433    /// Whether including reasoning is supported.
1434    pub include_reasoning: bool,
1435    /// Whether structured outputs are supported.
1436    pub structured_outputs: bool,
1437    /// Whether response format is supported.
1438    pub response_format: bool,
1439    /// Whether stop sequences are supported.
1440    pub stop: bool,
1441    /// Whether frequency penalty is supported.
1442    pub frequency_penalty: bool,
1443    /// Whether presence penalty is supported.
1444    pub presence_penalty: bool,
1445    /// Whether seed is supported.
1446    pub seed: bool,
1447}
1448
1449impl Profile {
1450    /// Creates a new `Profile` with the given name, description, and context length.
1451    ///
1452    /// # Arguments
1453    ///
1454    /// * `name` - The name of the model (e.g., "gpt-4", "claude-3-opus")
1455    /// * `description` - A human-readable description of the model
1456    /// * `context_length` - Maximum number of tokens the model can process
1457    ///
1458    /// # Examples
1459    ///
1460    /// ```rust,ignore
1461    /// use aither::llm::model::Profile;
1462    ///
1463    /// let profile = Profile::new("gpt-4", "GPT-4 Turbo", 128000);
1464    /// ```
1465    pub fn new(
1466        name: impl Into<String>,
1467        author: impl Into<String>,
1468        slug: impl Into<String>,
1469        description: impl Into<String>,
1470        context_length: u32,
1471    ) -> Self {
1472        Self {
1473            name: name.into(),
1474            author: author.into(),
1475            slug: slug.into(),
1476            description: description.into(),
1477            abilities: Vec::new(),
1478            context_length,
1479            pricing: None,
1480        }
1481    }
1482
1483    /// Adds a single ability to the profile.
1484    ///
1485    /// # Arguments
1486    ///
1487    /// * `ability` - The ability to add to this profile
1488    ///
1489    /// # Examples
1490    ///
1491    /// ```rust,ignore
1492    /// use aither::llm::model::{Profile, Ability};
1493    ///
1494    /// let profile = Profile::new("vision-model", "A vision-capable model", 8192)
1495    ///     .with_ability(Ability::Vision);
1496    /// ```
1497    #[must_use]
1498    pub fn with_ability(self, ability: Ability) -> Self {
1499        self.with_abilities([ability])
1500    }
1501
1502    /// Adds multiple abilities to the profile.
1503    ///
1504    /// # Arguments
1505    ///
1506    /// * `abilities` - An iterable collection of abilities to add
1507    ///
1508    /// # Examples
1509    ///
1510    /// ```rust,ignore
1511    /// use aither::llm::model::{Profile, Ability};
1512    ///
1513    /// let abilities = [Ability::ToolUse, Ability::Vision, Ability::Audio];
1514    /// let profile = Profile::new("multimodal", "A multimodal model", 32768)
1515    ///     .with_abilities(abilities);
1516    /// ```
1517    #[must_use]
1518    pub fn with_abilities(mut self, abilities: impl IntoIterator<Item = Ability>) -> Self {
1519        self.abilities.extend(abilities);
1520        self
1521    }
1522
1523    /// Sets the pricing information for the profile.
1524    ///
1525    /// # Arguments
1526    ///
1527    /// * `pricing` - The pricing structure for this model
1528    ///
1529    /// # Examples
1530    ///
1531    /// ```rust,ignore
1532    /// use aither::llm::model::{Profile, Pricing};
1533    ///
1534    /// let mut pricing = Pricing::default();
1535    ///
1536    /// pricing.prompt = 0.01;
1537    /// pricing.completion = 0.03;
1538    ///
1539    /// let profile = Profile::new("paid-model", "A paid model", 4096)
1540    ///     .with_pricing(pricing);
1541    /// ```
1542    #[must_use]
1543    pub const fn with_pricing(mut self, pricing: Pricing) -> Self {
1544        self.pricing = Some(pricing);
1545        self
1546    }
1547}
1548
1549/// Represents the capabilities that a language model may support.
1550///
1551/// This enum defines the various advanced capabilities that modern language
1552/// models can possess beyond basic text generation. These capabilities can
1553/// be used to determine which models are suitable for specific use cases.
1554///
1555/// # Examples
1556///
1557/// ```rust,ignore
1558/// use aither::llm::model::Ability;
1559///
1560/// // Check if a model supports vision
1561/// let abilities = [Ability::Vision, Ability::ToolUse];
1562/// let has_vision = abilities.contains(&Ability::Vision);
1563/// ```
1564#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1565#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1566#[non_exhaustive]
1567pub enum Ability {
1568    /// The model can use external tools/functions.
1569    ToolUse,
1570    /// The model can process and understand images.
1571    Vision,
1572    /// The model can process and understand audio.
1573    Audio,
1574    /// The model can generate audio output.
1575    AudioOutput,
1576    /// The model can process and understand video.
1577    Video,
1578    /// The model can perform web searches natively.
1579    WebSearch,
1580    /// The model can directly read and reason over PDF or document attachments.
1581    Pdf,
1582    /// The model can execute code.
1583    CodeExecution,
1584    /// The model supports extended thinking/reasoning.
1585    Reasoning,
1586    /// The model can generate images.
1587    ImageGeneration,
1588    /// The model supports computer use / desktop interaction.
1589    ComputerUse,
1590    /// The model supports prompt caching.
1591    PromptCaching,
1592    /// The model supports assistant prefill (pre-filling assistant responses).
1593    AssistantPrefill,
1594}
1595
1596#[cfg(test)]
1597mod tests {
1598    use super::*;
1599
1600    #[test]
1601    fn profile_creation() {
1602        let profile = Profile::new("Test model", "test", "test-model", "A test model", 4096);
1603
1604        assert_eq!(profile.name, "Test model");
1605        assert_eq!(profile.slug, "test-model");
1606        assert_eq!(profile.description, "A test model");
1607        assert_eq!(profile.context_length, 4096);
1608        assert!(
1609            profile.abilities.is_empty(),
1610            "expected no abilities, got {:?}",
1611            profile.abilities
1612        );
1613        assert!(profile.pricing.is_none());
1614    }
1615
1616    #[test]
1617    fn profile_with_single_ability() {
1618        let profile = Profile::new(
1619            "Test vision model",
1620            "test",
1621            "vision-model",
1622            "A vision model",
1623            8192,
1624        )
1625        .with_ability(Ability::Vision);
1626
1627        assert_eq!(profile.abilities.len(), 1);
1628        assert_eq!(profile.abilities[0], Ability::Vision);
1629    }
1630
1631    #[test]
1632    fn profile_with_multiple_abilities() {
1633        let abilities = [Ability::ToolUse, Ability::Vision, Ability::Audio];
1634        let profile = Profile::new(
1635            "Test",
1636            "test",
1637            "multimodal-model",
1638            "A multimodal model",
1639            16384,
1640        )
1641        .with_abilities(abilities);
1642
1643        assert_eq!(profile.abilities.len(), 3);
1644        assert_eq!(profile.abilities, abilities);
1645    }
1646
1647    #[test]
1648    #[allow(clippy::float_cmp)]
1649    fn profile_with_pricing() {
1650        let pricing = Pricing {
1651            prompt: 0.0001,
1652            completion: 0.0002,
1653            request: 0.001,
1654            image: 0.01,
1655            web_search: 0.005,
1656            internal_reasoning: 0.0003,
1657            input_cache_read: 0.00005,
1658            input_cache_write: 0.0001,
1659        };
1660
1661        let profile = Profile::new(
1662            "Test paid model",
1663            "test",
1664            "paid-model",
1665            "A paid model",
1666            2048,
1667        )
1668        .with_pricing(pricing);
1669
1670        assert!(profile.pricing.is_some());
1671        let profile_pricing = profile.pricing.unwrap();
1672        assert_eq!(profile_pricing.prompt, 0.0001);
1673        assert_eq!(profile_pricing.completion, 0.0002);
1674        assert_eq!(profile_pricing.request, 0.001);
1675        assert_eq!(profile_pricing.image, 0.01);
1676        assert_eq!(profile_pricing.web_search, 0.005);
1677        assert_eq!(profile_pricing.internal_reasoning, 0.0003);
1678        assert_eq!(profile_pricing.input_cache_read, 0.00005);
1679        assert_eq!(profile_pricing.input_cache_write, 0.0001);
1680    }
1681
1682    #[test]
1683    fn profile_builder_pattern() {
1684        let pricing = Pricing {
1685            prompt: 0.001,
1686            completion: 0.002,
1687            request: 0.01,
1688            image: 0.1,
1689            web_search: 0.05,
1690            internal_reasoning: 0.003,
1691            input_cache_read: 0.0005,
1692            input_cache_write: 0.001,
1693        };
1694
1695        let profile = Profile::new("Test", "test", "full-model", "A full-featured model", 32768)
1696            .with_ability(Ability::ToolUse)
1697            .with_ability(Ability::Vision)
1698            .with_abilities([Ability::Audio, Ability::WebSearch])
1699            .with_pricing(pricing);
1700
1701        assert_eq!(profile.name, "Test");
1702        assert_eq!(profile.slug, "full-model");
1703        assert_eq!(profile.description, "A full-featured model");
1704        assert_eq!(profile.context_length, 32768);
1705        assert_eq!(profile.abilities.len(), 4);
1706        assert!(profile.abilities.contains(&Ability::ToolUse));
1707        assert!(profile.abilities.contains(&Ability::Vision));
1708        assert!(profile.abilities.contains(&Ability::Audio));
1709        assert!(profile.abilities.contains(&Ability::WebSearch));
1710        assert!(profile.pricing.is_some());
1711    }
1712
1713    #[test]
1714    fn ability_equality() {
1715        assert_eq!(Ability::ToolUse, Ability::ToolUse);
1716        assert_eq!(Ability::Vision, Ability::Vision);
1717        assert_eq!(Ability::Audio, Ability::Audio);
1718        assert_eq!(Ability::WebSearch, Ability::WebSearch);
1719
1720        assert_ne!(Ability::ToolUse, Ability::Vision);
1721        assert_ne!(Ability::Audio, Ability::WebSearch);
1722    }
1723
1724    #[test]
1725    fn ability_debug() {
1726        let ability = Ability::ToolUse;
1727        let debug_str = alloc::format!("{ability:?}");
1728        assert!(debug_str.contains("ToolUse"));
1729    }
1730
1731    #[test]
1732    fn profile_debug() {
1733        let profile = Profile::new("Test model", "test", "debug-model", "A debug model", 1024);
1734        let debug_str = alloc::format!("{profile:?}");
1735        assert!(debug_str.contains("debug-model"));
1736        assert!(debug_str.contains("A debug model"));
1737        assert!(debug_str.contains("1024"));
1738    }
1739
1740    #[test]
1741    fn profile_clone() {
1742        let original = Profile::new("Test model", "test", "original", "Original model", 2048)
1743            .with_ability(Ability::Vision);
1744        let cloned = original.clone();
1745
1746        assert_eq!(original.name, cloned.name);
1747        assert_eq!(original.description, cloned.description);
1748        assert_eq!(original.context_length, cloned.context_length);
1749        assert_eq!(original.abilities, cloned.abilities);
1750    }
1751
1752    #[test]
1753    fn pricing_debug() {
1754        let pricing = Pricing {
1755            prompt: 0.001,
1756            completion: 0.002,
1757            request: 0.01,
1758            image: 0.1,
1759            web_search: 0.05,
1760            internal_reasoning: 0.003,
1761            input_cache_read: 0.0005,
1762            input_cache_write: 0.001,
1763        };
1764
1765        let debug_str = alloc::format!("{pricing:?}");
1766        assert!(debug_str.contains("0.001"));
1767        assert!(debug_str.contains("0.002"));
1768    }
1769
1770    #[test]
1771    #[allow(clippy::float_cmp)]
1772    fn pricing_clone() {
1773        let original = Pricing {
1774            prompt: 0.001,
1775            completion: 0.002,
1776            request: 0.01,
1777            image: 0.1,
1778            web_search: 0.05,
1779            internal_reasoning: 0.003,
1780            input_cache_read: 0.0005,
1781            input_cache_write: 0.001,
1782        };
1783        let cloned = original.clone();
1784
1785        assert_eq!(original.prompt, cloned.prompt);
1786        assert_eq!(original.completion, cloned.completion);
1787        assert_eq!(original.request, cloned.request);
1788        assert_eq!(original.image, cloned.image);
1789        assert_eq!(original.web_search, cloned.web_search);
1790        assert_eq!(original.internal_reasoning, cloned.internal_reasoning);
1791        assert_eq!(original.input_cache_read, cloned.input_cache_read);
1792        assert_eq!(original.input_cache_write, cloned.input_cache_write);
1793    }
1794
1795    #[test]
1796    fn pricing_equality() {
1797        let pricing1 = Pricing {
1798            prompt: 0.001,
1799            completion: 0.002,
1800            request: 0.01,
1801            image: 0.1,
1802            web_search: 0.05,
1803            internal_reasoning: 0.003,
1804            input_cache_read: 0.0005,
1805            input_cache_write: 0.001,
1806        };
1807
1808        let pricing2 = Pricing {
1809            prompt: 0.001,
1810            completion: 0.002,
1811            request: 0.01,
1812            image: 0.1,
1813            web_search: 0.05,
1814            internal_reasoning: 0.003,
1815            input_cache_read: 0.0005,
1816            input_cache_write: 0.001,
1817        };
1818
1819        let pricing3 = Pricing {
1820            prompt: 0.002, // Different value
1821            completion: 0.002,
1822            request: 0.01,
1823            image: 0.1,
1824            web_search: 0.05,
1825            internal_reasoning: 0.003,
1826            input_cache_read: 0.0005,
1827            input_cache_write: 0.001,
1828        };
1829
1830        assert_eq!(pricing1, pricing2);
1831        assert_ne!(pricing1, pricing3);
1832    }
1833
1834    #[test]
1835    fn supported_parameters() {
1836        let params = SupportedParameters {
1837            max_tokens: true,
1838            temperature: true,
1839            top_p: false,
1840            structured_outputs: true,
1841            stop: true,
1842            presence_penalty: true,
1843            ..Default::default()
1844        };
1845
1846        assert!(params.max_tokens);
1847        assert!(params.temperature);
1848        assert!(!params.top_p);
1849    }
1850
1851    #[test]
1852    fn parameters_debug() {
1853        let params = Parameters::default()
1854            .temperature(0.7)
1855            .top_p(0.9)
1856            .top_k(40)
1857            .seed(42)
1858            .max_tokens(1000);
1859
1860        let debug_str = alloc::format!("{params:?}");
1861        assert!(debug_str.contains("0.7"));
1862        assert!(debug_str.contains("42"));
1863        assert!(debug_str.contains("1000"));
1864    }
1865
1866    #[test]
1867    fn parameters_cache_builder_sets_expected_fields() {
1868        let params = Parameters::default()
1869            .prompt_cache_key("project:chat:42")
1870            .prompt_cache_retention(OpenAIPromptCacheRetention::Hours24)
1871            .claude_prompt_cache(ClaudePromptCache::new(ClaudePromptCacheTtl::OneHour))
1872            .gemini_cached_content("cachedContents/session-42");
1873
1874        let openai_cache = params
1875            .cache
1876            .openai
1877            .as_ref()
1878            .expect("openai cache should be set");
1879        assert_eq!(openai_cache.key.as_deref(), Some("project:chat:42"));
1880        assert_eq!(
1881            openai_cache.retention,
1882            Some(OpenAIPromptCacheRetention::Hours24)
1883        );
1884        assert_eq!(
1885            params.cache.claude,
1886            Some(ClaudePromptCache::new(ClaudePromptCacheTtl::OneHour))
1887        );
1888        assert_eq!(
1889            params
1890                .cache
1891                .gemini
1892                .as_ref()
1893                .map(|cache| cache.cached_content.as_str()),
1894            Some("cachedContents/session-42")
1895        );
1896    }
1897
1898    #[test]
1899    fn cache_options_empty_state_changes_with_provider_values() {
1900        let mut cache = CacheOptions::default();
1901        assert!(cache.is_empty());
1902
1903        cache.openai = Some(OpenAIPromptCache::default());
1904        assert_ne!(cache, CacheOptions::default());
1905    }
1906
1907    #[test]
1908    fn prompt_cache_retention_string_values_match_api() {
1909        assert_eq!(OpenAIPromptCacheRetention::InMemory.as_str(), "in-memory");
1910        assert_eq!(OpenAIPromptCacheRetention::Hours24.as_str(), "24h");
1911    }
1912
1913    #[test]
1914    fn claude_prompt_cache_ttl_string_values_match_api() {
1915        assert_eq!(ClaudePromptCacheTtl::FiveMinutes.as_str(), "5m");
1916        assert_eq!(ClaudePromptCacheTtl::OneHour.as_str(), "1h");
1917    }
1918
1919    #[test]
1920    fn claude_cache_default_strategy_is_automatic() {
1921        let cache = ClaudePromptCache::new(ClaudePromptCacheTtl::FiveMinutes);
1922        assert_eq!(cache.strategy, ClaudePromptCacheStrategy::Automatic);
1923    }
1924
1925    #[test]
1926    fn claude_explicit_breakpoints_default_to_messages_only() {
1927        let breakpoints = ClaudeExplicitCacheBreakpoints::default();
1928        assert_eq!(breakpoints.count(), 1);
1929        assert_eq!(
1930            breakpoints.first.target,
1931            ClaudeCacheBreakpointTarget::LastMessage
1932        );
1933        assert!(breakpoints.second.is_none());
1934    }
1935
1936    #[test]
1937    fn claude_prompt_cache_explicit_builder_preserves_breakpoints() {
1938        let breakpoints = ClaudeExplicitCacheBreakpoints::all();
1939        let params = Parameters::default()
1940            .claude_prompt_cache_explicit(ClaudePromptCacheTtl::OneHour, breakpoints);
1941        let cache = params
1942            .cache
1943            .claude
1944            .expect("claude cache should be set by explicit builder");
1945        assert_eq!(cache.ttl, ClaudePromptCacheTtl::OneHour);
1946        assert_eq!(
1947            cache.strategy,
1948            ClaudePromptCacheStrategy::Explicit(breakpoints)
1949        );
1950    }
1951
1952    #[test]
1953    fn claude_explicit_breakpoint_supports_per_block_ttl_override() {
1954        let breakpoint = ClaudeExplicitCacheBreakpoint::new(ClaudeCacheBreakpointTarget::Tool(0))
1955            .with_ttl(ClaudePromptCacheTtl::OneHour);
1956        assert_eq!(breakpoint.ttl, Some(ClaudePromptCacheTtl::OneHour));
1957        assert_eq!(
1958            breakpoint.effective_ttl(ClaudePromptCacheTtl::FiveMinutes),
1959            ClaudePromptCacheTtl::OneHour
1960        );
1961    }
1962
1963    #[test]
1964    fn claude_prompt_cache_automatic_with_explicit_builder_preserves_breakpoints() {
1965        let breakpoints = ClaudeExplicitCacheBreakpoints::messages_only();
1966        let params = Parameters::default().claude_prompt_cache_automatic_with_explicit(
1967            ClaudePromptCacheTtl::FiveMinutes,
1968            breakpoints,
1969        );
1970        let cache = params
1971            .cache
1972            .claude
1973            .expect("claude cache should be set by combined builder");
1974        assert_eq!(
1975            cache.strategy,
1976            ClaudePromptCacheStrategy::AutomaticAndExplicit(breakpoints)
1977        );
1978    }
1979}