Skip to main content

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}