Skip to main content

st/proxy/claude/
builder.rs

1//! Fluent Request Builder for the Claude Messages API
2//!
3//! Provides an ergonomic builder pattern for constructing requests:
4//!
5//! ```rust,no_run
6//! let response = client.messages()
7//!     .opus()
8//!     .system("You are a Rust expert")
9//!     .user("Explain lifetimes")
10//!     .thinking_adaptive()
11//!     .effort("high")
12//!     .max_tokens(4096)
13//!     .send()
14//!     .await?;
15//! ```
16//!
17//! The builder accumulates all parameters, then sends via the parent `ClaudeClient`.
18
19use super::error::ClaudeApiError;
20use super::stream::SseParser;
21use super::types::*;
22
23// ---------------------------------------------------------------------------
24// Builder struct - accumulates request parameters
25// ---------------------------------------------------------------------------
26
27/// Fluent builder for Claude API message requests.
28///
29/// Created via `ClaudeClient::messages()`. Chain methods to configure,
30/// then call `.send()` for the full response or `.stream()` for SSE events.
31pub struct MessageRequestBuilder<'a> {
32    /// Reference to the parent client (holds HTTP client, API key, base URL)
33    client: &'a super::ClaudeClient,
34    model: String,
35    messages: Vec<Message>,
36    system: Option<SystemContent>,
37    max_tokens: usize,
38    temperature: Option<f32>,
39    top_p: Option<f32>,
40    top_k: Option<usize>,
41    stop_sequences: Option<Vec<String>>,
42    thinking: Option<ThinkingConfig>,
43    tools: Option<Vec<Tool>>,
44    tool_choice: Option<ToolChoice>,
45    output_config: Option<OutputConfig>,
46    metadata: Option<Metadata>,
47    stream: bool,
48    beta_headers: Vec<String>,
49}
50
51impl<'a> MessageRequestBuilder<'a> {
52    /// Create a new builder with sensible defaults
53    pub(crate) fn new(client: &'a super::ClaudeClient) -> Self {
54        Self {
55            client,
56            model: client.default_model.clone(),
57            messages: Vec::new(),
58            system: None,
59            max_tokens: 4096,
60            temperature: None,
61            top_p: None,
62            top_k: None,
63            stop_sequences: None,
64            thinking: None,
65            tools: None,
66            tool_choice: None,
67            output_config: None,
68            metadata: None,
69            stream: false,
70            beta_headers: Vec::new(),
71        }
72    }
73
74    // === Model selection ===
75
76    /// Set the model by ID (e.g. "claude-opus-4-6")
77    pub fn model(mut self, model: &str) -> Self {
78        self.model = model.to_string();
79        self
80    }
81
82    /// Use Claude Opus 4.6 - most intelligent, best for agents and coding
83    pub fn opus(self) -> Self {
84        self.model(models::OPUS_4_6)
85    }
86
87    /// Use Claude Sonnet 4.6 - best speed/intelligence balance
88    pub fn sonnet(self) -> Self {
89        self.model(models::SONNET_4_6)
90    }
91
92    /// Use Claude Haiku 4.5 - fastest and cheapest
93    pub fn haiku(self) -> Self {
94        self.model(models::HAIKU_4_5)
95    }
96
97    // === Messages ===
98
99    /// Set the system prompt (plain text)
100    pub fn system(mut self, text: &str) -> Self {
101        self.system = Some(SystemContent::Text(text.to_string()));
102        self
103    }
104
105    /// Set the system prompt with prompt caching enabled
106    pub fn system_cached(mut self, text: &str) -> Self {
107        self.system = Some(SystemContent::Blocks(vec![SystemBlock {
108            block_type: "text".to_string(),
109            text: text.to_string(),
110            cache_control: Some(CacheControl::ephemeral()),
111        }]));
112        self
113    }
114
115    /// Add a user text message
116    pub fn user(mut self, text: &str) -> Self {
117        self.messages.push(Message::user(text));
118        self
119    }
120
121    /// Add a user message with an image (base64 encoded)
122    pub fn user_with_image_base64(
123        mut self,
124        text: &str,
125        media_type: &str,
126        base64_data: &str,
127    ) -> Self {
128        self.messages.push(Message {
129            role: MessageRole::User,
130            content: MessageContent::Blocks(vec![
131                ContentBlock::Image {
132                    source: ImageSource::Base64 {
133                        media_type: media_type.to_string(),
134                        data: base64_data.to_string(),
135                    },
136                    cache_control: None,
137                },
138                ContentBlock::Text {
139                    text: text.to_string(),
140                    cache_control: None,
141                },
142            ]),
143        });
144        self
145    }
146
147    /// Add a user message with an image URL
148    pub fn user_with_image_url(mut self, text: &str, url: &str) -> Self {
149        self.messages.push(Message {
150            role: MessageRole::User,
151            content: MessageContent::Blocks(vec![
152                ContentBlock::Image {
153                    source: ImageSource::Url {
154                        url: url.to_string(),
155                    },
156                    cache_control: None,
157                },
158                ContentBlock::Text {
159                    text: text.to_string(),
160                    cache_control: None,
161                },
162            ]),
163        });
164        self
165    }
166
167    /// Add an assistant text message (for multi-turn conversations)
168    pub fn assistant(mut self, text: &str) -> Self {
169        self.messages.push(Message::assistant(text));
170        self
171    }
172
173    /// Set the full message list (for agentic loops where you manage messages)
174    pub fn messages(mut self, msgs: Vec<Message>) -> Self {
175        self.messages = msgs;
176        self
177    }
178
179    /// Add tool results as a user message (for the agentic tool loop)
180    pub fn tool_results(mut self, results: Vec<ContentBlock>) -> Self {
181        self.messages.push(Message {
182            role: MessageRole::User,
183            content: MessageContent::Blocks(results),
184        });
185        self
186    }
187
188    // === Thinking ===
189
190    /// Enable adaptive thinking (recommended for Opus 4.6 / Sonnet 4.6).
191    /// Claude dynamically decides when and how much to think.
192    pub fn thinking_adaptive(mut self) -> Self {
193        self.thinking = Some(ThinkingConfig::Adaptive);
194        self
195    }
196
197    /// Enable thinking with a fixed token budget (older models only).
198    /// `budget_tokens` must be less than `max_tokens` (minimum 1024).
199    pub fn thinking_enabled(mut self, budget_tokens: usize) -> Self {
200        self.thinking = Some(ThinkingConfig::Enabled { budget_tokens });
201        self
202    }
203
204    /// Explicitly disable thinking
205    pub fn thinking_disabled(mut self) -> Self {
206        self.thinking = Some(ThinkingConfig::Disabled);
207        self
208    }
209
210    // === Effort ===
211
212    /// Set the effort level: "low", "medium", "high" (default), or "max" (Opus only).
213    /// Lower effort = cheaper/faster. Higher effort = deeper reasoning.
214    pub fn effort(mut self, level: &str) -> Self {
215        let effort = match level {
216            "low" => Effort::Low,
217            "medium" => Effort::Medium,
218            "max" => Effort::Max,
219            _ => Effort::High, // default
220        };
221        // Merge into existing output_config or create new one
222        match self.output_config.as_mut() {
223            Some(config) => config.effort = Some(effort),
224            None => {
225                self.output_config = Some(OutputConfig {
226                    effort: Some(effort),
227                    format: None,
228                });
229            }
230        }
231        self
232    }
233
234    // === Tools ===
235
236    /// Set the available tools for Claude to call
237    pub fn tools(mut self, tools: Vec<Tool>) -> Self {
238        self.tools = Some(tools);
239        self
240    }
241
242    /// Let Claude decide whether to use tools (default)
243    pub fn tool_choice_auto(mut self) -> Self {
244        self.tool_choice = Some(ToolChoice::Auto {
245            disable_parallel_tool_use: None,
246        });
247        self
248    }
249
250    /// Force Claude to use at least one tool
251    pub fn tool_choice_any(mut self) -> Self {
252        self.tool_choice = Some(ToolChoice::Any {
253            disable_parallel_tool_use: None,
254        });
255        self
256    }
257
258    /// Force Claude to use a specific tool by name
259    pub fn tool_choice_specific(mut self, name: &str) -> Self {
260        self.tool_choice = Some(ToolChoice::Tool {
261            name: name.to_string(),
262            disable_parallel_tool_use: None,
263        });
264        self
265    }
266
267    /// Prevent Claude from using any tools
268    pub fn tool_choice_none(mut self) -> Self {
269        self.tool_choice = Some(ToolChoice::None);
270        self
271    }
272
273    // === Structured output ===
274
275    /// Constrain the response to match a JSON schema
276    pub fn json_schema(mut self, schema: serde_json::Value) -> Self {
277        let format = Some(OutputFormat::JsonSchema { schema });
278        match self.output_config.as_mut() {
279            Some(config) => config.format = format,
280            None => {
281                self.output_config = Some(OutputConfig {
282                    effort: None,
283                    format,
284                });
285            }
286        }
287        self
288    }
289
290    // === Parameters ===
291
292    /// Maximum tokens to generate (default: 4096)
293    pub fn max_tokens(mut self, n: usize) -> Self {
294        self.max_tokens = n;
295        self
296    }
297
298    /// Sampling temperature (0.0 = deterministic, 1.0 = creative)
299    pub fn temperature(mut self, t: f32) -> Self {
300        self.temperature = Some(t);
301        self
302    }
303
304    /// Nucleus sampling parameter
305    pub fn top_p(mut self, p: f32) -> Self {
306        self.top_p = Some(p);
307        self
308    }
309
310    /// Top-k sampling parameter
311    pub fn top_k(mut self, k: usize) -> Self {
312        self.top_k = Some(k);
313        self
314    }
315
316    /// Custom stop sequences
317    pub fn stop_sequences(mut self, seqs: Vec<String>) -> Self {
318        self.stop_sequences = Some(seqs);
319        self
320    }
321
322    /// Set a user ID for tracking/billing
323    pub fn user_id(mut self, id: &str) -> Self {
324        self.metadata = Some(Metadata {
325            user_id: Some(id.to_string()),
326        });
327        self
328    }
329
330    /// Enable a beta feature by header string (e.g. "compact-2026-01-12")
331    pub fn beta(mut self, header: &str) -> Self {
332        self.beta_headers.push(header.to_string());
333        self
334    }
335
336    // === Build / Send / Stream ===
337
338    /// Build the `MessagesRequest` from current state (internal helper)
339    fn build_request(&self, force_stream: bool) -> MessagesRequest {
340        MessagesRequest {
341            model: self.model.clone(),
342            messages: self.messages.clone(),
343            max_tokens: self.max_tokens,
344            system: self.system.clone(),
345            temperature: self.temperature,
346            top_p: self.top_p,
347            top_k: self.top_k,
348            stop_sequences: self.stop_sequences.clone(),
349            thinking: self.thinking.clone(),
350            tools: self.tools.clone(),
351            tool_choice: self.tool_choice.clone(),
352            output_config: self.output_config.clone(),
353            metadata: self.metadata.clone(),
354            stream: force_stream || self.stream,
355        }
356    }
357
358    /// Build the request without sending (useful for inspection/testing)
359    pub fn build(self) -> MessagesRequest {
360        self.build_request(false)
361    }
362
363    /// Send the request and return the full response (non-streaming).
364    pub async fn send(&self) -> Result<MessagesResponse, ClaudeApiError> {
365        let request = self.build_request(false);
366        let response = self
367            .client
368            .send_request(&request, &self.beta_headers)
369            .await?;
370        response
371            .json::<MessagesResponse>()
372            .await
373            .map_err(ClaudeApiError::from)
374    }
375
376    /// Send the request with streaming, returning an SSE event parser.
377    /// Use `parser.next_event()` to consume events one at a time.
378    pub async fn stream(&self) -> Result<SseParser, ClaudeApiError> {
379        let request = self.build_request(true);
380        let response = self
381            .client
382            .send_request(&request, &self.beta_headers)
383            .await?;
384        Ok(SseParser::new(response))
385    }
386}