Skip to main content

mesh_llm_api_client/
client.rs

1use crate::events::{Event, EventListener};
2use crate::{InviteToken, OwnerKeypair};
3use mesh_client::ClientError;
4use std::sync::Arc;
5use std::time::Duration;
6use thiserror::Error;
7
8pub const MAX_RECONNECT_ATTEMPTS: u32 = mesh_client::client::builder::MAX_RECONNECT_ATTEMPTS;
9pub type ClientTransport = mesh_client::ClientTransport;
10
11#[derive(Debug, Error)]
12pub enum MeshApiError {
13    #[error(transparent)]
14    Client(#[from] ClientError),
15    #[error("public mesh discovery failed: {message}")]
16    Discovery { message: String },
17    #[error("no public mesh matched the requested criteria")]
18    NoPublicMeshFound,
19    #[error("invalid invite token: {message}")]
20    InvalidInviteToken { message: String },
21    #[error("invalid Mesh SDK configuration: {message}")]
22    InvalidConfig { message: &'static str },
23    #[error("model management failed: {message}")]
24    ModelManagement { message: String },
25    #[error("serving failed: {message}")]
26    Serving { message: String },
27    #[error("{feature} is not implemented in the Mesh SDK yet")]
28    Unsupported { feature: &'static str },
29}
30
31#[derive(Clone, Debug)]
32pub struct ClientConfig {
33    pub owner_keypair: OwnerKeypair,
34    pub invite_token: InviteToken,
35    pub user_agent: String,
36    pub connect_timeout: Duration,
37    pub transport: ClientTransport,
38}
39
40pub struct ClientBuilder {
41    config: ClientConfig,
42}
43
44impl ClientBuilder {
45    pub fn new(owner_keypair: OwnerKeypair, invite_token: InviteToken) -> Self {
46        Self {
47            config: ClientConfig {
48                owner_keypair,
49                invite_token,
50                user_agent: format!("mesh-llm-api-client/{}", env!("CARGO_PKG_VERSION")),
51                connect_timeout: Duration::from_secs(30),
52                transport: ClientTransport::DirectMesh,
53            },
54        }
55    }
56
57    pub fn with_user_agent(mut self, ua: String) -> Self {
58        self.config.user_agent = ua;
59        self
60    }
61
62    pub fn with_connect_timeout(mut self, d: Duration) -> Self {
63        self.config.connect_timeout = d;
64        self
65    }
66
67    pub fn with_transport(mut self, transport: ClientTransport) -> Self {
68        self.config.transport = transport;
69        self
70    }
71
72    pub fn with_direct_mesh_transport(self) -> Self {
73        self.with_transport(ClientTransport::DirectMesh)
74    }
75
76    pub fn with_openai_http_transport(mut self, api_base_url: impl Into<String>) -> Self {
77        self.config.transport = ClientTransport::OpenAiHttp {
78            api_base_url: api_base_url.into(),
79        };
80        self
81    }
82
83    pub fn build(self) -> Result<MeshClient, MeshApiError> {
84        let mut builder = mesh_client::ClientBuilder::new(
85            self.config.owner_keypair.into_inner(),
86            self.config.invite_token.into_inner(),
87        )
88        .with_user_agent(self.config.user_agent.clone())
89        .with_connect_timeout(self.config.connect_timeout);
90
91        builder = builder.with_transport(self.config.transport);
92
93        let inner = builder.build()?;
94
95        Ok(MeshClient { inner })
96    }
97}
98
99pub struct MeshClient {
100    inner: mesh_client::MeshClient,
101}
102
103impl MeshClient {
104    pub async fn join(&mut self) -> Result<(), MeshApiError> {
105        self.inner.join().await?;
106        Ok(())
107    }
108
109    pub async fn list_models(&self) -> Result<Vec<Model>, MeshApiError> {
110        Ok(self
111            .inner
112            .list_models()
113            .await?
114            .into_iter()
115            .map(Model::from)
116            .collect())
117    }
118
119    pub fn chat(&self, request: ChatRequest, listener: Arc<dyn EventListener>) -> RequestId {
120        let request_id = self.inner.chat(
121            mesh_client::ChatRequest::from(request),
122            Arc::new(EventListenerAdapter { inner: listener }),
123        );
124        RequestId(request_id.0)
125    }
126
127    pub fn responses(
128        &self,
129        request: ResponsesRequest,
130        listener: Arc<dyn EventListener>,
131    ) -> RequestId {
132        let request_id = self.inner.responses(
133            mesh_client::ResponsesRequest::from(request),
134            Arc::new(EventListenerAdapter { inner: listener }),
135        );
136        RequestId(request_id.0)
137    }
138
139    pub fn cancel(&self, request_id: RequestId) {
140        self.inner.cancel(mesh_client::RequestId(request_id.0));
141    }
142
143    pub async fn status(&self) -> Status {
144        Status::from(self.inner.status().await)
145    }
146
147    pub async fn disconnect(&mut self) {
148        self.inner.disconnect().await;
149    }
150
151    pub async fn reconnect(&mut self) -> Result<(), MeshApiError> {
152        self.inner.reconnect().await?;
153        Ok(())
154    }
155
156    pub fn add_event_listener(&self, listener: Arc<dyn EventListener>) -> String {
157        self.inner
158            .add_event_listener(Arc::new(EventListenerAdapter { inner: listener }))
159    }
160
161    pub fn remove_event_listener(&self, listener_id: &str) {
162        self.inner.remove_event_listener(listener_id);
163    }
164}
165
166#[derive(Clone, Debug)]
167pub struct ChatRequest {
168    pub model: String,
169    pub messages: Vec<ChatMessage>,
170}
171
172impl From<ChatRequest> for mesh_client::ChatRequest {
173    fn from(value: ChatRequest) -> Self {
174        Self {
175            model: value.model,
176            messages: value.messages.into_iter().map(Into::into).collect(),
177        }
178    }
179}
180
181#[derive(Clone, Debug)]
182pub struct ChatMessage {
183    pub role: String,
184    pub content: String,
185}
186
187impl From<ChatMessage> for mesh_client::ChatMessage {
188    fn from(value: ChatMessage) -> Self {
189        Self {
190            role: value.role,
191            content: value.content,
192        }
193    }
194}
195
196#[derive(Clone, Debug)]
197pub struct ResponsesRequest {
198    pub model: String,
199    pub input: String,
200}
201
202impl From<ResponsesRequest> for mesh_client::ResponsesRequest {
203    fn from(value: ResponsesRequest) -> Self {
204        Self {
205            model: value.model,
206            input: value.input,
207        }
208    }
209}
210
211#[derive(Debug, Clone)]
212pub struct Model {
213    pub id: String,
214    pub name: String,
215}
216
217impl From<mesh_client::Model> for Model {
218    fn from(value: mesh_client::Model) -> Self {
219        Self {
220            id: value.id,
221            name: value.name,
222        }
223    }
224}
225
226pub struct Status {
227    pub connected: bool,
228    pub peer_count: usize,
229}
230
231impl From<mesh_client::Status> for Status {
232    fn from(value: mesh_client::Status) -> Self {
233        Self {
234            connected: value.connected,
235            peer_count: value.peer_count,
236        }
237    }
238}
239
240pub struct RequestId(pub String);
241
242impl RequestId {
243    pub fn new() -> Self {
244        Self(mesh_client::RequestId::new().0)
245    }
246}
247
248impl Default for RequestId {
249    fn default() -> Self {
250        Self::new()
251    }
252}
253
254struct EventListenerAdapter {
255    inner: Arc<dyn EventListener>,
256}
257
258impl mesh_client::events::EventListener for EventListenerAdapter {
259    fn on_event(&self, event: mesh_client::events::Event) {
260        self.inner.on_event(match event {
261            mesh_client::events::Event::Connecting => Event::Connecting,
262            mesh_client::events::Event::Joined { node_id } => Event::Joined { node_id },
263            mesh_client::events::Event::ModelsUpdated { models } => Event::ModelsUpdated {
264                models: models.into_iter().map(Model::from).collect(),
265            },
266            mesh_client::events::Event::TokenDelta { request_id, delta } => {
267                Event::TokenDelta { request_id, delta }
268            }
269            mesh_client::events::Event::Completed { request_id } => Event::Completed { request_id },
270            mesh_client::events::Event::Failed { request_id, error } => {
271                Event::Failed { request_id, error }
272            }
273            mesh_client::events::Event::Disconnected { reason } => Event::Disconnected { reason },
274        });
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    #[test]
283    fn builder_accepts_explicit_openai_http_transport() {
284        let owner = OwnerKeypair::generate();
285        let invite = "mesh-test:token".parse::<InviteToken>().unwrap();
286
287        let builder = ClientBuilder::new(owner, invite)
288            .with_openai_http_transport("http://127.0.0.1:9337/v1");
289
290        assert_eq!(
291            builder.config.transport,
292            ClientTransport::OpenAiHttp {
293                api_base_url: "http://127.0.0.1:9337/v1".to_string()
294            }
295        );
296    }
297
298    #[test]
299    fn builder_defaults_to_direct_mesh_transport() {
300        let owner = OwnerKeypair::generate();
301        let invite = "mesh-test:token".parse::<InviteToken>().unwrap();
302        let builder = ClientBuilder::new(owner, invite);
303
304        assert_eq!(builder.config.transport, ClientTransport::DirectMesh);
305    }
306}