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 pub created_at: Option<String>,
28 pub updated_at: Option<String>,
29 }
30
31 /// A model from the Onde catalog, assignable to an [`OndeApp`].
32 #[derive(Debug, Clone, Serialize, Deserialize)]
33 pub struct OndeModel {
34 pub id: String,
35 pub name: Option<String>,
36 pub hf_repo_id: Option<String>,
37 pub gguf_file: Option<String>,
38 pub family: Option<String>,
39 pub parameter_class: Option<String>,
40 pub format: Option<String>,
41 pub approx_size_bytes: Option<i64>,
42 pub description: Option<String>,
43 }
44
45 // The models endpoint wraps its array: { "models": [...] }.
46 #[derive(Deserialize)]
47 struct ModelsEnvelope {
48 models: Vec<OndeModel>,
49 }
50
51 // POST /apps body shape: { "gresiq_app": { "name": "..." } }
52 #[derive(Serialize)]
53 struct CreateAppBody<'a> {
54 gresiq_app: CreateAppParams<'a>,
55 }
56
57 #[derive(Serialize)]
58 struct CreateAppParams<'a> {
59 name: &'a str,
60 }
61
62 fn endpoint(environment: &Environment, path: &str, app_id: &str, app_secret: &str) -> String {
63 format!(
64 "{}://{}/v1/client/gresiq/{}?client_id={}&client_secret={}",
65 environment.api_protocol(),
66 environment.api_host(),
67 path,
68 app_id,
69 app_secret,
70 )
71 }
72
73 fn bearer(token: &str) -> String {
74 format!("Bearer {token}")
75 }
76
77 // Returns the response on 2xx. On anything else, reads the body as text
78 // before returning so callers don't have to think about it.
79 async fn check(response: reqwest::Response) -> Result<reqwest::Response, GresiqError> {
80 if response.status().is_success() {
81 return Ok(response);
82 }
83 let status = response.status().as_u16();
84 let message = response
85 .text()
86 .await
87 .unwrap_or_else(|_| "unreadable response body".to_string());
88 Err(GresiqError::Api { status, message })
89 }
90
91 /// Fetch all apps for the authenticated user.
92 ///
93 /// `GET /v1/client/gresiq/apps`
94 pub async fn list_apps(
95 environment: &Environment,
96 app_id: &str,
97 app_secret: &str,
98 access_token: &str,
99 ) -> Result<Vec<OndeApp>, GresiqError> {
100 let url = endpoint(environment, "apps", app_id, app_secret);
101 let response = reqwest::Client::new()
102 .get(&url)
103 .header("Authorization", bearer(access_token))
104 .header("Content-Type", "application/json")
105 .send()
106 .await?;
107 Ok(check(response).await?.json::<Vec<OndeApp>>().await?)
108 }
109
110 /// Create a new app under the authenticated user's account.
111 ///
112 /// `POST /v1/client/gresiq/apps` — body: `{ "gresiq_app": { "name": "..." } }`
113 pub async fn create_app(
114 environment: &Environment,
115 app_id: &str,
116 app_secret: &str,
117 access_token: &str,
118 name: &str,
119 ) -> Result<OndeApp, GresiqError> {
120 let url = endpoint(environment, "apps", app_id, app_secret);
121 let body = CreateAppBody {
122 gresiq_app: CreateAppParams { name },
123 };
124 let response = reqwest::Client::new()
125 .post(&url)
126 .header("Authorization", bearer(access_token))
127 .header("Content-Type", "application/json")
128 .json(&body)
129 .send()
130 .await?;
131 Ok(check(response).await?.json::<OndeApp>().await?)
132 }
133
134 /// Assign a catalog model to an app. Creates the record if none exists yet.
135 ///
136 /// `PATCH /v1/client/gresiq/apps/{onde_app_id}/model` — body: `{ "model_id": "..." }`
137 pub async fn assign_model(
138 environment: &Environment,
139 app_id: &str,
140 app_secret: &str,
141 access_token: &str,
142 onde_app_id: &str,
143 model_id: &str,
144 ) -> Result<(), GresiqError> {
145 let path = format!("apps/{}/model", onde_app_id);
146 let url = endpoint(environment, &path, app_id, app_secret);
147 let body = serde_json::json!({ "model_id": model_id });
148 let response = reqwest::Client::new()
149 .patch(&url)
150 .header("Authorization", bearer(access_token))
151 .header("Content-Type", "application/json")
152 .json(&body)
153 .send()
154 .await?;
155 check(response).await?;
156 Ok(())
157 }
158
159 /// Fetch all models in the Onde catalog.
160 ///
161 /// `GET /v1/client/gresiq/models` — response: `{ "models": [...] }`
162 pub async fn list_models(
163 environment: &Environment,
164 app_id: &str,
165 app_secret: &str,
166 access_token: &str,
167 ) -> Result<Vec<OndeModel>, GresiqError> {
168 let url = endpoint(environment, "models", app_id, app_secret);
169 let response = reqwest::Client::new()
170 .get(&url)
171 .header("Authorization", bearer(access_token))
172 .header("Content-Type", "application/json")
173 .send()
174 .await?;
175 Ok(check(response)
176 .await?
177 .json::<ModelsEnvelope>()
178 .await?
179 .models)
180 }
181
182 /// Rename an existing app.
183 ///
184 /// `PATCH /v1/client/gresiq/apps/{onde_app_id}` — body: `{ "gresiq_app": { "name": "..." } }`
185 pub async fn rename_app(
186 environment: &Environment,
187 app_id: &str,
188 app_secret: &str,
189 access_token: &str,
190 onde_app_id: &str,
191 new_name: &str,
192 ) -> Result<OndeApp, GresiqError> {
193 let path = format!("apps/{}", onde_app_id);
194 let url = endpoint(environment, &path, app_id, app_secret);
195 let body = CreateAppBody {
196 gresiq_app: CreateAppParams { name: new_name },
197 };
198 let response = reqwest::Client::new()
199 .patch(&url)
200 .header("Authorization", bearer(access_token))
201 .header("Content-Type", "application/json")
202 .json(&body)
203 .send()
204 .await?;
205 Ok(check(response).await?.json::<OndeApp>().await?)
206 }