use alloc::{string::String, vec::Vec};
use schemars::Schema;
use serde_json::Value;
#[derive(Debug, Default, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[allow(clippy::struct_excessive_bools)]
pub struct Parameters {
pub temperature: Option<f32>,
pub top_p: Option<f32>,
pub top_k: Option<u32>,
pub frequency_penalty: Option<f32>,
pub presence_penalty: Option<f32>,
pub repetition_penalty: Option<f32>,
pub min_p: Option<f32>,
pub seed: Option<u32>,
pub max_tokens: Option<u32>,
pub logit_bias: Option<Vec<(String, f32)>>,
pub logprobs: Option<bool>,
pub top_logprobs: Option<u8>,
pub stop: Option<Vec<String>>,
pub tool_choice: ToolChoice,
pub parallel_tool_calls: Option<bool>,
pub reasoning_effort: Option<ReasoningEffort>,
pub include_reasoning: bool,
pub structured_outputs: bool,
pub response_format: Option<Schema>,
pub websearch: bool,
pub code_execution: bool,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "NativeTools::is_empty")
)]
pub native_tools: NativeTools,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "CacheOptions::is_empty")
)]
pub cache: CacheOptions,
}
macro_rules! impl_with_methods {
(
impl $ty:ty {
$($field:ident : $field_ty:ty),* $(,)?
}
) => {
impl $ty {
$(
#[allow(clippy::missing_const_for_fn)]
#[must_use] pub fn $field(mut self, value: $field_ty) -> Self {
self.$field = Some(value);
self
}
)*
}
};
}
impl_with_methods! {
impl Parameters {
temperature: f32,
top_p: f32,
top_k: u32,
frequency_penalty: f32,
presence_penalty: f32,
repetition_penalty: f32,
min_p: f32,
seed: u32,
max_tokens: u32,
logit_bias: Vec<(String, f32)>,
logprobs: bool,
top_logprobs: u8,
stop: Vec<String>,
parallel_tool_calls: bool,
}
}
impl Parameters {
#[must_use]
pub const fn include_reasoning(mut self, include: bool) -> Self {
self.include_reasoning = include;
self
}
#[must_use]
pub const fn reasoning_effort(mut self, effort: ReasoningEffort) -> Self {
self.reasoning_effort = Some(effort);
self
}
#[must_use]
pub const fn websearch(mut self, enabled: bool) -> Self {
self.websearch = enabled;
self
}
#[must_use]
pub const fn code_execution(mut self, enabled: bool) -> Self {
self.code_execution = enabled;
self
}
#[must_use]
pub fn native_tools(mut self, tools: NativeTools) -> Self {
self.native_tools = tools;
self
}
#[must_use]
pub fn openai_tools(mut self, tools: OpenAINativeTools) -> Self {
self.native_tools.openai = tools;
self
}
#[must_use]
pub const fn gemini_tools(mut self, tools: GeminiNativeTools) -> Self {
self.native_tools.gemini = tools;
self
}
#[must_use]
pub const fn claude_tools(mut self, tools: ClaudeNativeTools) -> Self {
self.native_tools.claude = tools;
self
}
#[must_use]
pub fn prompt_cache_key(mut self, key: impl Into<String>) -> Self {
let cache = self
.cache
.openai
.get_or_insert_with(OpenAIPromptCache::default);
cache.key = Some(key.into());
self
}
#[must_use]
pub fn prompt_cache_retention(mut self, retention: OpenAIPromptCacheRetention) -> Self {
let cache = self
.cache
.openai
.get_or_insert_with(OpenAIPromptCache::default);
cache.retention = Some(retention);
self
}
#[must_use]
pub const fn claude_prompt_cache(mut self, cache: ClaudePromptCache) -> Self {
self.cache.claude = Some(cache);
self
}
#[must_use]
pub const fn claude_prompt_cache_automatic(mut self, ttl: ClaudePromptCacheTtl) -> Self {
self.cache.claude = Some(ClaudePromptCache::automatic(ttl));
self
}
#[must_use]
pub const fn claude_prompt_cache_explicit(
mut self,
ttl: ClaudePromptCacheTtl,
breakpoints: ClaudeExplicitCacheBreakpoints,
) -> Self {
self.cache.claude = Some(ClaudePromptCache::explicit(ttl, breakpoints));
self
}
#[must_use]
pub const fn claude_prompt_cache_automatic_with_explicit(
mut self,
ttl: ClaudePromptCacheTtl,
breakpoints: ClaudeExplicitCacheBreakpoints,
) -> Self {
self.cache.claude = Some(ClaudePromptCache::automatic_with_explicit(ttl, breakpoints));
self
}
#[must_use]
pub fn gemini_cached_content(mut self, cached_content: impl Into<String>) -> Self {
self.cache.gemini = Some(GeminiPromptCache::new(cached_content));
self
}
#[must_use]
pub fn without_cache(mut self) -> Self {
self.cache = CacheOptions {
openai: None,
claude: None,
gemini: None,
};
self
}
#[must_use]
pub fn tool_choice(mut self, choice: ToolChoice) -> Self {
self.tool_choice = choice;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NativeTools {
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "OpenAINativeTools::is_empty")
)]
pub openai: OpenAINativeTools,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "GeminiNativeTools::is_empty")
)]
pub gemini: GeminiNativeTools,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "ClaudeNativeTools::is_empty")
)]
pub claude: ClaudeNativeTools,
}
impl NativeTools {
#[must_use]
pub const fn is_empty(&self) -> bool {
self.openai.is_empty() && self.gemini.is_empty() && self.claude.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OpenAINativeTools {
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub web_search: Option<OpenAIWebSearchTool>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Vec::is_empty")
)]
pub file_search: Vec<OpenAIFileSearchTool>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub code_interpreter: Option<OpenAICodeInterpreterTool>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub image_generation: Option<OpenAIImageGenerationTool>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Vec::is_empty")
)]
pub mcp: Vec<OpenAIMcpTool>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub computer_use: Option<OpenAIComputerUseTool>,
}
impl OpenAINativeTools {
#[must_use]
pub const fn is_empty(&self) -> bool {
self.web_search.is_none()
&& self.file_search.is_empty()
&& self.code_interpreter.is_none()
&& self.image_generation.is_none()
&& self.mcp.is_empty()
&& self.computer_use.is_none()
}
#[must_use]
pub fn with_web_search(mut self, tool: OpenAIWebSearchTool) -> Self {
self.web_search = Some(tool);
self
}
#[must_use]
pub fn enable_web_search(mut self) -> Self {
self.web_search = Some(OpenAIWebSearchTool::default());
self
}
#[must_use]
pub fn with_file_search(mut self, tool: OpenAIFileSearchTool) -> Self {
self.file_search.push(tool);
self
}
#[must_use]
pub fn with_code_interpreter(mut self, tool: OpenAICodeInterpreterTool) -> Self {
self.code_interpreter = Some(tool);
self
}
#[must_use]
pub const fn with_image_generation(mut self, tool: OpenAIImageGenerationTool) -> Self {
self.image_generation = Some(tool);
self
}
#[must_use]
pub fn with_mcp(mut self, tool: OpenAIMcpTool) -> Self {
self.mcp.push(tool);
self
}
#[must_use]
pub fn with_computer_use(mut self, tool: OpenAIComputerUseTool) -> Self {
self.computer_use = Some(tool);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OpenAIWebSearchTool {
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub external_web_access: Option<bool>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub filters: Option<Value>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub user_location: Option<Value>,
}
impl OpenAIWebSearchTool {
#[must_use]
pub const fn external_web_access(mut self, allowed: bool) -> Self {
self.external_web_access = Some(allowed);
self
}
#[must_use]
pub fn filters(mut self, filters: Value) -> Self {
self.filters = Some(filters);
self
}
#[must_use]
pub fn user_location(mut self, location: Value) -> Self {
self.user_location = Some(location);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OpenAIFileSearchTool {
pub vector_store_ids: Vec<String>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub max_num_results: Option<u32>,
pub include_results: bool,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub filters: Option<Value>,
}
impl OpenAIFileSearchTool {
#[must_use]
pub fn new(vector_store_ids: impl Into<Vec<String>>) -> Self {
Self {
vector_store_ids: vector_store_ids.into(),
max_num_results: None,
include_results: false,
filters: None,
}
}
#[must_use]
pub const fn max_num_results(mut self, value: u32) -> Self {
self.max_num_results = Some(value);
self
}
#[must_use]
pub const fn include_results(mut self, include: bool) -> Self {
self.include_results = include;
self
}
#[must_use]
pub fn filters(mut self, filters: Value) -> Self {
self.filters = Some(filters);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OpenAICodeInterpreterTool {
pub container: OpenAICodeInterpreterContainer,
}
impl Default for OpenAICodeInterpreterTool {
fn default() -> Self {
Self {
container: OpenAICodeInterpreterContainer::Auto(OpenAIAutoContainer::default()),
}
}
}
impl OpenAICodeInterpreterTool {
#[must_use]
pub const fn auto() -> Self {
Self {
container: OpenAICodeInterpreterContainer::Auto(OpenAIAutoContainer::new()),
}
}
#[must_use]
pub fn existing(container_id: impl Into<String>) -> Self {
Self {
container: OpenAICodeInterpreterContainer::Existing(container_id.into()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum OpenAICodeInterpreterContainer {
Auto(OpenAIAutoContainer),
Existing(String),
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OpenAIAutoContainer {
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub memory_limit: Option<String>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Vec::is_empty")
)]
pub file_ids: Vec<String>,
}
impl OpenAIAutoContainer {
#[must_use]
pub const fn new() -> Self {
Self {
memory_limit: None,
file_ids: Vec::new(),
}
}
#[must_use]
pub fn memory_limit(mut self, memory_limit: impl Into<String>) -> Self {
self.memory_limit = Some(memory_limit.into());
self
}
#[must_use]
pub fn file_ids(mut self, file_ids: impl Into<Vec<String>>) -> Self {
self.file_ids = file_ids.into();
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OpenAIImageGenerationTool {
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub partial_images: Option<u8>,
}
impl OpenAIImageGenerationTool {
#[must_use]
pub const fn partial_images(mut self, count: u8) -> Self {
self.partial_images = Some(count);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OpenAIMcpTool {
pub server_label: String,
pub server_url: String,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub require_approval: Option<String>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Vec::is_empty")
)]
pub allowed_tools: Vec<String>,
}
impl OpenAIMcpTool {
#[must_use]
pub fn new(server_label: impl Into<String>, server_url: impl Into<String>) -> Self {
Self {
server_label: server_label.into(),
server_url: server_url.into(),
require_approval: None,
allowed_tools: Vec::new(),
}
}
#[must_use]
pub fn require_approval(mut self, policy: impl Into<String>) -> Self {
self.require_approval = Some(policy.into());
self
}
#[must_use]
pub fn allowed_tools(mut self, tools: impl Into<Vec<String>>) -> Self {
self.allowed_tools = tools.into();
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OpenAIComputerUseTool {
pub display_width: u32,
pub display_height: u32,
pub environment: String,
}
impl OpenAIComputerUseTool {
#[must_use]
pub fn new(display_width: u32, display_height: u32, environment: impl Into<String>) -> Self {
Self {
display_width,
display_height,
environment: environment.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GeminiNativeTools {
pub google_search: bool,
pub code_execution: bool,
pub url_context: bool,
}
impl GeminiNativeTools {
#[must_use]
pub const fn is_empty(&self) -> bool {
!self.google_search && !self.code_execution && !self.url_context
}
#[must_use]
pub const fn google_search(mut self, enabled: bool) -> Self {
self.google_search = enabled;
self
}
#[must_use]
pub const fn code_execution(mut self, enabled: bool) -> Self {
self.code_execution = enabled;
self
}
#[must_use]
pub const fn url_context(mut self, enabled: bool) -> Self {
self.url_context = enabled;
self
}
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ClaudeNativeTools {
pub web_search: bool,
pub web_fetch: bool,
pub code_execution: bool,
pub bash: bool,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub text_editor: Option<ClaudeTextEditorTool>,
}
impl ClaudeNativeTools {
#[must_use]
pub const fn is_empty(&self) -> bool {
!self.web_search
&& !self.web_fetch
&& !self.code_execution
&& !self.bash
&& self.text_editor.is_none()
}
#[must_use]
pub const fn web_search(mut self, enabled: bool) -> Self {
self.web_search = enabled;
self
}
#[must_use]
pub const fn web_fetch(mut self, enabled: bool) -> Self {
self.web_fetch = enabled;
self
}
#[must_use]
pub const fn code_execution(mut self, enabled: bool) -> Self {
self.code_execution = enabled;
self
}
#[must_use]
pub const fn bash(mut self, enabled: bool) -> Self {
self.bash = enabled;
self
}
#[must_use]
pub const fn text_editor(mut self, tool: ClaudeTextEditorTool) -> Self {
self.text_editor = Some(tool);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ClaudeTextEditorTool {
pub max_characters: Option<u32>,
}
impl ClaudeTextEditorTool {
#[must_use]
pub const fn max_characters(mut self, value: u32) -> Self {
self.max_characters = Some(value);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum ToolChoice {
#[default]
Auto,
None,
Required,
Exact(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
pub enum ReasoningEffort {
None,
Minimal,
Low,
Medium,
High,
XHigh,
Max,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CacheOptions {
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub openai: Option<OpenAIPromptCache>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub claude: Option<ClaudePromptCache>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub gemini: Option<GeminiPromptCache>,
}
impl CacheOptions {
#[must_use]
pub const fn is_empty(&self) -> bool {
self.openai.is_none() && self.claude.is_none() && self.gemini.is_none()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OpenAIPromptCache {
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub key: Option<String>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub retention: Option<OpenAIPromptCacheRetention>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
pub enum OpenAIPromptCacheRetention {
InMemory,
#[cfg_attr(feature = "serde", serde(rename = "24h"))]
Hours24,
}
impl OpenAIPromptCacheRetention {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::InMemory => "in-memory",
Self::Hours24 => "24h",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ClaudePromptCache {
pub ttl: ClaudePromptCacheTtl,
pub strategy: ClaudePromptCacheStrategy,
}
impl ClaudePromptCache {
#[must_use]
pub const fn new(ttl: ClaudePromptCacheTtl) -> Self {
Self {
ttl,
strategy: ClaudePromptCacheStrategy::Automatic,
}
}
#[must_use]
pub const fn automatic(ttl: ClaudePromptCacheTtl) -> Self {
Self::new(ttl)
}
#[must_use]
pub const fn explicit(
ttl: ClaudePromptCacheTtl,
breakpoints: ClaudeExplicitCacheBreakpoints,
) -> Self {
Self {
ttl,
strategy: ClaudePromptCacheStrategy::Explicit(breakpoints),
}
}
#[must_use]
pub const fn automatic_with_explicit(
ttl: ClaudePromptCacheTtl,
breakpoints: ClaudeExplicitCacheBreakpoints,
) -> Self {
Self {
ttl,
strategy: ClaudePromptCacheStrategy::AutomaticAndExplicit(breakpoints),
}
}
#[must_use]
pub const fn with_strategy(mut self, strategy: ClaudePromptCacheStrategy) -> Self {
self.strategy = strategy;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ClaudePromptCacheTtl {
#[default]
FiveMinutes,
OneHour,
}
impl ClaudePromptCacheTtl {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::FiveMinutes => "5m",
Self::OneHour => "1h",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ClaudePromptCacheStrategy {
#[default]
Automatic,
Explicit(ClaudeExplicitCacheBreakpoints),
AutomaticAndExplicit(ClaudeExplicitCacheBreakpoints),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ClaudeExplicitCacheBreakpoint {
pub target: ClaudeCacheBreakpointTarget,
pub ttl: Option<ClaudePromptCacheTtl>,
}
impl ClaudeExplicitCacheBreakpoint {
#[must_use]
pub const fn new(target: ClaudeCacheBreakpointTarget) -> Self {
Self { target, ttl: None }
}
#[must_use]
pub const fn with_ttl(mut self, ttl: ClaudePromptCacheTtl) -> Self {
self.ttl = Some(ttl);
self
}
#[must_use]
pub const fn effective_ttl(self, default_ttl: ClaudePromptCacheTtl) -> ClaudePromptCacheTtl {
match self.ttl {
Some(ttl) => ttl,
None => default_ttl,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ClaudeCacheBreakpointTarget {
LastTool,
Tool(usize),
LastSystem,
System(usize),
LastMessage,
Message {
message_index: usize,
block_index: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ClaudeExplicitCacheBreakpoints {
pub first: ClaudeExplicitCacheBreakpoint,
pub second: Option<ClaudeExplicitCacheBreakpoint>,
pub third: Option<ClaudeExplicitCacheBreakpoint>,
pub fourth: Option<ClaudeExplicitCacheBreakpoint>,
}
impl ClaudeExplicitCacheBreakpoints {
#[must_use]
pub const fn new(first: ClaudeExplicitCacheBreakpoint) -> Self {
Self {
first,
second: None,
third: None,
fourth: None,
}
}
#[must_use]
pub const fn with_second(mut self, second: ClaudeExplicitCacheBreakpoint) -> Self {
self.second = Some(second);
self
}
#[must_use]
pub const fn with_third(mut self, third: ClaudeExplicitCacheBreakpoint) -> Self {
self.third = Some(third);
self
}
#[must_use]
pub const fn with_fourth(mut self, fourth: ClaudeExplicitCacheBreakpoint) -> Self {
self.fourth = Some(fourth);
self
}
pub fn iter(self) -> impl Iterator<Item = ClaudeExplicitCacheBreakpoint> {
[Some(self.first), self.second, self.third, self.fourth]
.into_iter()
.flatten()
}
#[must_use]
pub const fn count(&self) -> usize {
1 + self.second.is_some() as usize
+ self.third.is_some() as usize
+ self.fourth.is_some() as usize
}
#[must_use]
pub const fn is_full(&self) -> bool {
self.fourth.is_some()
}
#[must_use]
pub const fn messages_only() -> Self {
Self::new(ClaudeExplicitCacheBreakpoint::new(
ClaudeCacheBreakpointTarget::LastMessage,
))
}
#[must_use]
pub const fn all() -> Self {
Self::new(ClaudeExplicitCacheBreakpoint::new(
ClaudeCacheBreakpointTarget::LastTool,
))
.with_second(ClaudeExplicitCacheBreakpoint::new(
ClaudeCacheBreakpointTarget::LastSystem,
))
.with_third(ClaudeExplicitCacheBreakpoint::new(
ClaudeCacheBreakpointTarget::LastMessage,
))
}
}
impl Default for ClaudeExplicitCacheBreakpoints {
fn default() -> Self {
Self::messages_only()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GeminiPromptCache {
pub cached_content: String,
}
impl GeminiPromptCache {
#[must_use]
pub fn new(cached_content: impl Into<String>) -> Self {
Self {
cached_content: cached_content.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, PartialOrd)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct Profile {
pub name: String,
pub author: String,
pub slug: String,
pub description: String,
pub abilities: Vec<Ability>,
pub context_length: u32,
pub pricing: Option<Pricing>,
}
#[derive(Debug, Clone, PartialEq, PartialOrd, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct Pricing {
pub prompt: f64,
pub completion: f64,
pub request: f64,
pub image: f64,
pub web_search: f64,
pub internal_reasoning: f64,
pub input_cache_read: f64,
pub input_cache_write: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[allow(clippy::struct_excessive_bools)]
#[non_exhaustive]
pub struct SupportedParameters {
pub max_tokens: bool,
pub temperature: bool,
pub top_p: bool,
pub reasoning: bool,
pub include_reasoning: bool,
pub structured_outputs: bool,
pub response_format: bool,
pub stop: bool,
pub frequency_penalty: bool,
pub presence_penalty: bool,
pub seed: bool,
}
impl Profile {
pub fn new(
name: impl Into<String>,
author: impl Into<String>,
slug: impl Into<String>,
description: impl Into<String>,
context_length: u32,
) -> Self {
Self {
name: name.into(),
author: author.into(),
slug: slug.into(),
description: description.into(),
abilities: Vec::new(),
context_length,
pricing: None,
}
}
#[must_use]
pub fn with_ability(self, ability: Ability) -> Self {
self.with_abilities([ability])
}
#[must_use]
pub fn with_abilities(mut self, abilities: impl IntoIterator<Item = Ability>) -> Self {
self.abilities.extend(abilities);
self
}
#[must_use]
pub const fn with_pricing(mut self, pricing: Pricing) -> Self {
self.pricing = Some(pricing);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Ability {
ToolUse,
Vision,
Audio,
AudioOutput,
Video,
WebSearch,
Pdf,
CodeExecution,
Reasoning,
ImageGeneration,
ComputerUse,
PromptCaching,
AssistantPrefill,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn profile_creation() {
let profile = Profile::new("Test model", "test", "test-model", "A test model", 4096);
assert_eq!(profile.name, "Test model");
assert_eq!(profile.slug, "test-model");
assert_eq!(profile.description, "A test model");
assert_eq!(profile.context_length, 4096);
assert!(
profile.abilities.is_empty(),
"expected no abilities, got {:?}",
profile.abilities
);
assert!(profile.pricing.is_none());
}
#[test]
fn profile_with_single_ability() {
let profile = Profile::new(
"Test vision model",
"test",
"vision-model",
"A vision model",
8192,
)
.with_ability(Ability::Vision);
assert_eq!(profile.abilities.len(), 1);
assert_eq!(profile.abilities[0], Ability::Vision);
}
#[test]
fn profile_with_multiple_abilities() {
let abilities = [Ability::ToolUse, Ability::Vision, Ability::Audio];
let profile = Profile::new(
"Test",
"test",
"multimodal-model",
"A multimodal model",
16384,
)
.with_abilities(abilities);
assert_eq!(profile.abilities.len(), 3);
assert_eq!(profile.abilities, abilities);
}
#[test]
#[allow(clippy::float_cmp)]
fn profile_with_pricing() {
let pricing = Pricing {
prompt: 0.0001,
completion: 0.0002,
request: 0.001,
image: 0.01,
web_search: 0.005,
internal_reasoning: 0.0003,
input_cache_read: 0.00005,
input_cache_write: 0.0001,
};
let profile = Profile::new(
"Test paid model",
"test",
"paid-model",
"A paid model",
2048,
)
.with_pricing(pricing);
assert!(profile.pricing.is_some());
let profile_pricing = profile.pricing.unwrap();
assert_eq!(profile_pricing.prompt, 0.0001);
assert_eq!(profile_pricing.completion, 0.0002);
assert_eq!(profile_pricing.request, 0.001);
assert_eq!(profile_pricing.image, 0.01);
assert_eq!(profile_pricing.web_search, 0.005);
assert_eq!(profile_pricing.internal_reasoning, 0.0003);
assert_eq!(profile_pricing.input_cache_read, 0.00005);
assert_eq!(profile_pricing.input_cache_write, 0.0001);
}
#[test]
fn profile_builder_pattern() {
let pricing = Pricing {
prompt: 0.001,
completion: 0.002,
request: 0.01,
image: 0.1,
web_search: 0.05,
internal_reasoning: 0.003,
input_cache_read: 0.0005,
input_cache_write: 0.001,
};
let profile = Profile::new("Test", "test", "full-model", "A full-featured model", 32768)
.with_ability(Ability::ToolUse)
.with_ability(Ability::Vision)
.with_abilities([Ability::Audio, Ability::WebSearch])
.with_pricing(pricing);
assert_eq!(profile.name, "Test");
assert_eq!(profile.slug, "full-model");
assert_eq!(profile.description, "A full-featured model");
assert_eq!(profile.context_length, 32768);
assert_eq!(profile.abilities.len(), 4);
assert!(profile.abilities.contains(&Ability::ToolUse));
assert!(profile.abilities.contains(&Ability::Vision));
assert!(profile.abilities.contains(&Ability::Audio));
assert!(profile.abilities.contains(&Ability::WebSearch));
assert!(profile.pricing.is_some());
}
#[test]
fn ability_equality() {
assert_eq!(Ability::ToolUse, Ability::ToolUse);
assert_eq!(Ability::Vision, Ability::Vision);
assert_eq!(Ability::Audio, Ability::Audio);
assert_eq!(Ability::WebSearch, Ability::WebSearch);
assert_ne!(Ability::ToolUse, Ability::Vision);
assert_ne!(Ability::Audio, Ability::WebSearch);
}
#[test]
fn ability_debug() {
let ability = Ability::ToolUse;
let debug_str = alloc::format!("{ability:?}");
assert!(debug_str.contains("ToolUse"));
}
#[test]
fn profile_debug() {
let profile = Profile::new("Test model", "test", "debug-model", "A debug model", 1024);
let debug_str = alloc::format!("{profile:?}");
assert!(debug_str.contains("debug-model"));
assert!(debug_str.contains("A debug model"));
assert!(debug_str.contains("1024"));
}
#[test]
fn profile_clone() {
let original = Profile::new("Test model", "test", "original", "Original model", 2048)
.with_ability(Ability::Vision);
let cloned = original.clone();
assert_eq!(original.name, cloned.name);
assert_eq!(original.description, cloned.description);
assert_eq!(original.context_length, cloned.context_length);
assert_eq!(original.abilities, cloned.abilities);
}
#[test]
fn pricing_debug() {
let pricing = Pricing {
prompt: 0.001,
completion: 0.002,
request: 0.01,
image: 0.1,
web_search: 0.05,
internal_reasoning: 0.003,
input_cache_read: 0.0005,
input_cache_write: 0.001,
};
let debug_str = alloc::format!("{pricing:?}");
assert!(debug_str.contains("0.001"));
assert!(debug_str.contains("0.002"));
}
#[test]
#[allow(clippy::float_cmp)]
fn pricing_clone() {
let original = Pricing {
prompt: 0.001,
completion: 0.002,
request: 0.01,
image: 0.1,
web_search: 0.05,
internal_reasoning: 0.003,
input_cache_read: 0.0005,
input_cache_write: 0.001,
};
let cloned = original.clone();
assert_eq!(original.prompt, cloned.prompt);
assert_eq!(original.completion, cloned.completion);
assert_eq!(original.request, cloned.request);
assert_eq!(original.image, cloned.image);
assert_eq!(original.web_search, cloned.web_search);
assert_eq!(original.internal_reasoning, cloned.internal_reasoning);
assert_eq!(original.input_cache_read, cloned.input_cache_read);
assert_eq!(original.input_cache_write, cloned.input_cache_write);
}
#[test]
fn pricing_equality() {
let pricing1 = Pricing {
prompt: 0.001,
completion: 0.002,
request: 0.01,
image: 0.1,
web_search: 0.05,
internal_reasoning: 0.003,
input_cache_read: 0.0005,
input_cache_write: 0.001,
};
let pricing2 = Pricing {
prompt: 0.001,
completion: 0.002,
request: 0.01,
image: 0.1,
web_search: 0.05,
internal_reasoning: 0.003,
input_cache_read: 0.0005,
input_cache_write: 0.001,
};
let pricing3 = Pricing {
prompt: 0.002, completion: 0.002,
request: 0.01,
image: 0.1,
web_search: 0.05,
internal_reasoning: 0.003,
input_cache_read: 0.0005,
input_cache_write: 0.001,
};
assert_eq!(pricing1, pricing2);
assert_ne!(pricing1, pricing3);
}
#[test]
fn supported_parameters() {
let params = SupportedParameters {
max_tokens: true,
temperature: true,
top_p: false,
structured_outputs: true,
stop: true,
presence_penalty: true,
..Default::default()
};
assert!(params.max_tokens);
assert!(params.temperature);
assert!(!params.top_p);
}
#[test]
fn parameters_debug() {
let params = Parameters::default()
.temperature(0.7)
.top_p(0.9)
.top_k(40)
.seed(42)
.max_tokens(1000);
let debug_str = alloc::format!("{params:?}");
assert!(debug_str.contains("0.7"));
assert!(debug_str.contains("42"));
assert!(debug_str.contains("1000"));
}
#[test]
fn parameters_cache_builder_sets_expected_fields() {
let params = Parameters::default()
.prompt_cache_key("project:chat:42")
.prompt_cache_retention(OpenAIPromptCacheRetention::Hours24)
.claude_prompt_cache(ClaudePromptCache::new(ClaudePromptCacheTtl::OneHour))
.gemini_cached_content("cachedContents/session-42");
let openai_cache = params
.cache
.openai
.as_ref()
.expect("openai cache should be set");
assert_eq!(openai_cache.key.as_deref(), Some("project:chat:42"));
assert_eq!(
openai_cache.retention,
Some(OpenAIPromptCacheRetention::Hours24)
);
assert_eq!(
params.cache.claude,
Some(ClaudePromptCache::new(ClaudePromptCacheTtl::OneHour))
);
assert_eq!(
params
.cache
.gemini
.as_ref()
.map(|cache| cache.cached_content.as_str()),
Some("cachedContents/session-42")
);
}
#[test]
fn cache_options_empty_state_changes_with_provider_values() {
let mut cache = CacheOptions::default();
assert!(cache.is_empty());
cache.openai = Some(OpenAIPromptCache::default());
assert!(!cache.is_empty());
}
#[test]
fn prompt_cache_retention_string_values_match_api() {
assert_eq!(OpenAIPromptCacheRetention::InMemory.as_str(), "in-memory");
assert_eq!(OpenAIPromptCacheRetention::Hours24.as_str(), "24h");
}
#[test]
fn claude_prompt_cache_ttl_string_values_match_api() {
assert_eq!(ClaudePromptCacheTtl::FiveMinutes.as_str(), "5m");
assert_eq!(ClaudePromptCacheTtl::OneHour.as_str(), "1h");
}
#[test]
fn claude_cache_default_strategy_is_automatic() {
let cache = ClaudePromptCache::new(ClaudePromptCacheTtl::FiveMinutes);
assert_eq!(cache.strategy, ClaudePromptCacheStrategy::Automatic);
}
#[test]
fn claude_explicit_breakpoints_default_to_messages_only() {
let breakpoints = ClaudeExplicitCacheBreakpoints::default();
assert_eq!(breakpoints.count(), 1);
assert_eq!(
breakpoints.first.target,
ClaudeCacheBreakpointTarget::LastMessage
);
assert!(breakpoints.second.is_none());
}
#[test]
fn claude_prompt_cache_explicit_builder_preserves_breakpoints() {
let breakpoints = ClaudeExplicitCacheBreakpoints::all();
let params = Parameters::default()
.claude_prompt_cache_explicit(ClaudePromptCacheTtl::OneHour, breakpoints);
let cache = params
.cache
.claude
.expect("claude cache should be set by explicit builder");
assert_eq!(cache.ttl, ClaudePromptCacheTtl::OneHour);
assert_eq!(
cache.strategy,
ClaudePromptCacheStrategy::Explicit(breakpoints)
);
}
#[test]
fn claude_explicit_breakpoint_supports_per_block_ttl_override() {
let breakpoint = ClaudeExplicitCacheBreakpoint::new(ClaudeCacheBreakpointTarget::Tool(0))
.with_ttl(ClaudePromptCacheTtl::OneHour);
assert_eq!(breakpoint.ttl, Some(ClaudePromptCacheTtl::OneHour));
assert_eq!(
breakpoint.effective_ttl(ClaudePromptCacheTtl::FiveMinutes),
ClaudePromptCacheTtl::OneHour
);
}
#[test]
fn claude_prompt_cache_automatic_with_explicit_builder_preserves_breakpoints() {
let breakpoints = ClaudeExplicitCacheBreakpoints::messages_only();
let params = Parameters::default().claude_prompt_cache_automatic_with_explicit(
ClaudePromptCacheTtl::FiveMinutes,
breakpoints,
);
let cache = params
.cache
.claude
.expect("claude cache should be set by combined builder");
assert_eq!(
cache.strategy,
ClaudePromptCacheStrategy::AutomaticAndExplicit(breakpoints)
);
}
}