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, 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(&current_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(&current_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}