Skip to main content

smbcloud_gresiq_sdk/
onde_apps.rs

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
15use crate::error::GresiqError;
16use serde::{Deserialize, Serialize};
17use smbcloud_network::environment::Environment;
18
19/// An app registered to the user's account.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub 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)]
35pub 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)]
55pub 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)]
70struct CreateModelBody<'a> {
71    gresiq_model: CreateModelParams<'a>,
72}
73
74// The models endpoint wraps its array: { "models": [...] }.
75#[derive(Deserialize)]
76struct ModelsEnvelope {
77    models: Vec<OndeModel>,
78}
79
80// POST /apps body shape: { "gresiq_app": { "name": "..." } }
81#[derive(Serialize)]
82struct CreateAppBody<'a> {
83    gresiq_app: CreateAppParams<'a>,
84}
85
86#[derive(Serialize)]
87struct CreateAppParams<'a> {
88    name: &'a str,
89}
90
91fn 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
102fn 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.
108async 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`
123pub 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": "..." } }`
142pub 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": "..." }`
166pub 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": [...] }`
191pub 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": { ... } }`
217pub 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": "..." } }`
241pub 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}