Skip to main content

just_llm_client/client/
mod.rs

1//! Unified chat client and backend factory.
2//!
3//! Two building blocks on top of the concrete backend adapters:
4//!
5//! - [`BackendFactory`] dispatches a backend family string to that backend's
6//!   [`LlmBackend::new`] constructor — a composable `family ->
7//!   constructor` primitive with no caching or held configuration.
8//! - [`ChatClient`] pairs per-call request defaults (model, system prompt) with a shared
9//!   [`LlmBackend`](crate::LlmBackend) and derefs to `dyn LlmBackend` so chat and capability
10//!   methods are reachable directly.
11//!
12//! Backends can also be constructed directly via
13//! [`LlmBackend::new`] (the trait constructor, with the trait in scope),
14//! or from a pre-built provider client via each backend's `from_provider_client`.
15
16mod factory;
17
18use std::{ops::Deref, sync::Arc};
19
20use crate::{
21    provider::LlmBackend,
22    types::chat::{ChatCompletionRequest, ChatMessage},
23};
24
25pub use factory::BackendFactory;
26
27/// Per-call defaults for constructing a [`ChatClient`].
28#[derive(Clone, Debug, PartialEq, Eq)]
29pub struct ChatClientOptions {
30    model: String,
31    system_prompt: Option<String>,
32}
33
34impl ChatClientOptions {
35    /// Create options with a required default model.
36    pub fn new(model: impl Into<String>) -> Self {
37        Self {
38            model: model.into(),
39            system_prompt: None,
40        }
41    }
42
43    /// Add a default system prompt that will be injected into requests built by the client.
44    pub fn with_system_prompt(mut self, system_prompt: impl Into<String>) -> Self {
45        self.system_prompt = Some(system_prompt.into());
46        self
47    }
48
49    /// Returns the configured default model.
50    pub fn model(&self) -> &str {
51        &self.model
52    }
53
54    /// Returns the configured default system prompt.
55    pub fn system_prompt(&self) -> Option<&str> {
56        self.system_prompt.as_deref()
57    }
58}
59
60/// Thin facade that pairs per-call default request values with a shared backend.
61///
62/// Constructed via [`ChatClient::new`], typically with a backend produced by a
63/// [`BackendFactory`] or by [`LlmBackend::new`] directly. Implements
64/// [`Deref`] to [`dyn LlmBackend`](crate::LlmBackend) so the direct and prepared chat execution
65/// paths, plus capability negotiation methods, are accessible directly.
66#[derive(Clone)]
67pub struct ChatClient {
68    model: String,
69    system_prompt: Option<String>,
70    backend: Arc<dyn LlmBackend>,
71}
72
73impl ChatClient {
74    /// Create a client pairing per-call defaults with a shared backend.
75    ///
76    /// The backend identity (family) is available via [`family`](crate::Identifiable::family)
77    /// through the [`Deref`] to [`LlmBackend`].
78    pub fn new(backend: Arc<dyn LlmBackend>, options: ChatClientOptions) -> Self {
79        Self {
80            model: options.model,
81            system_prompt: options.system_prompt,
82            backend,
83        }
84    }
85
86    /// Returns the resolved model string (explicit or provider default).
87    pub fn model(&self) -> &str {
88        &self.model
89    }
90
91    /// Returns the configured default system prompt, if any.
92    pub fn system_prompt(&self) -> Option<&str> {
93        self.system_prompt.as_deref()
94    }
95
96    /// Returns the client with a new default model.
97    pub fn with_model(mut self, model: impl Into<String>) -> Self {
98        self.model = model.into();
99        self
100    }
101
102    /// Returns the client with a new default system prompt.
103    pub fn with_system_prompt(mut self, system_prompt: impl Into<String>) -> Self {
104        self.system_prompt = Some(system_prompt.into());
105        self
106    }
107
108    /// Returns the client without a default system prompt.
109    pub fn clear_system_prompt(mut self) -> Self {
110        self.system_prompt = None;
111        self
112    }
113
114    /// Create a request pre-filled with this client's default model and system prompt.
115    #[must_use = "the returned request must be passed to a backend method to have any effect"]
116    pub fn create_request(&self, messages: Vec<ChatMessage>) -> ChatCompletionRequest {
117        let mut request = ChatCompletionRequest::new(self.model.clone(), messages);
118        if let Some(system_prompt) = &self.system_prompt {
119            request = request.with_system_prompt(system_prompt.clone());
120        }
121        request
122    }
123}
124
125impl Deref for ChatClient {
126    type Target = dyn crate::LlmBackend;
127
128    fn deref(&self) -> &Self::Target {
129        &*self.backend
130    }
131}
132
133impl std::fmt::Debug for ChatClient {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        f.debug_struct("ChatClient")
136            .field("family", &self.backend.family())
137            .field("model", &self.model)
138            .field("has_system_prompt", &self.system_prompt.is_some())
139            .finish()
140    }
141}