rai_sdk/client.rs
1//! The client and request builders that drive generation.
2//!
3//! [`ClientBuilder`] assembles configuration, a default model, and any shared
4//! tools into a [`Client`]. Each call to [`Client::request`] returns a
5//! [`RequestBuilder`], a typestate builder whose terminal methods only become
6//! available once the request has both a prompt and a model.
7//!
8//! The builder exposes four families of terminal operations: `generate` and
9//! `generate_once` for text, `generate_structured` and
10//! `generate_structured_once` for typed output, `stream` and
11//! `stream_accumulated` for streaming, and per-request overrides such as
12//! configuration, tools, and retry policy. The `_once` variants perform a single
13//! provider call and do not execute registered tools.
14
15use std::{any::type_name, collections::HashSet, marker::PhantomData, pin::Pin};
16
17use futures::{Stream, StreamExt};
18use schemars::JsonSchema;
19use serde::de::DeserializeOwned;
20use tracing::{debug, error, info, instrument};
21
22use crate::{
23 config::Config,
24 error::{Error, ProviderKind, Result},
25 generation::GenerationConfig,
26 message::{Message, Prompt, Response, StructuredOutput, ToolDefinition},
27 model::Model,
28 retry::RetryConfig,
29 tool::{Tool, ToolContext, ToolRegistry},
30};
31
32#[cfg(feature = "openai")]
33use crate::provider::{OpenAICompatibleProvider, OpenAIProvider};
34
35#[cfg(feature = "anthropic")]
36use crate::provider::AnthropicProvider;
37
38#[cfg(feature = "openrouter")]
39use crate::provider::OpenRouterProvider;
40
41#[doc(hidden)]
42pub struct ModelMissing;
43
44#[doc(hidden)]
45pub struct ModelReady;
46
47/// Unified AI client for OpenAI, Anthropic, and OpenRouter.
48///
49/// A client owns provider credentials and HTTP clients, an optional default
50/// model, default generation and retry settings, and any tools shared by every
51/// request. Build one once and reuse it: individual requests are cheap, but
52/// constructing a client initializes a client per configured provider.
53///
54/// Use [`ClientBuilder`] for the common path, or [`Client::new`] when you
55/// already have a [`Config`].
56///
57/// # Typestate
58///
59/// The `ModelState` parameter records whether a default model is present.
60/// [`ClientBuilder::model`] moves the builder into the model-ready state, and
61/// only a model-ready client hands out request builders that can call
62/// [`RequestBuilder::generate`] without naming a model. A client built without a
63/// default model is still fully usable — every request just has to call
64/// [`RequestBuilder::model`] first. Either way, a request missing a model is a
65/// compile error rather than a runtime one.
66///
67/// # Examples
68///
69/// ```no_run
70/// use rai_sdk::{ClientBuilder, Model};
71///
72/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
73/// let client = ClientBuilder::new()
74/// .from_env()
75/// .model(Model::gpt4o_mini())
76/// .build()?;
77///
78/// // Reuse the same client for many requests.
79/// for prompt in ["Define a trait.", "Define a lifetime."] {
80/// let response = client.request().prompt(prompt).generate().await?;
81/// println!("{}", response.text());
82/// }
83/// # Ok(())
84/// # }
85/// ```
86pub struct Client<ModelState = ModelMissing> {
87 config: Config,
88 default_model: Option<Model>,
89 default_config: GenerationConfig,
90 default_retry_config: RetryConfig,
91 tool_registry: ToolRegistry,
92 state: PhantomData<ModelState>,
93
94 #[cfg(feature = "openai")]
95 openai: Option<OpenAIProvider>,
96
97 #[cfg(feature = "openai")]
98 openai_compatible: Option<OpenAICompatibleProvider>,
99
100 #[cfg(feature = "anthropic")]
101 anthropic: Option<AnthropicProvider>,
102
103 #[cfg(feature = "openrouter")]
104 openrouter: Option<OpenRouterProvider>,
105}
106
107impl<ModelState> std::fmt::Debug for Client<ModelState> {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 let mut s = f.debug_struct("Client");
110 s.field("default_model", &self.default_model);
111 s.field("default_config", &self.default_config);
112
113 #[cfg(feature = "openai")]
114 s.field("openai", &self.openai);
115
116 #[cfg(feature = "openai")]
117 s.field("openai_compatible", &self.openai_compatible);
118
119 #[cfg(feature = "anthropic")]
120 s.field("anthropic", &self.anthropic);
121
122 #[cfg(feature = "openrouter")]
123 s.field("openrouter", &self.openrouter);
124
125 s.finish()
126 }
127}
128
129impl Client<ModelMissing> {
130 /// Create a client from an explicit [`Config`], with no default model.
131 ///
132 /// Every request from this client must select a model with
133 /// [`RequestBuilder::model`]. Use [`ClientBuilder`] instead if you want a
134 /// default model or client-level tools.
135 ///
136 /// A provider whose API key is missing is simply left uninitialized rather
137 /// than failing here; using it later returns
138 /// [`Error::ProviderNotConfigured`].
139 ///
140 /// # Errors
141 ///
142 /// Returns an error if a configured provider's HTTP client cannot be
143 /// constructed, for example because the request timeout is invalid.
144 ///
145 /// # Examples
146 ///
147 /// ```no_run
148 /// use rai_sdk::{Client, Config, Model};
149 ///
150 /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
151 /// let client = Client::new(Config::from_env())?;
152 ///
153 /// let response = client
154 /// .request()
155 /// .model(Model::gpt4o_mini())
156 /// .prompt("Hello")
157 /// .generate()
158 /// .await?;
159 /// # println!("{}", response.text());
160 /// # Ok(())
161 /// # }
162 /// ```
163 pub fn new(config: Config) -> Result<Self> {
164 let default_retry_config = config.retry_config();
165 Self::new_with_defaults(
166 config,
167 None,
168 GenerationConfig::default(),
169 default_retry_config,
170 ToolRegistry::new(),
171 )
172 }
173
174 /// Create a builder for configuring a client with defaults.
175 ///
176 /// Equivalent to [`ClientBuilder::new`].
177 pub fn builder() -> ClientBuilder<ModelMissing> {
178 ClientBuilder::new()
179 }
180
181 /// Start a request.
182 ///
183 /// This client has no default model, so the returned builder requires
184 /// [`RequestBuilder::model`] before it will expose `generate` and friends.
185 pub fn request(&self) -> RequestBuilder<'_, PromptMissing, ModelMissing, ModelMissing> {
186 self.request_builder()
187 }
188}
189
190impl<ModelState> Client<ModelState> {
191 fn request_builder(&self) -> RequestBuilder<'_, PromptMissing, ModelState, ModelState> {
192 RequestBuilder::new(self)
193 }
194
195 fn new_with_defaults(
196 config: Config,
197 default_model: Option<Model>,
198 default_config: GenerationConfig,
199 default_retry_config: RetryConfig,
200 tool_registry: ToolRegistry,
201 ) -> Result<Self> {
202 info!("Initializing AI client");
203
204 #[cfg(feature = "openai")]
205 let openai = if config.openai_key().is_some() {
206 match OpenAIProvider::new(&config) {
207 Ok(provider) => {
208 info!("OpenAI provider initialized");
209 Some(provider)
210 }
211 Err(e) => {
212 tracing::warn!(error = %e, "Failed to initialize OpenAI provider");
213 None
214 }
215 }
216 } else {
217 tracing::debug!("OpenAI API key not configured, provider disabled");
218 None
219 };
220
221 // Configured by base URL rather than by credential: an OpenAI-compatible
222 // endpoint often needs no key at all, so naming the endpoint is what
223 // signals intent to use one.
224 #[cfg(feature = "openai")]
225 let openai_compatible = if config.openai_compatible_base_url().is_some() {
226 match OpenAICompatibleProvider::new(&config) {
227 Ok(provider) => {
228 info!(
229 base_url = provider.base_url(),
230 "OpenAI-compatible provider initialized"
231 );
232 Some(provider)
233 }
234 Err(e) => {
235 tracing::warn!(error = %e, "Failed to initialize OpenAI-compatible provider");
236 None
237 }
238 }
239 } else {
240 tracing::debug!("No OpenAI-compatible base URL configured, provider disabled");
241 None
242 };
243
244 #[cfg(feature = "anthropic")]
245 let anthropic = if config.anthropic_key().is_some() {
246 match AnthropicProvider::new(&config) {
247 Ok(provider) => {
248 info!("Anthropic provider initialized");
249 Some(provider)
250 }
251 Err(e) => {
252 tracing::warn!(error = %e, "Failed to initialize Anthropic provider");
253 None
254 }
255 }
256 } else {
257 tracing::debug!("Anthropic API key not configured, provider disabled");
258 None
259 };
260
261 #[cfg(feature = "openrouter")]
262 let openrouter = if config.openrouter_key().is_some() {
263 match OpenRouterProvider::new(&config) {
264 Ok(provider) => {
265 info!("OpenRouter provider initialized");
266 Some(provider)
267 }
268 Err(e) => {
269 tracing::warn!(error = %e, "Failed to initialize OpenRouter provider");
270 None
271 }
272 }
273 } else {
274 tracing::debug!("OpenRouter API key not configured, provider disabled");
275 None
276 };
277
278 info!("AI client initialized successfully");
279
280 Ok(Self {
281 config,
282 default_model,
283 default_config,
284 default_retry_config,
285 tool_registry,
286 state: PhantomData,
287 #[cfg(feature = "openai")]
288 openai,
289 #[cfg(feature = "openai")]
290 openai_compatible,
291 #[cfg(feature = "anthropic")]
292 anthropic,
293 #[cfg(feature = "openrouter")]
294 openrouter,
295 })
296 }
297
298 async fn generate_with_tools(
299 &self,
300 model: Model,
301 prompt: &Prompt,
302 config: &GenerationConfig,
303 retry_config: &RetryConfig,
304 tool_registry: &ToolRegistry,
305 ) -> Result<Response> {
306 let Some(tool_definitions) =
307 (!tool_registry.is_empty()).then(|| tool_registry.definitions())
308 else {
309 return crate::retry::with_retry(retry_config, "generate", || {
310 self.generate_once_internal(model.clone(), prompt, config, None)
311 })
312 .await;
313 };
314
315 let mut prompt_with_tools = prompt.clone();
316 let max_rounds = config.tool_round_limit();
317
318 for round in 0..max_rounds {
319 let response = crate::retry::with_retry(retry_config, "generate", || {
320 self.generate_once_internal(
321 model.clone(),
322 &prompt_with_tools,
323 config,
324 Some(&tool_definitions),
325 )
326 })
327 .await?;
328
329 let tool_calls: Vec<_> = response
330 .messages
331 .iter()
332 .flat_map(|message| message.tool_calls.iter().cloned())
333 .collect();
334
335 if tool_calls.is_empty() {
336 return Ok(response);
337 }
338
339 prompt_with_tools.messages.extend(response.messages.clone());
340
341 for tool_call in tool_calls {
342 let tool_message = tool_registry
343 .execute(
344 &tool_call,
345 ToolContext {
346 provider: model.provider(),
347 model: model.as_str().to_string(),
348 round,
349 tool_name: tool_call.name.clone(),
350 tool_call_id: tool_call.id.clone(),
351 },
352 )
353 .await?;
354 prompt_with_tools.messages.push(tool_message);
355 }
356 }
357
358 Err(Error::ToolLoopLimitExceeded { max_rounds })
359 }
360
361 async fn generate_once_internal(
362 &self,
363 model: Model,
364 prompt: &Prompt,
365 config: &GenerationConfig,
366 tool_definitions: Option<&[ToolDefinition]>,
367 ) -> Result<Response> {
368 #[cfg(not(any(feature = "openai", feature = "anthropic", feature = "openrouter")))]
369 let _ = (prompt, config, tool_definitions);
370
371 match model {
372 #[cfg(feature = "openai")]
373 Model::OpenAI(ref openai_model) => {
374 let provider = self
375 .openai
376 .as_ref()
377 .ok_or_else(|| Error::ProviderNotConfigured(ProviderKind::OpenAI))?;
378 provider
379 .generate(openai_model, prompt, config, tool_definitions)
380 .await
381 }
382
383 #[cfg(feature = "openai")]
384 Model::OpenAICompatible(ref compatible_model) => {
385 let provider = self
386 .openai_compatible
387 .as_ref()
388 .ok_or_else(|| Error::ProviderNotConfigured(ProviderKind::OpenAICompatible))?;
389 provider
390 .generate(compatible_model, prompt, config, tool_definitions)
391 .await
392 }
393
394 #[cfg(feature = "anthropic")]
395 Model::Anthropic(ref anthropic_model) => {
396 let provider = self
397 .anthropic
398 .as_ref()
399 .ok_or_else(|| Error::ProviderNotConfigured(ProviderKind::Anthropic))?;
400 provider
401 .generate(anthropic_model, prompt, config, tool_definitions)
402 .await
403 }
404
405 #[cfg(feature = "openrouter")]
406 Model::OpenRouter(ref openrouter_model) => {
407 let provider = self
408 .openrouter
409 .as_ref()
410 .ok_or_else(|| Error::ProviderNotConfigured(ProviderKind::OpenRouter))?;
411 provider
412 .generate(openrouter_model, prompt, config, tool_definitions)
413 .await
414 }
415
416 #[allow(unreachable_patterns)]
417 _ => Err(Error::ProviderNotEnabled(model.provider())),
418 }
419 }
420
421 /// Stream a completion for an explicit model and prompt.
422 ///
423 /// Prefer [`RequestBuilder::stream`], which applies the client's defaults,
424 /// retry policy, and per-request tool overrides. This lower-level entry
425 /// point is useful when you are driving the model and prompt yourself.
426 ///
427 /// # Errors
428 ///
429 /// - [`Error::InvalidRequest`] if any tool is registered on this client,
430 /// since streaming cannot run a tool loop. Because this method takes no
431 /// request context, it can only consider the client's tools; use
432 /// [`RequestBuilder::stream`] with [`RequestBuilder::no_tools`] to stream
433 /// from a client that has tools registered.
434 /// - [`Error::ProviderNotConfigured`] if the model's provider has no API key.
435 /// - [`Error::ProviderNotEnabled`] if its Cargo feature is disabled.
436 /// - A transport or provider error if the request itself fails.
437 pub async fn generate_stream(
438 &self,
439 model: Model,
440 prompt: &Prompt,
441 config: &GenerationConfig,
442 ) -> Result<Pin<Box<dyn Stream<Item = Result<crate::provider::ProviderStreamEvent>> + Send>>>
443 {
444 ensure_streamable(&self.tool_registry, &[])?;
445 self.generate_stream_inner(model, prompt, config, None)
446 .await
447 }
448
449 /// Open a provider stream with optional tool definitions.
450 ///
451 /// Callers are responsible for rejecting tool-bearing requests that cannot
452 /// support them. [`stream_wire_events`](RequestBuilder::stream_wire_events)
453 /// passes definitions through for its remote client to execute.
454 #[instrument(skip(self, prompt, config))]
455 async fn generate_stream_inner(
456 &self,
457 model: Model,
458 prompt: &Prompt,
459 config: &GenerationConfig,
460 tool_definitions: Option<&[ToolDefinition]>,
461 ) -> Result<Pin<Box<dyn Stream<Item = Result<crate::provider::ProviderStreamEvent>> + Send>>>
462 {
463 #[cfg(not(any(feature = "openai", feature = "anthropic", feature = "openrouter")))]
464 let _ = (prompt, config, tool_definitions);
465
466 match model {
467 #[cfg(feature = "openai")]
468 Model::OpenAI(ref openai_model) => {
469 let provider = self
470 .openai
471 .as_ref()
472 .ok_or_else(|| Error::ProviderNotConfigured(ProviderKind::OpenAI))?;
473 provider
474 .generate_stream(openai_model, prompt, config, tool_definitions)
475 .await
476 }
477
478 #[cfg(feature = "openai")]
479 Model::OpenAICompatible(ref compatible_model) => {
480 let provider = self
481 .openai_compatible
482 .as_ref()
483 .ok_or_else(|| Error::ProviderNotConfigured(ProviderKind::OpenAICompatible))?;
484 provider
485 .generate_stream(compatible_model, prompt, config, tool_definitions)
486 .await
487 }
488
489 #[cfg(feature = "anthropic")]
490 Model::Anthropic(ref anthropic_model) => {
491 let provider = self
492 .anthropic
493 .as_ref()
494 .ok_or_else(|| Error::ProviderNotConfigured(ProviderKind::Anthropic))?;
495 provider
496 .generate_stream(anthropic_model, prompt, config, tool_definitions)
497 .await
498 }
499
500 #[cfg(feature = "openrouter")]
501 Model::OpenRouter(ref openrouter_model) => {
502 let provider = self
503 .openrouter
504 .as_ref()
505 .ok_or_else(|| Error::ProviderNotConfigured(ProviderKind::OpenRouter))?;
506 provider
507 .generate_stream(openrouter_model, prompt, config, tool_definitions)
508 .await
509 }
510
511 #[allow(unreachable_patterns)]
512 _ => Err(Error::ProviderNotEnabled(model.provider())),
513 }
514 }
515
516 /// Whether a provider is usable: its feature is enabled and it has
517 /// credentials.
518 ///
519 /// Use this to branch at runtime instead of discovering a missing key
520 /// through a failed request.
521 ///
522 /// # Examples
523 ///
524 /// ```no_run
525 /// use rai_sdk::{ClientBuilder, Model, ProviderKind};
526 ///
527 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
528 /// let client = ClientBuilder::new().from_env().build()?;
529 ///
530 /// let model = if client.is_provider_available(ProviderKind::Anthropic) {
531 /// Model::claude_sonnet_46()
532 /// } else {
533 /// Model::gpt4o_mini()
534 /// };
535 /// # let _ = model;
536 /// # Ok(())
537 /// # }
538 /// ```
539 pub fn is_provider_available(&self, provider: ProviderKind) -> bool {
540 match provider {
541 #[cfg(feature = "openai")]
542 ProviderKind::OpenAI => self.openai.is_some(),
543
544 #[cfg(feature = "openai")]
545 ProviderKind::OpenAICompatible => self.openai_compatible.is_some(),
546
547 #[cfg(feature = "anthropic")]
548 ProviderKind::Anthropic => self.anthropic.is_some(),
549
550 #[cfg(feature = "openrouter")]
551 ProviderKind::OpenRouter => self.openrouter.is_some(),
552
553 #[allow(unreachable_patterns)]
554 _ => false,
555 }
556 }
557
558 /// The configuration this client was built with.
559 ///
560 /// Note that the returned [`Config`] contains API keys; do not log it.
561 pub fn config(&self) -> &Config {
562 &self.config
563 }
564}
565
566impl Client<ModelReady> {
567 /// Start a request that inherits this client's default model.
568 ///
569 /// Because the model is already known, the returned builder only needs a
570 /// prompt before you can call [`RequestBuilder::generate`]. Override the
571 /// model per request with [`RequestBuilder::model`].
572 pub fn request(&self) -> RequestBuilder<'_, PromptMissing, ModelReady, ModelReady> {
573 self.request_builder()
574 }
575}
576
577enum ToolOverride {
578 Inherit,
579 Replace(Vec<Tool>),
580 Append(Vec<Tool>),
581 None,
582}
583
584struct ResolvedRequest {
585 model: Model,
586 config: GenerationConfig,
587 retry_config: RetryConfig,
588 tool_registry: ToolRegistry,
589 definition_only_tools: Vec<ToolDefinition>,
590}
591
592#[doc(hidden)]
593pub struct PromptMissing;
594
595#[doc(hidden)]
596pub struct PromptReady;
597
598/// Builder for a single AI generation request.
599///
600/// Created by [`Client::request`]. Chain overrides, supply a prompt, then call
601/// one terminal method. Anything you do not override is inherited from the
602/// client.
603///
604/// # Terminal methods
605///
606/// | Method | Returns | Runs registered tools |
607/// | --- | --- | --- |
608/// | [`generate`](Self::generate) | [`Response`] | yes, until a final answer |
609/// | [`generate_once`](Self::generate_once) | [`Response`] | no, one provider call |
610/// | [`generate_structured`](Self::generate_structured) | [`StructuredOutput<T>`] | yes |
611/// | [`generate_structured_once`](Self::generate_structured_once) | [`StructuredOutput<T>`] | no |
612/// | [`generate_with_history`](Self::generate_with_history) | [`Response`] | yes |
613/// | [`stream`](Self::stream) | stream of provider events | not supported |
614/// | [`generate_stream_events`](Self::generate_stream_events) | stream of high-level events | not supported |
615/// | [`stream_wire_events`](Self::stream_wire_events) | proxy-safe wire events | no, definitions are advertised |
616/// | [`stream_accumulated`](Self::stream_accumulated) | [`Response`] | not supported |
617///
618/// Methods ending in `_once` make exactly one provider call and never execute
619/// tools.
620///
621/// # Typestate
622///
623/// The terminal methods only exist once the builder has both a prompt and a
624/// model, so an incomplete request cannot be sent. A model comes either from
625/// the client's default or from [`model`](Self::model); the prompt comes from
626/// [`prompt`](Self::prompt). If `generate` appears to be missing, one of those
627/// two is absent.
628///
629/// # Examples
630///
631/// ```no_run
632/// use rai_sdk::{ClientBuilder, GenerationConfig, Model};
633///
634/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
635/// let client = ClientBuilder::new()
636/// .from_env()
637/// .model(Model::gpt4o_mini())
638/// .build()?;
639///
640/// let response = client
641/// .request()
642/// .model(Model::claude_sonnet_46()) // override the model
643/// .config(GenerationConfig::new().with_temperature(0.2)) // override sampling
644/// .no_tools() // ignore client tools
645/// .prompt("Summarize the borrow checker.")
646/// .generate()
647/// .await?;
648/// # println!("{}", response.text());
649/// # Ok(())
650/// # }
651/// ```
652pub struct RequestBuilder<
653 'a,
654 PromptState = PromptMissing,
655 RequestModelState = ModelMissing,
656 ClientModelState = ModelMissing,
657> {
658 client: &'a Client<ClientModelState>,
659 model: Option<Model>,
660 config: Option<GenerationConfig>,
661 retry_config: Option<RetryConfig>,
662 prompt: Option<Prompt>,
663 tool_override: ToolOverride,
664 definition_only_tools: Vec<ToolDefinition>,
665 prompt_state: PhantomData<PromptState>,
666 model_state: PhantomData<RequestModelState>,
667}
668
669impl<'a, ClientModelState> RequestBuilder<'a, PromptMissing, ClientModelState, ClientModelState> {
670 fn new(client: &'a Client<ClientModelState>) -> Self {
671 Self {
672 client,
673 model: None,
674 config: None,
675 retry_config: None,
676 prompt: None,
677 tool_override: ToolOverride::Inherit,
678 definition_only_tools: Vec::new(),
679 prompt_state: PhantomData,
680 model_state: PhantomData,
681 }
682 }
683}
684
685impl<'a, PromptState, RequestModelState, ClientModelState>
686 RequestBuilder<'a, PromptState, RequestModelState, ClientModelState>
687{
688 fn with_prompt_state<NextPromptState>(
689 self,
690 ) -> RequestBuilder<'a, NextPromptState, RequestModelState, ClientModelState> {
691 RequestBuilder {
692 client: self.client,
693 model: self.model,
694 config: self.config,
695 retry_config: self.retry_config,
696 prompt: self.prompt,
697 tool_override: self.tool_override,
698 definition_only_tools: self.definition_only_tools,
699 prompt_state: PhantomData,
700 model_state: PhantomData,
701 }
702 }
703
704 fn with_model_state<NextRequestModelState>(
705 self,
706 ) -> RequestBuilder<'a, PromptState, NextRequestModelState, ClientModelState> {
707 RequestBuilder {
708 client: self.client,
709 model: self.model,
710 config: self.config,
711 retry_config: self.retry_config,
712 prompt: self.prompt,
713 tool_override: self.tool_override,
714 definition_only_tools: self.definition_only_tools,
715 prompt_state: PhantomData,
716 model_state: PhantomData,
717 }
718 }
719
720 /// Override the model, and therefore the provider, for this request.
721 ///
722 /// Takes precedence over the client's default model. Calling this makes the
723 /// builder model-ready even if the client has no default.
724 pub fn model(
725 mut self,
726 model: Model,
727 ) -> RequestBuilder<'a, PromptState, ModelReady, ClientModelState> {
728 self.model = Some(model);
729 self.with_model_state()
730 }
731
732 /// Override generation settings for this request.
733 ///
734 /// Replaces the client's default [`GenerationConfig`] wholesale rather than
735 /// merging with it, so include every setting you want.
736 pub fn config(mut self, config: GenerationConfig) -> Self {
737 self.config = Some(config);
738 self
739 }
740
741 /// Override the retry configuration for this request.
742 pub fn retry_config(mut self, config: RetryConfig) -> Self {
743 self.retry_config = Some(config);
744 self
745 }
746
747 /// Disable retries for this request.
748 pub fn no_retry(mut self) -> Self {
749 self.retry_config = Some(RetryConfig::none());
750 self
751 }
752
753 /// Set the prompt or conversation history for this request.
754 ///
755 /// Accepts anything convertible into a [`Prompt`]: a `&str`, a `String`, a
756 /// single [`Message`], a `Vec<Message>`, or a full `Prompt` with multi-turn
757 /// history and multimodal content.
758 ///
759 /// # Examples
760 ///
761 /// ```
762 /// use rai_sdk::{Message, Prompt};
763 ///
764 /// // Each of these is accepted by `prompt()`.
765 /// let _: Prompt = "a plain string".into();
766 /// let _: Prompt = Message::user("a single message").into();
767 /// let _: Prompt = vec![
768 /// Message::system("You are terse."),
769 /// Message::user("Explain lifetimes."),
770 /// ]
771 /// .into();
772 ///
773 /// // Or build one up explicitly.
774 /// let _ = Prompt::single(Message::system("You are terse."))
775 /// .with_message(Message::user("Explain lifetimes."));
776 /// ```
777 pub fn prompt<P>(
778 mut self,
779 prompt: P,
780 ) -> RequestBuilder<'a, PromptReady, RequestModelState, ClientModelState>
781 where
782 P: Into<Prompt>,
783 {
784 self.prompt = Some(prompt.into());
785 self.with_prompt_state()
786 }
787
788 /// Replace inherited tools with a single request-specific tool.
789 pub fn tool(mut self, tool: Tool) -> Self {
790 match &mut self.tool_override {
791 ToolOverride::Replace(tools) => tools.push(tool),
792 _ => self.tool_override = ToolOverride::Replace(vec![tool]),
793 }
794 self
795 }
796
797 /// Replace inherited tools with a custom set for this request.
798 pub fn tools<T>(mut self, tools: T) -> Self
799 where
800 T: IntoIterator<Item = Tool>,
801 {
802 let mut collected: Vec<_> = tools.into_iter().collect();
803
804 match &mut self.tool_override {
805 ToolOverride::Replace(existing) => existing.append(&mut collected),
806 _ => self.tool_override = ToolOverride::Replace(collected),
807 }
808
809 self
810 }
811
812 /// Add one more tool while still keeping client-level tools.
813 pub fn additional_tool(mut self, tool: Tool) -> Self {
814 match &mut self.tool_override {
815 ToolOverride::Replace(tools) => tools.push(tool),
816 ToolOverride::Append(tools) => tools.push(tool),
817 ToolOverride::Inherit => self.tool_override = ToolOverride::Append(vec![tool]),
818 ToolOverride::None => self.tool_override = ToolOverride::Replace(vec![tool]),
819 }
820 self
821 }
822
823 /// Add several request-only tools while still keeping client-level tools.
824 pub fn additional_tools<T>(mut self, tools: T) -> Self
825 where
826 T: IntoIterator<Item = Tool>,
827 {
828 let mut collected: Vec<_> = tools.into_iter().collect();
829
830 match &mut self.tool_override {
831 ToolOverride::Replace(existing) => existing.append(&mut collected),
832 ToolOverride::Append(existing) => existing.append(&mut collected),
833 ToolOverride::Inherit => self.tool_override = ToolOverride::Append(collected),
834 ToolOverride::None => self.tool_override = ToolOverride::Replace(collected),
835 }
836
837 self
838 }
839
840 /// Advertise a handler-free tool definition to a proxy client.
841 ///
842 /// Definition-only tools are supported by [`stream_wire_events`](Self::stream_wire_events)
843 /// and [`generate_once`](Self::generate_once), neither of which executes
844 /// tools. Auto-executing methods and non-wire streams reject them;
845 /// [`generate_structured_once`](Self::generate_structured_once) ignores them
846 /// just as it ignores registered tools.
847 pub fn tool_definition(mut self, tool: ToolDefinition) -> Self {
848 self.definition_only_tools.push(tool);
849 self
850 }
851
852 /// Advertise multiple handler-free tool definitions to a proxy client.
853 ///
854 /// See [`tool_definition`](Self::tool_definition) for where definition-only
855 /// tools are supported.
856 pub fn tool_definitions<T>(mut self, tools: T) -> Self
857 where
858 T: IntoIterator<Item = ToolDefinition>,
859 {
860 self.definition_only_tools.extend(tools);
861 self
862 }
863
864 /// Disable all tools for this request, including client defaults.
865 ///
866 /// Also the way to stream from a client that has tools registered, since the
867 /// streaming methods reject any request carrying tools.
868 pub fn no_tools(mut self) -> Self {
869 self.tool_override = ToolOverride::None;
870 self.definition_only_tools.clear();
871 self
872 }
873}
874
875impl<'a, PromptState, ClientModelState>
876 RequestBuilder<'a, PromptState, ModelReady, ClientModelState>
877{
878 fn resolve(&self) -> Result<ResolvedRequest> {
879 let model = self
880 .model
881 .clone()
882 .or_else(|| self.client.default_model.clone())
883 .expect("model-ready request builder must contain or inherit a model");
884
885 let config = self
886 .config
887 .clone()
888 .unwrap_or_else(|| self.client.default_config.clone());
889
890 let tool_registry = match &self.tool_override {
891 ToolOverride::Inherit => self.client.tool_registry.clone(),
892 ToolOverride::Replace(tools) => {
893 let mut registry = ToolRegistry::new();
894 registry.extend(tools.clone())?;
895 registry
896 }
897 ToolOverride::Append(tools) => {
898 let mut registry = self.client.tool_registry.clone();
899 registry.extend(tools.clone())?;
900 registry
901 }
902 ToolOverride::None => ToolRegistry::new(),
903 };
904
905 let retry_config = self
906 .retry_config
907 .clone()
908 .unwrap_or_else(|| self.client.default_retry_config.clone());
909
910 Ok(ResolvedRequest {
911 model,
912 config,
913 retry_config,
914 tool_registry,
915 definition_only_tools: self.definition_only_tools.clone(),
916 })
917 }
918}
919
920impl<'a, ClientModelState> RequestBuilder<'a, PromptReady, ModelReady, ClientModelState> {
921 /// Generate a response, automatically executing any tool calls the model
922 /// requests.
923 ///
924 /// This is the method you usually want. If tools are registered, it runs the
925 /// loop — send, execute requested tools, append results, send again — until
926 /// the model answers without asking for more tools. With no tools
927 /// registered, it is a single call.
928 ///
929 /// Transient failures are retried according to the effective
930 /// [`RetryConfig`].
931 ///
932 /// # Errors
933 ///
934 /// - [`Error::ProviderNotConfigured`] if the provider has no API key, or
935 /// [`Error::ProviderNotEnabled`] if its Cargo feature is off.
936 /// - [`Error::ToolLoopLimitExceeded`] if the model keeps requesting tools
937 /// past [`GenerationConfig::with_max_tool_rounds`] (default 8).
938 /// - [`Error::ToolNotFound`] if the model requests a tool that is not
939 /// registered.
940 /// - [`Error::RateLimit`], [`Error::Timeout`], or [`Error::Http`] if the
941 /// request still fails after retries.
942 /// - [`Error::Auth`], [`Error::InvalidRequest`], [`Error::ContentFiltered`],
943 /// or [`Error::Request`] for provider-side rejections.
944 ///
945 /// Note that a tool handler returning an error does *not* fail this call:
946 /// the error is passed back to the model as tool content so it can react.
947 ///
948 /// # Examples
949 ///
950 /// ```no_run
951 /// use rai_sdk::{ClientBuilder, Model};
952 ///
953 /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
954 /// let client = ClientBuilder::new()
955 /// .from_env()
956 /// .model(Model::gpt4o_mini())
957 /// .build()?;
958 ///
959 /// let response = client
960 /// .request()
961 /// .prompt("Name one Rust testing crate.")
962 /// .generate()
963 /// .await?;
964 ///
965 /// println!("{}", response.text());
966 /// if let Some(usage) = &response.usage {
967 /// println!("tokens: {:?}", usage.total_tokens);
968 /// }
969 /// # Ok(())
970 /// # }
971 /// ```
972 pub async fn generate(self) -> Result<Response> {
973 let resolved = self.resolve()?;
974 ensure_executable_tools_only(&resolved.definition_only_tools)?;
975 let prompt = self
976 .prompt
977 .as_ref()
978 .expect("prompt-ready request builder must contain a prompt");
979 self.client
980 .generate_with_tools(
981 resolved.model,
982 prompt,
983 &resolved.config,
984 &resolved.retry_config,
985 &resolved.tool_registry,
986 )
987 .await
988 }
989
990 /// Make exactly one provider call, without executing tools.
991 ///
992 /// Tool *definitions* are still advertised to the model, so the response may
993 /// contain tool calls — they are returned to you on the response messages
994 /// instead of being executed. Use this when you want to inspect, gate, or
995 /// approve tool calls, or drive the loop yourself.
996 ///
997 /// # Errors
998 ///
999 /// Same as [`generate`](Self::generate), except it cannot return
1000 /// [`Error::ToolLoopLimitExceeded`] or [`Error::ToolNotFound`], since no
1001 /// tool is executed.
1002 ///
1003 /// # Examples
1004 ///
1005 /// ```no_run
1006 /// use rai_sdk::{ClientBuilder, Model};
1007 ///
1008 /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
1009 /// # let client = ClientBuilder::new().from_env().model(Model::gpt4o_mini()).build()?;
1010 /// let response = client
1011 /// .request()
1012 /// .prompt("What is the weather in Paris?")
1013 /// .generate_once()
1014 /// .await?;
1015 ///
1016 /// for message in &response.messages {
1017 /// for call in &message.tool_calls {
1018 /// println!("requested {} with {}", call.name, call.arguments);
1019 /// }
1020 /// }
1021 /// # Ok(())
1022 /// # }
1023 /// ```
1024 pub async fn generate_once(self) -> Result<Response> {
1025 let resolved = self.resolve()?;
1026 let prompt = self
1027 .prompt
1028 .as_ref()
1029 .expect("prompt-ready request builder must contain a prompt");
1030 let tool_definitions =
1031 effective_tool_definitions(&resolved.tool_registry, &resolved.definition_only_tools)?;
1032
1033 crate::retry::with_retry(&resolved.retry_config, "generate_once", || {
1034 self.client.generate_once_internal(
1035 resolved.model.clone(),
1036 prompt,
1037 &resolved.config,
1038 tool_definitions.as_deref(),
1039 )
1040 })
1041 .await
1042 }
1043
1044 /// Generate a response that must match the Rust type `T`.
1045 ///
1046 /// A JSON Schema is generated from `T` and sent to the provider, the
1047 /// response is validated against that schema, and only then deserialized.
1048 /// Tools still run as in [`generate`](Self::generate).
1049 ///
1050 /// `T` must be non-recursive: recursive types force `$ref`/`$defs`, which
1051 /// strict providers reject. See
1052 /// [`GenerationConfig::with_json_schema_for`].
1053 ///
1054 /// # Errors
1055 ///
1056 /// Everything [`generate`](Self::generate) can return, plus
1057 /// [`Error::StructuredOutput`] if the response is empty, is not valid JSON,
1058 /// fails schema validation, or does not deserialize into `T`.
1059 ///
1060 /// # Examples
1061 ///
1062 /// ```no_run
1063 /// use rai_sdk::{ClientBuilder, JsonSchema, Model};
1064 /// use serde::Deserialize;
1065 ///
1066 /// #[derive(Debug, Deserialize, JsonSchema)]
1067 /// struct Summary {
1068 /// title: String,
1069 /// bullet_points: Vec<String>,
1070 /// }
1071 ///
1072 /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
1073 /// # let client = ClientBuilder::new().from_env().model(Model::gpt4o_mini()).build()?;
1074 /// let structured = client
1075 /// .request()
1076 /// .prompt("Summarize the Rust ownership model.")
1077 /// .generate_structured::<Summary>()
1078 /// .await?;
1079 ///
1080 /// println!("{}", structured.output.title);
1081 /// for point in &structured.output.bullet_points {
1082 /// println!("- {point}");
1083 /// }
1084 /// # Ok(())
1085 /// # }
1086 /// ```
1087 pub async fn generate_structured<T>(self) -> Result<StructuredOutput<T>>
1088 where
1089 T: DeserializeOwned + JsonSchema,
1090 {
1091 let resolved = self.resolve()?;
1092 ensure_executable_tools_only(&resolved.definition_only_tools)?;
1093 let prompt = self
1094 .prompt
1095 .as_ref()
1096 .expect("prompt-ready request builder must contain a prompt");
1097 let config = structured_config_for::<T>(&resolved.config)?;
1098 let response = self
1099 .client
1100 .generate_with_tools(
1101 resolved.model,
1102 prompt,
1103 &config,
1104 &resolved.retry_config,
1105 &resolved.tool_registry,
1106 )
1107 .await?;
1108
1109 parse_structured_output(response)
1110 }
1111
1112 /// Make exactly one provider call and parse the result as `T`.
1113 ///
1114 /// Unlike [`generate_once`](Self::generate_once), configured tools are not
1115 /// even advertised to the model: they are ignored entirely (and a log line
1116 /// records that). Use this for a pure transformation on a client that
1117 /// happens to have tools registered.
1118 ///
1119 /// # Errors
1120 ///
1121 /// Same as [`generate_structured`](Self::generate_structured), minus the
1122 /// tool-loop errors.
1123 pub async fn generate_structured_once<T>(self) -> Result<StructuredOutput<T>>
1124 where
1125 T: DeserializeOwned + JsonSchema,
1126 {
1127 let resolved = self.resolve()?;
1128 let prompt = self
1129 .prompt
1130 .as_ref()
1131 .expect("prompt-ready request builder must contain a prompt");
1132 let config = structured_config_for::<T>(&resolved.config)?;
1133 let ignored_tool_count =
1134 resolved.tool_registry.definitions().len() + resolved.definition_only_tools.len();
1135 if ignored_tool_count > 0 {
1136 info!(
1137 tool_count = ignored_tool_count,
1138 "Ignoring configured tools for generate_structured_once; use generate_structured for tool loops"
1139 );
1140 }
1141
1142 let response =
1143 crate::retry::with_retry(&resolved.retry_config, "generate_structured_once", || {
1144 self.client
1145 .generate_once_internal(resolved.model.clone(), prompt, &config, None)
1146 })
1147 .await?;
1148
1149 parse_structured_output(response)
1150 }
1151
1152 /// Generate a response with prior conversation turns prepended.
1153 ///
1154 /// A convenience over assembling the history into the [`Prompt`] yourself:
1155 /// each [`ConversationTurn`](crate::message::ConversationTurn) contributes
1156 /// its user message, assistant message, and any tool results, followed by
1157 /// this request's prompt. Tools run as in [`generate`](Self::generate).
1158 ///
1159 /// # Errors
1160 ///
1161 /// Same as [`generate`](Self::generate).
1162 pub async fn generate_with_history(
1163 self,
1164 history: &[crate::message::ConversationTurn],
1165 ) -> Result<Response> {
1166 let resolved = self.resolve()?;
1167 ensure_executable_tools_only(&resolved.definition_only_tools)?;
1168 let prompt = self
1169 .prompt
1170 .as_ref()
1171 .expect("prompt-ready request builder must contain a prompt")
1172 .clone()
1173 .with_history(history.to_vec());
1174
1175 self.client
1176 .generate_with_tools(
1177 resolved.model,
1178 &prompt,
1179 &resolved.config,
1180 &resolved.retry_config,
1181 &resolved.tool_registry,
1182 )
1183 .await
1184 }
1185
1186 /// Stream the response as high-level [`StreamEvent`](crate::message::StreamEvent)s.
1187 ///
1188 /// Higher level than [`stream`](Self::stream): text deltas are passed
1189 /// through, tool-call argument fragments are buffered and emitted as whole
1190 /// calls, and a final `TurnComplete` event carries the assembled
1191 /// [`ConversationTurn`](crate::message::ConversationTurn) — convenient for
1192 /// feeding conversation history back into a later request.
1193 ///
1194 /// Registered tools are *not* executed; this only reports what the model
1195 /// asked for.
1196 ///
1197 /// To forward these events to a remote client instead of consuming them in
1198 /// process, see [`stream_wire_events`](Self::stream_wire_events);
1199 /// [`WireStreamEvent`](crate::wire::WireStreamEvent) also implements
1200 /// `From<StreamEvent>` if you would rather convert these.
1201 ///
1202 /// # Cancellation
1203 ///
1204 /// Dropping the returned stream aborts the upstream provider request. See
1205 /// the "Cancellation" section of [`stream`](Self::stream).
1206 ///
1207 /// # Errors
1208 ///
1209 /// Same as [`stream`](Self::stream), including [`Error::InvalidRequest`]
1210 /// when the request's effective tool set is non-empty. Once the stream is
1211 /// open, individual items may also be errors.
1212 pub async fn generate_stream_events(
1213 self,
1214 ) -> Result<impl Stream<Item = Result<crate::message::StreamEvent>> + Send> {
1215 let resolved = self.resolve()?;
1216 let prompt = self
1217 .prompt
1218 .as_ref()
1219 .expect("prompt-ready request builder must contain a prompt");
1220
1221 ensure_streamable(&resolved.tool_registry, &resolved.definition_only_tools)?;
1222
1223 let mut stream = crate::retry::with_retry(&resolved.retry_config, "stream", || {
1224 self.client.generate_stream_inner(
1225 resolved.model.clone(),
1226 prompt,
1227 &resolved.config,
1228 None,
1229 )
1230 })
1231 .await?;
1232
1233 let user_message = prompt
1234 .messages
1235 .last()
1236 .cloned()
1237 .unwrap_or_else(|| crate::message::Message::user(""));
1238
1239 let stream_events = async_stream::stream! {
1240 let mut accumulated_content = String::new();
1241 let mut current_tool_id: Option<String> = None;
1242 let mut current_tool_name: Option<String> = None;
1243 let mut current_tool_args = String::new();
1244 let mut tool_calls = Vec::new();
1245
1246 while let Some(chunk_result) = stream.next().await {
1247 match chunk_result {
1248 Ok(chunk) => {
1249 match chunk {
1250 crate::provider::ProviderStreamEvent::Text(text) => {
1251 accumulated_content.push_str(&text);
1252 yield Ok(crate::message::StreamEvent::TextDelta { text });
1253 }
1254 crate::provider::ProviderStreamEvent::ToolCallStart { id, name } => {
1255 if let (Some(tid), Some(tname)) = (current_tool_id.take(), current_tool_name.take()) {
1256 let args_json = serde_json::from_str(¤t_tool_args).unwrap_or(serde_json::Value::Null);
1257 tool_calls.push(crate::message::ToolCall {
1258 id: tid.clone(),
1259 name: tname.clone(),
1260 arguments: args_json,
1261 });
1262 yield Ok(crate::message::StreamEvent::ToolCall {
1263 id: tid,
1264 name: tname,
1265 arguments: current_tool_args.clone(),
1266 });
1267 current_tool_args.clear();
1268 }
1269 current_tool_id = Some(id);
1270 current_tool_name = Some(name);
1271 }
1272 crate::provider::ProviderStreamEvent::ToolCallChunk { id: _, arguments } => {
1273 current_tool_args.push_str(&arguments);
1274 }
1275 crate::provider::ProviderStreamEvent::Done { finish_reason: _, usage: _ } => {
1276 if let (Some(tid), Some(tname)) = (current_tool_id.take(), current_tool_name.take()) {
1277 let args_json = serde_json::from_str(¤t_tool_args).unwrap_or(serde_json::Value::Null);
1278 tool_calls.push(crate::message::ToolCall {
1279 id: tid.clone(),
1280 name: tname.clone(),
1281 arguments: args_json,
1282 });
1283 yield Ok(crate::message::StreamEvent::ToolCall {
1284 id: tid,
1285 name: tname,
1286 arguments: current_tool_args.clone(),
1287 });
1288 }
1289
1290 let mut assistant_message = crate::message::Message::assistant(accumulated_content.clone());
1291 assistant_message.tool_calls = tool_calls.clone();
1292
1293 let turn = crate::message::ConversationTurn {
1294 user_message: user_message.clone(),
1295 assistant_message,
1296 tool_results: Vec::new(),
1297 };
1298 yield Ok(crate::message::StreamEvent::TurnComplete { turn });
1299 }
1300 }
1301 }
1302 Err(e) => {
1303 yield Err(e);
1304 }
1305 }
1306 }
1307 };
1308
1309 Ok(stream_events)
1310 }
1311
1312 /// Stream the response as serializable
1313 /// [`WireStreamEvent`](crate::wire::WireStreamEvent)s, ready to forward to a
1314 /// remote client.
1315 ///
1316 /// This is the SDK half of the proxy pattern: your server holds the
1317 /// provider credentials, calls this, and re-emits each event as an SSE
1318 /// `data:` payload; the client parses them back into `WireStreamEvent`s and
1319 /// rebuilds the response with
1320 /// [`StreamAccumulator`](crate::wire::StreamAccumulator). See the
1321 /// [`wire`](crate::wire) module for the format and its compatibility
1322 /// guarantees, and `examples/sse_proxy.rs` for the whole loop.
1323 ///
1324 /// # Stream shape
1325 ///
1326 /// Unlike the other streaming methods, items are **not** `Result`s. Once the
1327 /// stream is open every outcome is an event, so a mid-stream provider
1328 /// failure reaches the client as
1329 /// [`WireStreamEvent::Error`](crate::wire::WireStreamEvent::Error) instead
1330 /// of as a silently truncated response. The sequence is:
1331 ///
1332 /// 1. exactly one
1333 /// [`MessageStart`](crate::wire::WireStreamEvent::MessageStart);
1334 /// 2. any number of text and tool-call events;
1335 /// 3. one [`Usage`](crate::wire::WireStreamEvent::Usage), when the provider
1336 /// reported token counts;
1337 /// 4. exactly one terminal event —
1338 /// [`MessageStop`](crate::wire::WireStreamEvent::MessageStop) on success,
1339 /// [`Error`](crate::wire::WireStreamEvent::Error) on failure.
1340 ///
1341 /// Tool-call arguments are reported twice over: incrementally as
1342 /// [`ToolCallStart`](crate::wire::WireStreamEvent::ToolCallStart) plus
1343 /// [`ToolCallDelta`](crate::wire::WireStreamEvent::ToolCallDelta) so a UI can
1344 /// render progress, then once assembled as
1345 /// [`ToolCallEnd`](crate::wire::WireStreamEvent::ToolCallEnd). A client that
1346 /// only wants finished calls can ignore the first two.
1347 ///
1348 /// Registered and definition-only tools are advertised to the provider but
1349 /// are *not* executed. Tool calls flow through the returned stream so the
1350 /// proxy's client can execute them and send the results in a later request.
1351 ///
1352 /// # Cancellation
1353 ///
1354 /// Dropping the returned stream aborts the upstream provider request. See
1355 /// the "Cancellation" section of [`stream`](Self::stream) — it matters more
1356 /// here than anywhere else, because for a proxy the consumer being dropped
1357 /// *is* the end client hanging up.
1358 ///
1359 /// # Errors
1360 ///
1361 /// The returned `Result` covers only failures that happen before the stream
1362 /// opens, such as provider configuration, transport, or invalid tool
1363 /// definitions. Unlike the other streaming methods, a non-empty effective
1364 /// tool set is supported. A server that wants its client to see opening
1365 /// failures too can forward them with
1366 /// [`WireStreamEvent::error`](crate::wire::WireStreamEvent::error).
1367 ///
1368 /// # Examples
1369 ///
1370 /// ```no_run
1371 /// use futures::StreamExt;
1372 /// use rai_sdk::{ClientBuilder, Model};
1373 ///
1374 /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
1375 /// # let client = ClientBuilder::new().from_env().model(Model::gpt4o_mini()).build()?;
1376 /// let mut events = client
1377 /// .request()
1378 /// .prompt("Summarize the news.")
1379 /// .stream_wire_events()
1380 /// .await?;
1381 ///
1382 /// while let Some(event) = events.next().await {
1383 /// // `data: {"type":"text_delta","text":"..."}`
1384 /// println!("data: {}\n", serde_json::to_string(&event)?);
1385 /// }
1386 /// # Ok(())
1387 /// # }
1388 /// ```
1389 pub async fn stream_wire_events(
1390 self,
1391 ) -> Result<Pin<Box<dyn Stream<Item = crate::wire::WireStreamEvent> + Send>>> {
1392 use crate::wire::WireStreamEvent;
1393
1394 let resolved = self.resolve()?;
1395 let prompt = self
1396 .prompt
1397 .as_ref()
1398 .expect("prompt-ready request builder must contain a prompt");
1399
1400 let tool_definitions =
1401 effective_tool_definitions(&resolved.tool_registry, &resolved.definition_only_tools)?;
1402
1403 let model_str = resolved.model.as_str().to_string();
1404 let provider = resolved.model.provider();
1405
1406 let mut stream = crate::retry::with_retry(&resolved.retry_config, "stream", || {
1407 self.client.generate_stream_inner(
1408 resolved.model.clone(),
1409 prompt,
1410 &resolved.config,
1411 tool_definitions.as_deref(),
1412 )
1413 })
1414 .await?;
1415
1416 let wire_events = async_stream::stream! {
1417 yield WireStreamEvent::message_start(model_str, provider);
1418
1419 // The assembled `ToolCallEnd` for the call currently streaming.
1420 // Providers do not delimit tool calls explicitly, so a call is
1421 // closed by the next `ToolCallStart` or by the end of the stream.
1422 let mut pending_tool: Option<(String, String, String)> = None;
1423 let mut finish_reason: Option<String> = None;
1424 let mut usage: Option<crate::message::Usage> = None;
1425 let mut failed = false;
1426
1427 while let Some(chunk_result) = stream.next().await {
1428 let chunk = match chunk_result {
1429 Ok(chunk) => chunk,
1430 Err(error) => {
1431 // Terminal: a provider failure is an event, not a
1432 // dropped connection.
1433 yield WireStreamEvent::error(&error);
1434 failed = true;
1435 break;
1436 }
1437 };
1438
1439 match chunk {
1440 crate::provider::ProviderStreamEvent::Text(text) => {
1441 yield WireStreamEvent::TextDelta { text };
1442 }
1443
1444 crate::provider::ProviderStreamEvent::ToolCallStart { id, name } => {
1445 if let Some((prev_id, prev_name, prev_args)) = pending_tool.take() {
1446 yield WireStreamEvent::ToolCallEnd {
1447 id: prev_id,
1448 name: prev_name,
1449 arguments: prev_args,
1450 };
1451 }
1452 pending_tool = Some((id.clone(), name.clone(), String::new()));
1453 yield WireStreamEvent::ToolCallStart { id, name };
1454 }
1455
1456 crate::provider::ProviderStreamEvent::ToolCallChunk { id, arguments } => {
1457 // Some providers omit the id on continuation chunks;
1458 // attribute those to the call already in flight.
1459 let id = match (&pending_tool, id.is_empty()) {
1460 (Some((pending_id, _, _)), true) => pending_id.clone(),
1461 _ => id,
1462 };
1463 if let Some((pending_id, _, pending_args)) = pending_tool.as_mut() {
1464 if *pending_id == id {
1465 pending_args.push_str(&arguments);
1466 }
1467 }
1468 yield WireStreamEvent::ToolCallDelta { id, arguments };
1469 }
1470
1471 crate::provider::ProviderStreamEvent::Done {
1472 finish_reason: reason,
1473 usage: reported,
1474 } => {
1475 if let Some((id, name, arguments)) = pending_tool.take() {
1476 yield WireStreamEvent::ToolCallEnd { id, name, arguments };
1477 }
1478 // Providers may split the finish reason and the usage
1479 // across separate `Done` events, so keep the last of
1480 // each rather than emitting one terminal event per
1481 // `Done`.
1482 if reason.is_some() {
1483 finish_reason = reason;
1484 }
1485 if reported.is_some() {
1486 usage = reported;
1487 }
1488 }
1489 }
1490 }
1491
1492 if failed {
1493 return;
1494 }
1495
1496 if let Some((id, name, arguments)) = pending_tool.take() {
1497 yield WireStreamEvent::ToolCallEnd { id, name, arguments };
1498 }
1499 if let Some(usage) = usage {
1500 yield WireStreamEvent::Usage { usage };
1501 }
1502 yield WireStreamEvent::MessageStop { finish_reason };
1503 };
1504
1505 // Boxed rather than `impl Stream` so the result borrows nothing and is
1506 // `'static`. A proxy handler builds this from a shared `Client` and
1507 // hands it straight to its web framework, which needs an owned,
1508 // lifetime-free stream.
1509 Ok(Box::pin(wire_events))
1510 }
1511
1512 /// Stream raw provider events as they arrive.
1513 ///
1514 /// Use this to render output incrementally. Each item is a [`Result`], since
1515 /// a stream can fail partway through — do not discard the error case, or a
1516 /// mid-stream failure will look like a clean end of output.
1517 ///
1518 /// # Cancellation
1519 ///
1520 /// **Dropping the stream aborts the upstream provider request.** Every
1521 /// streaming method in this crate is driven entirely by the consumer: the
1522 /// provider's HTTP response body is polled from inside the returned stream,
1523 /// never from a detached background task. Dropping the stream therefore
1524 /// drops the response body and closes the underlying connection, and the
1525 /// provider stops generating. Nothing keeps running in the background and
1526 /// no tokens are burned on output nobody will read.
1527 ///
1528 /// Two consequences worth planning for:
1529 ///
1530 /// - A generation cancelled this way produces **no terminal event** — no
1531 /// `Done`, no usage. Providers bill for what they generated before the
1532 /// abort, so a server that meters usage cannot rely on the final usage
1533 /// event alone.
1534 /// - Cancellation propagates through wrappers. Dropping the future or
1535 /// stream returned by [`generate_stream_events`](Self::generate_stream_events),
1536 /// [`stream_wire_events`](Self::stream_wire_events), or
1537 /// [`stream_accumulated`](Self::stream_accumulated) — including when the
1538 /// whole task is cancelled by `tokio::time::timeout` or by an axum client
1539 /// disconnect — aborts the provider request just the same.
1540 ///
1541 /// # Errors
1542 ///
1543 /// Returns [`Error::InvalidRequest`] if the request would carry any tool,
1544 /// because streaming cannot run a tool loop. This considers the request's
1545 /// effective tool set, so [`no_tools`](Self::no_tools) lets you stream from
1546 /// a client that has tools registered, and [`tool`](Self::tool) on the
1547 /// request is rejected even when the client itself has none.
1548 ///
1549 /// Otherwise the same causes as [`Client::generate_stream`]:
1550 /// [`Error::ProviderNotConfigured`], [`Error::ProviderNotEnabled`], or a
1551 /// transport or provider failure. Once the stream is open, individual items
1552 /// may also be errors.
1553 ///
1554 /// # Examples
1555 ///
1556 /// ```no_run
1557 /// use futures::StreamExt;
1558 /// use rai_sdk::{ClientBuilder, Model, provider::ProviderStreamEvent};
1559 ///
1560 /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
1561 /// # let client = ClientBuilder::new().from_env().model(Model::gpt4o_mini()).build()?;
1562 /// let mut stream = client
1563 /// .request()
1564 /// .prompt("Count from one to five.")
1565 /// .stream()
1566 /// .await?;
1567 ///
1568 /// while let Some(event) = stream.next().await {
1569 /// match event? {
1570 /// ProviderStreamEvent::Text(text) => print!("{text}"),
1571 /// ProviderStreamEvent::Done { .. } => println!(),
1572 /// _ => {}
1573 /// }
1574 /// }
1575 /// # Ok(())
1576 /// # }
1577 /// ```
1578 pub async fn stream(
1579 self,
1580 ) -> Result<Pin<Box<dyn Stream<Item = Result<crate::provider::ProviderStreamEvent>> + Send>>>
1581 {
1582 let resolved = self.resolve()?;
1583 let prompt = self
1584 .prompt
1585 .as_ref()
1586 .expect("prompt-ready request builder must contain a prompt");
1587
1588 ensure_streamable(&resolved.tool_registry, &resolved.definition_only_tools)?;
1589
1590 crate::retry::with_retry(&resolved.retry_config, "stream", || {
1591 self.client.generate_stream_inner(
1592 resolved.model.clone(),
1593 prompt,
1594 &resolved.config,
1595 None,
1596 )
1597 })
1598 .await
1599 }
1600
1601 /// Stream internally and return one complete [`Response`].
1602 ///
1603 /// Uses the streaming transport (lower time-to-first-byte, and less likely
1604 /// to sit near a timeout on long generations) but consumes every chunk for
1605 /// you, so the result is shaped exactly like [`generate`](Self::generate).
1606 /// Reach for this when you want streaming's latency behavior without
1607 /// handling events.
1608 ///
1609 /// Only text and the terminating event are accumulated, so tool calls are
1610 /// not represented in the returned response.
1611 ///
1612 /// # Cancellation
1613 ///
1614 /// Dropping the returned future aborts the upstream provider request. See
1615 /// the "Cancellation" section of [`stream`](Self::stream).
1616 ///
1617 /// # Errors
1618 ///
1619 /// Same as [`stream`](Self::stream), including [`Error::InvalidRequest`]
1620 /// when the request's effective tool set is non-empty, plus any error
1621 /// encountered while consuming the stream.
1622 ///
1623 /// # Examples
1624 ///
1625 /// ```no_run
1626 /// use rai_sdk::{ClientBuilder, Model};
1627 ///
1628 /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
1629 /// # let client = ClientBuilder::new().from_env().model(Model::gpt4o_mini()).build()?;
1630 /// let response = client
1631 /// .request()
1632 /// .prompt("Write a short launch announcement.")
1633 /// .stream_accumulated()
1634 /// .await?;
1635 ///
1636 /// println!("{}", response.text());
1637 /// # Ok(())
1638 /// # }
1639 /// ```
1640 pub async fn stream_accumulated(self) -> Result<Response> {
1641 let resolved = self.resolve()?;
1642 let prompt = self
1643 .prompt
1644 .as_ref()
1645 .expect("prompt-ready request builder must contain a prompt");
1646
1647 ensure_streamable(&resolved.tool_registry, &resolved.definition_only_tools)?;
1648
1649 let model_str = resolved.model.as_str().to_string();
1650 let provider = resolved.model.provider();
1651
1652 let mut stream = crate::retry::with_retry(&resolved.retry_config, "stream", || {
1653 self.client.generate_stream_inner(
1654 resolved.model.clone(),
1655 prompt,
1656 &resolved.config,
1657 None,
1658 )
1659 })
1660 .await?;
1661
1662 let mut accumulated_content = String::new();
1663 let mut finish_reason = None;
1664 let mut usage = None;
1665
1666 while let Some(chunk_result) = stream.next().await {
1667 let chunk = chunk_result?;
1668 match chunk {
1669 crate::provider::ProviderStreamEvent::Text(text) => {
1670 accumulated_content.push_str(&text);
1671 }
1672 crate::provider::ProviderStreamEvent::Done {
1673 finish_reason: fr,
1674 usage: u,
1675 } => {
1676 if fr.is_some() {
1677 finish_reason = fr;
1678 }
1679 if u.is_some() {
1680 usage = u;
1681 }
1682 }
1683 _ => {}
1684 }
1685 }
1686
1687 Ok(Response {
1688 messages: vec![Message::assistant(accumulated_content)],
1689 usage,
1690 model: model_str,
1691 provider,
1692 finish_reason,
1693 })
1694 }
1695}
1696
1697/// Reject a streaming request when tools are in play.
1698///
1699/// Streaming has no way to execute a tool loop, since that requires issuing
1700/// follow-up requests. Failing loudly is better than quietly dropping tools the
1701/// caller registered.
1702fn ensure_streamable(
1703 tool_registry: &ToolRegistry,
1704 definition_only_tools: &[ToolDefinition],
1705) -> Result<()> {
1706 if tool_registry.is_empty() && definition_only_tools.is_empty() {
1707 return Ok(());
1708 }
1709
1710 Err(Error::InvalidRequest(
1711 "Streaming with tools is not supported. Use generate() to run tools, \
1712 or no_tools() on the request to stream without them."
1713 .into(),
1714 ))
1715}
1716
1717fn ensure_executable_tools_only(definition_only_tools: &[ToolDefinition]) -> Result<()> {
1718 if definition_only_tools.is_empty() {
1719 return Ok(());
1720 }
1721
1722 Err(Error::InvalidRequest(
1723 "Definition-only tools cannot be auto-executed. Use generate_once() or \
1724 stream_wire_events(), or register Tool values with handlers."
1725 .into(),
1726 ))
1727}
1728
1729fn effective_tool_definitions(
1730 tool_registry: &ToolRegistry,
1731 definition_only_tools: &[ToolDefinition],
1732) -> Result<Option<Vec<ToolDefinition>>> {
1733 let mut definitions = tool_registry.definitions();
1734 definitions.extend_from_slice(definition_only_tools);
1735
1736 if definitions.is_empty() {
1737 return Ok(None);
1738 }
1739
1740 let mut names = HashSet::new();
1741 for definition in &definitions {
1742 if definition.name.trim().is_empty() {
1743 return Err(Error::InvalidRequest(
1744 "Tool name cannot be empty".to_string(),
1745 ));
1746 }
1747 if !names.insert(&definition.name) {
1748 return Err(Error::InvalidRequest(format!(
1749 "Tool '{}' is already registered",
1750 definition.name
1751 )));
1752 }
1753 }
1754
1755 Ok(Some(definitions))
1756}
1757
1758fn structured_config_for<T>(config: &GenerationConfig) -> Result<GenerationConfig>
1759where
1760 T: JsonSchema,
1761{
1762 let mut config = config.clone();
1763 config.json_schema = Some(structured_schema_for::<T>()?);
1764 Ok(config)
1765}
1766
1767fn parse_structured_output<T>(response: Response) -> Result<StructuredOutput<T>>
1768where
1769 T: DeserializeOwned + JsonSchema,
1770{
1771 let provider = response.provider;
1772 let model = response.model.clone();
1773 let content = response
1774 .messages
1775 .first()
1776 .map(|message| message.content.trim())
1777 .unwrap_or_default();
1778
1779 if content.is_empty() {
1780 error!(provider = %provider, model = %model, "Structured output was empty");
1781 return Err(Error::StructuredOutput {
1782 provider,
1783 model,
1784 message: "response content was empty".to_string(),
1785 });
1786 }
1787
1788 let instance = serde_json::from_str::<serde_json::Value>(content).map_err(|parse_error| {
1789 error!(
1790 provider = %provider,
1791 model = %model,
1792 error = %parse_error,
1793 response_content = %content,
1794 "Structured output was not valid JSON"
1795 );
1796 Error::StructuredOutput {
1797 provider,
1798 model: model.clone(),
1799 message: parse_error.to_string(),
1800 }
1801 })?;
1802
1803 let schema = structured_schema_for::<T>()?;
1804
1805 if let Err(validation_error) = jsonschema::validate(&schema, &instance) {
1806 error!(
1807 provider = %provider,
1808 model = %model,
1809 error = %validation_error,
1810 response_content = %content,
1811 response_schema = ?schema,
1812 "Structured output failed JSON schema validation"
1813 );
1814 return Err(Error::StructuredOutput {
1815 provider,
1816 model,
1817 message: validation_error.to_string(),
1818 });
1819 }
1820
1821 match serde_json::from_str::<T>(content) {
1822 Ok(output) => {
1823 debug!(
1824 provider = %provider,
1825 model = %model,
1826 output_type = %type_name::<T>(),
1827 "Structured output validated successfully"
1828 );
1829 Ok(StructuredOutput { output, response })
1830 }
1831 Err(parse_error) => {
1832 error!(
1833 provider = %provider,
1834 model = %model,
1835 error = %parse_error,
1836 response_content = %content,
1837 "Structured output validation failed"
1838 );
1839 Err(Error::StructuredOutput {
1840 provider,
1841 model,
1842 message: parse_error.to_string(),
1843 })
1844 }
1845 }
1846}
1847
1848fn structured_schema_for<T>() -> Result<serde_json::Value>
1849where
1850 T: JsonSchema,
1851{
1852 GenerationConfig::new()
1853 .with_json_schema_for::<T>()
1854 .map(|config| {
1855 config
1856 .json_schema
1857 .expect("structured schema should be present")
1858 })
1859}
1860
1861/// Builder for creating a [`Client`].
1862///
1863/// Set credentials (usually with [`from_env`](Self::from_env)), then optionally
1864/// a default model, generation config, retry policy, and shared tools, and
1865/// finish with [`build`](Self::build).
1866///
1867/// Explicit setters win over the environment regardless of chain order relative
1868/// to `from_env()`, because `from_env()` replaces the accumulated config — so
1869/// call it first.
1870///
1871/// # Examples
1872///
1873/// ```no_run
1874/// use std::time::Duration;
1875///
1876/// use rai_sdk::{ClientBuilder, GenerationConfig, Model, RetryConfig};
1877///
1878/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1879/// let client = ClientBuilder::new()
1880/// .from_env()
1881/// .model(Model::gpt4o_mini())
1882/// .config(GenerationConfig::new().with_max_tokens(1024))
1883/// .retry_config(RetryConfig::new().with_initial_delay(Duration::from_millis(250)))
1884/// .timeout(60)
1885/// .build()?;
1886/// # let _ = client;
1887/// # Ok(())
1888/// # }
1889/// ```
1890pub struct ClientBuilder<ModelState = ModelMissing> {
1891 config: Config,
1892 default_model: Option<Model>,
1893 default_config: GenerationConfig,
1894 default_retry_config: RetryConfig,
1895 tools: Vec<Tool>,
1896 state: PhantomData<ModelState>,
1897}
1898
1899impl<ModelState> std::fmt::Debug for ClientBuilder<ModelState> {
1900 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1901 f.debug_struct("ClientBuilder")
1902 .field("default_model", &self.default_model)
1903 .field("default_config", &self.default_config)
1904 .field("tools_count", &self.tools.len())
1905 .finish()
1906 }
1907}
1908
1909impl ClientBuilder<ModelMissing> {
1910 /// Start a client builder.
1911 pub fn new() -> Self {
1912 Self {
1913 config: Config::new(),
1914 default_model: None,
1915 default_config: GenerationConfig::default(),
1916 default_retry_config: RetryConfig::default(),
1917 tools: Vec::new(),
1918 state: PhantomData,
1919 }
1920 }
1921}
1922
1923impl<ModelState> ClientBuilder<ModelState> {
1924 fn with_state<NextModelState>(self) -> ClientBuilder<NextModelState> {
1925 ClientBuilder {
1926 config: self.config,
1927 default_model: self.default_model,
1928 default_config: self.default_config,
1929 default_retry_config: self.default_retry_config,
1930 tools: self.tools,
1931 state: PhantomData,
1932 }
1933 }
1934
1935 /// Load configuration from environment variables.
1936 ///
1937 /// Reads the API keys, base URLs, timeout, and retry variables documented in
1938 /// [`config`](crate::config). This **replaces** any configuration already
1939 /// accumulated on the builder, so call it first and then override
1940 /// individual values.
1941 pub fn from_env(mut self) -> Self {
1942 self.config = Config::from_env();
1943 self.default_retry_config = self.config.retry_config();
1944 self
1945 }
1946
1947 /// Set the OpenAI API key.
1948 pub fn openai_key(mut self, key: impl Into<String>) -> Self {
1949 self.config.openai_api_key = Some(key.into());
1950 self
1951 }
1952
1953 /// Set the OpenAI base URL.
1954 pub fn openai_base_url(mut self, url: impl Into<String>) -> Self {
1955 self.config.openai_base_url = Some(url.into());
1956 self
1957 }
1958
1959 /// Point this client at an OpenAI-compatible endpoint.
1960 ///
1961 /// The URL is the API root serving `POST /chat/completions`, so it usually
1962 /// ends in `/v1` — `http://localhost:8000/v1` for vLLM,
1963 /// `http://localhost:1234/v1` for LM Studio. Setting it is what makes
1964 /// [`ProviderKind::OpenAICompatible`] available; there is no default
1965 /// endpoint and no environment variable, because the endpoint is a property
1966 /// of this client rather than of the process. See
1967 /// [`Config::openai_compatible_base_url`](crate::Config::openai_compatible_base_url).
1968 ///
1969 /// # Examples
1970 ///
1971 /// Two endpoints in one process, each with its own client:
1972 ///
1973 /// ```no_run
1974 /// use rai_sdk::{ClientBuilder, Model};
1975 ///
1976 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1977 /// let local = ClientBuilder::new()
1978 /// .ollama()
1979 /// .model(Model::openai_compatible("llama3.1:8b"))
1980 /// .build()?;
1981 ///
1982 /// let cluster = ClientBuilder::new()
1983 /// .openai_compatible_base_url("https://vllm.internal.example/v1")
1984 /// .openai_compatible_key("shared-secret")
1985 /// .model(Model::openai_compatible("Qwen/Qwen2.5-7B-Instruct"))
1986 /// .build()?;
1987 /// # let _ = (local, cluster);
1988 /// # Ok(())
1989 /// # }
1990 /// ```
1991 pub fn openai_compatible_base_url(mut self, url: impl Into<String>) -> Self {
1992 self.config.openai_compatible_base_url = Some(url.into());
1993 self
1994 }
1995
1996 /// Set the bearer token for the OpenAI-compatible endpoint.
1997 ///
1998 /// Optional. With no key set, requests carry no `Authorization` header at
1999 /// all, which is what a local runtime expects.
2000 pub fn openai_compatible_key(mut self, key: impl Into<String>) -> Self {
2001 self.config.openai_compatible_api_key = Some(key.into());
2002 self
2003 }
2004
2005 /// Declare what the OpenAI-compatible endpoint supports.
2006 ///
2007 /// Requests needing something it was declared not to support fail with
2008 /// [`Error::CapabilityUnsupported`] before any HTTP call, instead of
2009 /// reaching the endpoint and coming back as an opaque bad request.
2010 ///
2011 /// # Examples
2012 ///
2013 /// ```no_run
2014 /// use rai_sdk::{ClientBuilder, EndpointCapabilities, Model};
2015 ///
2016 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
2017 /// let client = ClientBuilder::new()
2018 /// .ollama()
2019 /// .openai_compatible_capabilities(
2020 /// EndpointCapabilities::default().with_tool_calling(false),
2021 /// )
2022 /// .model(Model::openai_compatible("gemma3:4b"))
2023 /// .build()?;
2024 /// # let _ = client;
2025 /// # Ok(())
2026 /// # }
2027 /// ```
2028 pub fn openai_compatible_capabilities(
2029 mut self,
2030 capabilities: crate::config::EndpointCapabilities,
2031 ) -> Self {
2032 self.config.openai_compatible_capabilities = Some(capabilities);
2033 self
2034 }
2035
2036 /// Point this client at a local Ollama server.
2037 ///
2038 /// Shorthand for
2039 /// [`openai_compatible_base_url`](Self::openai_compatible_base_url) with
2040 /// [`OLLAMA_BASE_URL`](crate::config::OLLAMA_BASE_URL)
2041 /// (`http://localhost:11434/v1`). Pass the URL explicitly for any other
2042 /// host or port.
2043 pub fn ollama(self) -> Self {
2044 self.openai_compatible_base_url(crate::config::OLLAMA_BASE_URL)
2045 }
2046
2047 /// Set the Anthropic API key.
2048 pub fn anthropic_key(mut self, key: impl Into<String>) -> Self {
2049 self.config.anthropic_api_key = Some(key.into());
2050 self
2051 }
2052
2053 /// Set the Anthropic base URL.
2054 pub fn anthropic_base_url(mut self, url: impl Into<String>) -> Self {
2055 self.config.anthropic_base_url = Some(url.into());
2056 self
2057 }
2058
2059 /// Set the OpenRouter API key.
2060 pub fn openrouter_key(mut self, key: impl Into<String>) -> Self {
2061 self.config.openrouter_api_key = Some(key.into());
2062 self
2063 }
2064
2065 /// Set the OpenRouter base URL.
2066 pub fn openrouter_base_url(mut self, url: impl Into<String>) -> Self {
2067 self.config.openrouter_base_url = Some(url.into());
2068 self
2069 }
2070
2071 /// Set the OpenRouter HTTP referer attribution header.
2072 pub fn openrouter_http_referer(mut self, referer: impl Into<String>) -> Self {
2073 self.config.openrouter_http_referer = Some(referer.into());
2074 self
2075 }
2076
2077 /// Set the OpenRouter title attribution header.
2078 pub fn openrouter_title(mut self, title: impl Into<String>) -> Self {
2079 self.config.openrouter_title = Some(title.into());
2080 self
2081 }
2082
2083 /// Set OpenRouter app categories attribution header.
2084 pub fn openrouter_categories(mut self, categories: Vec<String>) -> Self {
2085 self.config.openrouter_categories = Some(categories);
2086 self
2087 }
2088
2089 /// Set the OpenRouter App URL.
2090 pub fn openrouter_app_url(mut self, url: impl Into<String>) -> Self {
2091 let url = url.into();
2092 self.config.openrouter_app_url = Some(url.clone());
2093 self.config.openrouter_http_referer = Some(url);
2094 self
2095 }
2096
2097 /// Set the OpenRouter App Title.
2098 pub fn openrouter_app_title(mut self, title: impl Into<String>) -> Self {
2099 let title = title.into();
2100 self.config.openrouter_app_title = Some(title.clone());
2101 self.config.openrouter_title = Some(title);
2102 self
2103 }
2104
2105 /// Set the request timeout.
2106 pub fn timeout(mut self, seconds: u64) -> Self {
2107 self.config.timeout_seconds = Some(seconds);
2108 self
2109 }
2110
2111 /// Set the default model used by request builders.
2112 ///
2113 /// This also moves the builder into the model-ready state, so the resulting
2114 /// client can start requests that need only a prompt. Individual requests
2115 /// can still override it with [`RequestBuilder::model`].
2116 pub fn model(mut self, model: Model) -> ClientBuilder<ModelReady> {
2117 self.default_model = Some(model);
2118 self.with_state()
2119 }
2120
2121 /// Set the default generation config used by request builders.
2122 pub fn config(mut self, config: GenerationConfig) -> Self {
2123 self.default_config = config;
2124 self
2125 }
2126
2127 /// Set the default retry configuration for all requests.
2128 pub fn retry_config(mut self, config: RetryConfig) -> Self {
2129 self.default_retry_config = config;
2130 self
2131 }
2132
2133 /// Disable retries by default for all requests.
2134 pub fn no_retry(mut self) -> Self {
2135 self.default_retry_config = RetryConfig::none();
2136 self
2137 }
2138
2139 /// Register a tool that [`RequestBuilder::generate`] may auto-execute.
2140 ///
2141 /// Client-level tools are available to every request. Requests that stream
2142 /// must opt out with [`RequestBuilder::no_tools`], since streaming cannot
2143 /// run a tool loop; see [`RequestBuilder::stream`].
2144 pub fn tool(mut self, tool: Tool) -> Self {
2145 self.tools.push(tool);
2146 self
2147 }
2148
2149 /// Register multiple tools to be auto-executed by `generate()`.
2150 pub fn tools<T>(mut self, tools: T) -> Self
2151 where
2152 T: IntoIterator<Item = Tool>,
2153 {
2154 self.tools.extend(tools);
2155 self
2156 }
2157
2158 /// Build the client.
2159 ///
2160 /// Providers with credentials are initialized; providers without them are
2161 /// left unavailable rather than causing a failure, so this succeeds even if
2162 /// only one key is present.
2163 ///
2164 /// # Errors
2165 ///
2166 /// - [`Error::InvalidRequest`] if two registered tools share a name, or a
2167 /// tool's input schema is invalid.
2168 /// - An error if a provider's HTTP client cannot be constructed.
2169 pub fn build(self) -> Result<Client<ModelState>> {
2170 let mut tool_registry = ToolRegistry::new();
2171 tool_registry.extend(self.tools)?;
2172 Client::new_with_defaults(
2173 self.config,
2174 self.default_model,
2175 self.default_config,
2176 self.default_retry_config,
2177 tool_registry,
2178 )
2179 }
2180}
2181
2182impl Default for ClientBuilder<ModelMissing> {
2183 fn default() -> Self {
2184 Self::new()
2185 }
2186}