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(¤t_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(¤t_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}