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