1 //! Apps and models management for Onde Inference.
2 //!
3 //! Each function opens its own `reqwest::Client`. These are low-frequency
4 //! management calls, so there's no benefit to a shared pool.
5 //!
6 //! Every request needs two things: the Onde app's client credentials as
7 //! query params, and the user's bearer token as an Authorization header.
8 //!
9 //! ```text
10 //! {protocol}://{host}/v1/client/gresiq/{path}
11 //! ?client_id={app_id}&client_secret={app_secret}
12 //! Authorization: Bearer {access_token}
13 //! ```
14
15 use crate::error::GresiqError;
16 use serde::{Deserialize, Serialize};
17 use smbcloud_network::environment::Environment;
18
19 /// An app registered to the user's account.
20 #[derive(Debug, Clone, Serialize, Deserialize)]
21 pub struct OndeApp {
22 pub id: String,
23 pub name: String,
24 pub status: Option<String>,
25 pub app_secret: Option<String>,
26 pub current_model_id: Option<String>,
27 #[serde(alias = "activeModel")]
28 pub active_model: Option<String>,
29 pub created_at: Option<String>,
30 pub updated_at: Option<String>,
31 }
32
33 /// A model from the Onde catalog, assignable to an [`OndeApp`].
34 #[derive(Debug, Clone, Serialize, Deserialize)]
35 pub struct OndeModel {
36 pub id: String,
37 pub name: Option<String>,
38 pub hf_repo_id: Option<String>,
39 pub gguf_file: Option<String>,
40 pub family: Option<String>,
41 pub parameter_class: Option<String>,
42 pub format: Option<String>,
43 pub approx_size_bytes: Option<i64>,
44 pub description: Option<String>,
45 /// `true` if this row was self-registered by a client (e.g. a fine-tune
46 /// pushed to Hugging Face) rather than one of the official catalog
47 /// models. Private to the registering user — other clients never see it.
48 #[serde(default)]
49 pub custom: bool,
50 }
51
52 /// Parameters for registering a new model into the catalog via
53 /// [`create_model`]. The new row is private to the calling user.
54 #[derive(Debug, Clone, Serialize)]
55 pub struct CreateModelParams<'a> {
56 pub hf_repo_id: &'a str,
57 pub name: &'a str,
58 pub org: &'a str,
59 pub family: &'a str,
60 pub parameter_class: &'a str,
61 pub format: &'a str,
62 pub gguf_file: Option<&'a str>,
63 pub modality: Option<&'a str>,
64 pub description: Option<&'a str>,
65 pub approx_size_bytes: Option<i64>,
66 }
67
68 // POST /models body shape: { "gresiq_model": { ... } }
69 #[derive(Serialize)]
70 struct CreateModelBody<'a> {
71 gresiq_model: CreateModelParams<'a>,
72 }
73
74 // The models endpoint wraps its array: { "models": [...] }.
75 #[derive(Deserialize)]
76 struct ModelsEnvelope {
77 models: Vec<OndeModel>,
78 }
79
80 // POST /apps body shape: { "gresiq_app": { "name": "..." } }
81 #[derive(Serialize)]
82 struct CreateAppBody<'a> {
83 gresiq_app: CreateAppParams<'a>,
84 }
85
86 #[derive(Serialize)]
87 struct CreateAppParams<'a> {
88 name: &'a str,
89 }
90
91 fn endpoint(environment: &Environment, path: &str, app_id: &str, app_secret: &str) -> String {
92 format!(
93 "{}://{}/v1/client/gresiq/{}?client_id={}&client_secret={}",
94 environment.api_protocol(),
95 environment.api_host(),
96 path,
97 app_id,
98 app_secret,
99 )
100 }
101
102 fn bearer(token: &str) -> String {
103 format!("Bearer {token}")
104 }
105
106 // Returns the response on 2xx. On anything else, reads the body as text
107 // before returning so callers don't have to think about it.
108 async fn check(response: reqwest::Response) -> Result<reqwest::Response, GresiqError> {
109 if response.status().is_success() {
110 return Ok(response);
111 }
112 let status = response.status().as_u16();
113 let message = response
114 .text()
115 .await
116 .unwrap_or_else(|_| "unreadable response body".to_string());
117 Err(GresiqError::Api { status, message })
118 }
119
120 /// Fetch all apps for the authenticated user.
121 ///
122 /// `GET /v1/client/gresiq/apps`
123 pub async fn list_apps(
124 environment: &Environment,
125 app_id: &str,
126 app_secret: &str,
127 access_token: &str,
128 ) -> Result<Vec<OndeApp>, GresiqError> {
129 let url = endpoint(environment, "apps", app_id, app_secret);
130 let response = reqwest::Client::new()
131 .get(&url)
132 .header("Authorization", bearer(access_token))
133 .header("Content-Type", "application/json")
134 .send()
135 .await?;
136 Ok(check(response).await?.json::<Vec<OndeApp>>().await?)
137 }
138
139 /// Create a new app under the authenticated user's account.
140 ///
141 /// `POST /v1/client/gresiq/apps` — body: `{ "gresiq_app": { "name": "..." } }`
142 pub async fn create_app(
143 environment: &Environment,
144 app_id: &str,
145 app_secret: &str,
146 access_token: &str,
147 name: &str,
148 ) -> Result<OndeApp, GresiqError> {
149 let url = endpoint(environment, "apps", app_id, app_secret);
150 let body = CreateAppBody {
151 gresiq_app: CreateAppParams { name },
152 };
153 let response = reqwest::Client::new()
154 .post(&url)
155 .header("Authorization", bearer(access_token))
156 .header("Content-Type", "application/json")
157 .json(&body)
158 .send()
159 .await?;
160 Ok(check(response).await?.json::<OndeApp>().await?)
161 }
162
163 /// Assign a catalog model to an app. Creates the record if none exists yet.
164 ///
165 /// `PATCH /v1/client/gresiq/apps/{onde_app_id}/model` — body: `{ "model_id": "..." }`
166 pub async fn assign_model(
167 environment: &Environment,
168 app_id: &str,
169 app_secret: &str,
170 access_token: &str,
171 onde_app_id: &str,
172 model_id: &str,
173 ) -> Result<(), GresiqError> {
174 let path = format!("apps/{}/model", onde_app_id);
175 let url = endpoint(environment, &path, app_id, app_secret);
176 let body = serde_json::json!({ "model_id": model_id });
177 let response = reqwest::Client::new()
178 .patch(&url)
179 .header("Authorization", bearer(access_token))
180 .header("Content-Type", "application/json")
181 .json(&body)
182 .send()
183 .await?;
184 check(response).await?;
185 Ok(())
186 }
187
188 /// Fetch all models in the Onde catalog.
189 ///
190 /// `GET /v1/client/gresiq/models` — response: `{ "models": [...] }`
191 pub async fn list_models(
192 environment: &Environment,
193 app_id: &str,
194 app_secret: &str,
195 access_token: &str,
196 ) -> Result<Vec<OndeModel>, GresiqError> {
197 let url = endpoint(environment, "models", app_id, app_secret);
198 let response = reqwest::Client::new()
199 .get(&url)
200 .header("Authorization", bearer(access_token))
201 .header("Content-Type", "application/json")
202 .send()
203 .await?;
204 Ok(check(response)
205 .await?
206 .json::<ModelsEnvelope>()
207 .await?
208 .models)
209 }
210
211 /// Register a new model into the catalog, private to the calling user.
212 ///
213 /// Typical use: right after uploading a fine-tuned GGUF to Hugging Face, so
214 /// it can then be targeted by [`assign_model`].
215 ///
216 /// `POST /v1/client/gresiq/models` — body: `{ "gresiq_model": { ... } }`
217 pub async fn create_model(
218 environment: &Environment,
219 app_id: &str,
220 app_secret: &str,
221 access_token: &str,
222 params: CreateModelParams<'_>,
223 ) -> Result<OndeModel, GresiqError> {
224 let url = endpoint(environment, "models", app_id, app_secret);
225 let body = CreateModelBody {
226 gresiq_model: params,
227 };
228 let response = reqwest::Client::new()
229 .post(&url)
230 .header("Authorization", bearer(access_token))
231 .header("Content-Type", "application/json")
232 .json(&body)
233 .send()
234 .await?;
235 Ok(check(response).await?.json::<OndeModel>().await?)
236 }
237
238 /// Rename an existing app.
239 ///
240 /// `PATCH /v1/client/gresiq/apps/{onde_app_id}` — body: `{ "gresiq_app": { "name": "..." } }`
241 pub async fn rename_app(
242 environment: &Environment,
243 app_id: &str,
244 app_secret: &str,
245 access_token: &str,
246 onde_app_id: &str,
247 new_name: &str,
248 ) -> Result<OndeApp, GresiqError> {
249 let path = format!("apps/{}", onde_app_id);
250 let url = endpoint(environment, &path, app_id, app_secret);
251 let body = CreateAppBody {
252 gresiq_app: CreateAppParams { name: new_name },
253 };
254 let response = reqwest::Client::new()
255 .patch(&url)
256 .header("Authorization", bearer(access_token))
257 .header("Content-Type", "application/json")
258 .json(&body)
259 .send()
260 .await?;
261 Ok(check(response).await?.json::<OndeApp>().await?)
262 }