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}