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