use std::sync::Arc;
use crate::ir::{ReasoningEffort, ResponseFormat, SystemPrompt, ToolChoice, ToolSpec};
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct RunOverrides {
model: Option<String>,
system_prompt: Option<SystemPrompt>,
max_iterations: Option<usize>,
tool_specs: Option<Arc<[ToolSpec]>>,
}
impl RunOverrides {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
#[must_use]
pub fn with_system_prompt(mut self, prompt: SystemPrompt) -> Self {
self.system_prompt = Some(prompt);
self
}
#[must_use]
pub const fn with_max_iterations(mut self, n: usize) -> Self {
self.max_iterations = Some(n);
self
}
#[must_use]
pub fn with_tool_specs(mut self, specs: impl Into<Arc<[ToolSpec]>>) -> Self {
self.tool_specs = Some(specs.into());
self
}
#[must_use]
pub fn model(&self) -> Option<&str> {
self.model.as_deref()
}
#[must_use]
pub const fn system_prompt(&self) -> Option<&SystemPrompt> {
self.system_prompt.as_ref()
}
#[must_use]
pub const fn max_iterations(&self) -> Option<usize> {
self.max_iterations
}
#[must_use]
pub fn tool_specs(&self) -> Option<&Arc<[ToolSpec]>> {
self.tool_specs.as_ref()
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct RequestOverrides {
temperature: Option<f32>,
top_p: Option<f32>,
top_k: Option<u32>,
max_tokens: Option<u32>,
stop_sequences: Option<Vec<String>>,
reasoning_effort: Option<ReasoningEffort>,
tool_choice: Option<ToolChoice>,
response_format: Option<ResponseFormat>,
parallel_tool_calls: Option<bool>,
end_user_id: Option<String>,
seed: Option<i64>,
}
impl RequestOverrides {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn with_temperature(mut self, t: f32) -> Self {
self.temperature = Some(t);
self
}
#[must_use]
pub const fn with_top_p(mut self, p: f32) -> Self {
self.top_p = Some(p);
self
}
#[must_use]
pub const fn with_top_k(mut self, k: u32) -> Self {
self.top_k = Some(k);
self
}
#[must_use]
pub const fn with_max_tokens(mut self, n: u32) -> Self {
self.max_tokens = Some(n);
self
}
#[must_use]
pub fn with_stop_sequences(mut self, sequences: Vec<String>) -> Self {
self.stop_sequences = Some(sequences);
self
}
#[must_use]
pub fn with_reasoning_effort(mut self, effort: ReasoningEffort) -> Self {
self.reasoning_effort = Some(effort);
self
}
#[must_use]
pub fn with_tool_choice(mut self, choice: ToolChoice) -> Self {
self.tool_choice = Some(choice);
self
}
#[must_use]
pub fn with_response_format(mut self, format: ResponseFormat) -> Self {
self.response_format = Some(format);
self
}
#[must_use]
pub const fn with_parallel_tool_calls(mut self, enabled: bool) -> Self {
self.parallel_tool_calls = Some(enabled);
self
}
#[must_use]
pub fn with_end_user_id(mut self, user_id: impl Into<String>) -> Self {
self.end_user_id = Some(user_id.into());
self
}
#[must_use]
pub const fn with_seed(mut self, seed: i64) -> Self {
self.seed = Some(seed);
self
}
#[must_use]
pub const fn temperature(&self) -> Option<f32> {
self.temperature
}
#[must_use]
pub const fn top_p(&self) -> Option<f32> {
self.top_p
}
#[must_use]
pub const fn top_k(&self) -> Option<u32> {
self.top_k
}
#[must_use]
pub const fn max_tokens(&self) -> Option<u32> {
self.max_tokens
}
#[must_use]
pub fn stop_sequences(&self) -> Option<&[String]> {
self.stop_sequences.as_deref()
}
#[must_use]
pub const fn reasoning_effort(&self) -> Option<&ReasoningEffort> {
self.reasoning_effort.as_ref()
}
#[must_use]
pub const fn tool_choice(&self) -> Option<&ToolChoice> {
self.tool_choice.as_ref()
}
#[must_use]
pub const fn response_format(&self) -> Option<&ResponseFormat> {
self.response_format.as_ref()
}
#[must_use]
pub const fn parallel_tool_calls(&self) -> Option<bool> {
self.parallel_tool_calls
}
#[must_use]
pub fn end_user_id(&self) -> Option<&str> {
self.end_user_id.as_deref()
}
#[must_use]
pub const fn seed(&self) -> Option<i64> {
self.seed
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn run_overrides_empty() {
let o = RunOverrides::new();
assert!(o.model().is_none());
assert!(o.system_prompt().is_none());
assert!(o.max_iterations().is_none());
}
#[test]
fn run_overrides_setters_chain() {
let o = RunOverrides::new()
.with_model("haiku")
.with_system_prompt(SystemPrompt::text("be brief"))
.with_max_iterations(8);
assert_eq!(o.model(), Some("haiku"));
assert!(o.system_prompt().is_some());
assert_eq!(o.max_iterations(), Some(8));
}
#[test]
fn request_overrides_empty() {
let r = RequestOverrides::new();
assert!(r.temperature().is_none());
assert!(r.top_p().is_none());
assert!(r.max_tokens().is_none());
assert!(r.stop_sequences().is_none());
assert!(r.reasoning_effort().is_none());
assert!(r.tool_choice().is_none());
assert!(r.response_format().is_none());
assert!(r.end_user_id().is_none());
assert!(r.seed().is_none());
}
#[test]
fn request_overrides_setters_chain() {
let format = ResponseFormat::strict(
crate::ir::JsonSchemaSpec::new("answer", serde_json::json!({"type": "object"}))
.expect("valid schema"),
);
let r = RequestOverrides::new()
.with_temperature(0.3)
.with_top_p(0.9)
.with_max_tokens(512)
.with_stop_sequences(vec!["</done>".into()])
.with_reasoning_effort(ReasoningEffort::Low)
.with_tool_choice(ToolChoice::Required)
.with_response_format(format)
.with_end_user_id("op-7")
.with_seed(42);
assert_eq!(r.temperature(), Some(0.3));
assert_eq!(r.top_p(), Some(0.9));
assert_eq!(r.max_tokens(), Some(512));
assert_eq!(r.stop_sequences(), Some(&["</done>".to_string()][..]));
assert_eq!(r.end_user_id(), Some("op-7"));
assert_eq!(r.seed(), Some(42));
assert!(matches!(r.reasoning_effort(), Some(&ReasoningEffort::Low)));
assert!(matches!(r.tool_choice(), Some(&ToolChoice::Required)));
assert_eq!(r.response_format().expect("set").json_schema.name, "answer");
}
#[test]
fn request_overrides_stop_sequences_empty_vs_none() {
let cleared = RequestOverrides::new().with_stop_sequences(Vec::new());
assert_eq!(cleared.stop_sequences(), Some(&[][..]));
let unset = RequestOverrides::new();
assert!(unset.stop_sequences().is_none());
}
}