ailoop_core/request.rs
1//! Per-turn request shape: [`ChatRequest`] plus [`ToolChoice`],
2//! [`ToolDefinition`], and [`ToolTag`].
3
4use serde::Serialize;
5use serde_json::Value;
6
7use crate::{CacheControl, Message, SystemPrompt};
8
9/// Per-turn request the engine assembles and the adapter lowers to
10/// the provider's wire format.
11///
12/// The struct is `#[non_exhaustive]`, so external construction goes
13/// through [`Default`] / [`ChatRequest::new`] rather than struct
14/// literals. Adapters that don't natively support a control (e.g.
15/// `top_k` on Chat Completions) ignore it; provider-specific knobs
16/// outside the typed surface ride [`Self::additional_params`].
17#[derive(Clone)]
18#[non_exhaustive]
19pub struct ChatRequest {
20 /// Conversation history sent to the provider, in order. Adapters
21 /// translate each [`Message`] block to the provider's content
22 /// shape.
23 pub messages: Vec<Message>,
24 /// System instructions sent out-of-band from `messages`. `None`
25 /// lets the provider use its default behaviour.
26 pub system_prompt: Option<SystemPrompt>,
27 /// Tools the model may call this turn. `None` disables tool use
28 /// entirely and lets adapters omit the `tools` field on the wire,
29 /// which some providers require when no tools are configured.
30 pub tools: Option<Vec<ToolDefinition>>,
31 /// Sampling temperature in the provider's native range
32 /// (typically `[0.0, 1.0]` or `[0.0, 2.0]`). `None` keeps the
33 /// provider default.
34 pub temperature: Option<f32>,
35 /// Nucleus sampling cutoff. `None` keeps the provider default.
36 pub top_p: Option<f32>,
37 /// Top-k sampling cutoff. Only honoured by providers that expose
38 /// the parameter (Anthropic). `None` keeps the provider default;
39 /// adapters without `top_k` ignore the field.
40 pub top_k: Option<u32>,
41 /// Strings that, if produced, terminate the response with
42 /// [`crate::FinishReason::StopSequence`].
43 pub stop_sequences: Vec<String>,
44 /// Maximum output tokens the provider should generate this turn.
45 pub max_tokens: u32,
46 /// How the model should pick among the available tools. `None`
47 /// leaves the provider default (typically `Auto`). Mapped to each
48 /// provider's wire format by the adapter.
49 pub tool_choice: Option<ToolChoice>,
50 /// Forbid the model from emitting more than one `tool_use` block
51 /// per turn. `None` leaves the provider default (parallel allowed).
52 /// Adapters lower this to the field their API expects:
53 /// `tool_choice.disable_parallel_tool_use` for Anthropic,
54 /// `parallel_tool_calls` (negated) for Chat Completions.
55 pub disable_parallel_tool_use: Option<bool>,
56
57 /// Free-form JSON merged into the provider's request body.
58 /// Escape hatch for provider-specific knobs not yet typed in the
59 /// shared shape (Anthropic `thinking`, OpenAI `reasoning_effort`,
60 /// future betas). Adapters merge this last so it can override
61 /// any field the typed surface set.
62 pub additional_params: Option<Value>,
63}
64
65impl Default for ChatRequest {
66 fn default() -> Self {
67 Self {
68 messages: Vec::new(),
69 system_prompt: None,
70 tools: None,
71 temperature: None,
72 top_p: None,
73 top_k: None,
74 stop_sequences: Vec::new(),
75 max_tokens: 4096,
76 tool_choice: None,
77 disable_parallel_tool_use: None,
78 additional_params: None,
79 }
80 }
81}
82
83impl ChatRequest {
84 /// Build a request with the given history and token cap; every
85 /// other field takes its [`Default`] value. Use `..Default::default()`
86 /// in struct-update form when more than these two fields are set.
87 pub fn new(messages: Vec<Message>, max_tokens: u32) -> Self {
88 Self {
89 messages,
90 max_tokens,
91 ..Default::default()
92 }
93 }
94}
95
96/// Constraint placed on the model's tool selection for a single
97/// request. Variant naming follows Anthropic's wire vocabulary; the
98/// Chat Completions adapter translates `Any` → `"required"` and
99/// `None_` → `"none"`.
100#[derive(Debug, Clone, PartialEq, Eq)]
101#[non_exhaustive]
102pub enum ToolChoice {
103 /// Model decides whether to call a tool. Provider default.
104 Auto,
105 /// Model must call **some** tool, but may pick which one.
106 /// Translates to `"required"` on Chat Completions.
107 Any,
108 /// Model must call this specific tool.
109 Tool {
110 /// Name of the required tool; must appear in the request's
111 /// `tools` list.
112 name: String,
113 },
114 /// Model is forbidden from calling any tool. Trailing underscore
115 /// avoids collision with the keyword `None`.
116 None_,
117}
118
119/// Tool description sent to the provider so the model can decide when
120/// and how to call it.
121///
122/// `ToolDefinition` is the wire-level shape; tool *implementations*
123/// live behind the `ailoop-tools` `Tool` / `ToolDyn` traits and only
124/// produce a `ToolDefinition` when they are mounted on a
125/// [`ChatRequest`].
126#[derive(Debug, Serialize, Clone)]
127#[non_exhaustive]
128pub struct ToolDefinition {
129 /// Tool name; must match the `name` the model emits on
130 /// [`crate::AssistantBlock::ToolCall`].
131 pub name: String,
132 /// Human-readable description used by the model to decide when to
133 /// invoke the tool.
134 pub description: String,
135 /// JSON Schema (2020-12) for the tool's input arguments.
136 pub input_schema: serde_json::Value,
137 /// Tags consumed by capability gating and approval middleware.
138 pub tags: Vec<ToolTag>,
139 /// Cache breakpoint for this tool entry on providers that support
140 /// per-tool prompt caching (Anthropic). Adapters without per-tool
141 /// caching ignore the field.
142 #[serde(skip)]
143 pub cache_control: Option<CacheControl>,
144}
145
146impl ToolDefinition {
147 /// Build a definition with the given identity, schema, and tags
148 /// and no cache breakpoint.
149 pub fn new(
150 name: &str,
151 description: &str,
152 input_schema: serde_json::Value,
153 tags: Vec<ToolTag>,
154 ) -> Self {
155 Self {
156 name: name.into(),
157 description: description.into(),
158 input_schema,
159 tags,
160 cache_control: None,
161 }
162 }
163
164 /// Builder-style helper: attach a cache breakpoint to this tool
165 /// entry.
166 pub fn with_cache_control(mut self, cache_control: CacheControl) -> Self {
167 self.cache_control = Some(cache_control);
168 self
169 }
170}
171
172/// Capability tag attached to a [`ToolDefinition`].
173///
174/// Tags are the input to the façade's capability filter
175/// (`ConversationBuilder::with_capabilities`) and to the built-in
176/// approval middleware. The `ailoop_derive::ailoop_tool` proc-macro
177/// emits these via the `tags(...)` argument.
178#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
179#[non_exhaustive]
180pub enum ToolTag {
181 /// Tool mutates state outside the conversation (delete files,
182 /// send a payment, ...). The default approval middleware gates
183 /// these by default.
184 Destructive,
185 /// Tool only reads state; safe to run unattended.
186 ReadOnly,
187 /// Tool reaches the network. Useful for sandboxed deployments.
188 Network,
189 /// Tool writes to the local filesystem. Approval-gated by the
190 /// default middleware alongside [`Self::Destructive`].
191 WritesFiles,
192 /// Free-form tag for caller-defined capability schemes.
193 Custom(String),
194}