Skip to main content

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