use crate::chat::message::Message;
use crate::model::ModelId;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ToolSchema {
pub name: String,
pub description: String,
pub parameters: serde_json::Value,
}
impl ToolSchema {
pub fn new(
name: impl Into<String>,
description: impl Into<String>,
parameters: serde_json::Value,
) -> Self {
Self {
name: name.into(),
description: description.into(),
parameters,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Effort {
Low,
Medium,
High,
XHigh,
Max,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Thinking {
#[default]
Unset,
Off,
On(Effort),
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Generation {
pub temperature: Option<f32>,
pub top_p: Option<f32>,
pub max_tokens: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ChatRequest {
pub model: ModelId,
pub system: Option<String>,
pub messages: Vec<Message>,
pub tools: Vec<ToolSchema>,
pub generation: Generation,
pub thinking: Thinking,
pub response_schema: Option<serde_json::Value>,
pub cache_breakpoints: Vec<usize>,
}
impl ChatRequest {
pub fn new(model: impl Into<ModelId>, messages: Vec<Message>) -> Self {
Self {
model: model.into(),
system: None,
messages,
tools: Vec::new(),
generation: Generation::default(),
thinking: Thinking::Unset,
response_schema: None,
cache_breakpoints: Vec::new(),
}
}
#[must_use]
pub fn with_system(mut self, system: impl Into<String>) -> Self {
self.system = Some(system.into());
self
}
#[must_use]
pub fn with_tools(mut self, tools: Vec<ToolSchema>) -> Self {
self.tools = tools;
self
}
#[must_use]
pub fn with_max_tokens(mut self, max: u32) -> Self {
self.generation.max_tokens = Some(max);
self
}
#[must_use]
pub fn with_temperature(mut self, temperature: f32) -> Self {
self.generation.temperature = Some(temperature);
self
}
#[must_use]
pub fn with_thinking(mut self, thinking: Thinking) -> Self {
self.thinking = thinking;
self
}
#[must_use]
pub fn with_response_schema(mut self, schema: serde_json::Value) -> Self {
self.response_schema = Some(schema);
self
}
#[must_use]
pub fn with_cache_breakpoint(mut self, after_message: usize) -> Self {
self.cache_breakpoints.push(after_message);
self
}
pub fn needs(&self) -> Needs {
Needs {
tools: !self.tools.is_empty(),
structured_output: self.response_schema.is_some(),
prompt_caching: !self.cache_breakpoints.is_empty(),
thinking: matches!(self.thinking, Thinking::On(_)),
images: self.messages.iter().any(|message| {
message
.content
.iter()
.any(|block| matches!(block, crate::chat::message::ContentBlock::Image { .. }))
}),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct Needs {
pub tools: bool,
pub structured_output: bool,
pub prompt_caching: bool,
pub thinking: bool,
pub images: bool,
}
impl Needs {
pub fn unmet_by(self, have: &crate::model::ModelCapabilities) -> Vec<&'static str> {
let mut missing = Vec::new();
if self.tools && !have.tools {
missing.push("tools");
}
if self.structured_output && !have.structured_output {
missing.push("structured_output");
}
if self.prompt_caching && !have.prompt_caching {
missing.push("prompt_caching");
}
if self.thinking && !have.thinking {
missing.push("thinking");
}
if self.images && !have.images {
missing.push("images");
}
missing
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{ModelCapabilities, Reach};
#[test]
fn a_plain_request_needs_nothing_special() {
let request = ChatRequest::new("m", vec![Message::user("hi")]);
assert_eq!(request.needs(), Needs::default());
}
#[test]
fn what_a_provider_will_drop_is_listed_before_the_call() {
let request = ChatRequest::new("m", vec![Message::user("hi")])
.with_tools(vec![ToolSchema {
name: "search".into(),
description: "look it up".into(),
parameters: serde_json::json!({}),
}])
.with_thinking(Thinking::On(Effort::High));
let plain = ModelCapabilities::none(Reach::LocalCli);
assert_eq!(request.needs().unmet_by(&plain), vec!["tools", "thinking"]);
}
#[test]
fn a_capable_model_leaves_nothing_unmet() {
let request = ChatRequest::new("m", vec![Message::user("hi")])
.with_response_schema(serde_json::json!({ "type": "object" }));
let capable = ModelCapabilities {
structured_output: true,
..ModelCapabilities::none(Reach::FirstPartyApi)
};
assert!(request.needs().unmet_by(&capable).is_empty());
}
#[test]
fn no_opinion_is_not_the_same_as_off() {
assert_ne!(Thinking::Unset, Thinking::Off);
assert_eq!(Thinking::default(), Thinking::Unset);
}
#[test]
fn no_opinion_asks_for_nothing() {
let request = ChatRequest::new("m", vec![Message::user("hi")]);
assert!(!request.needs().thinking);
}
}