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