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    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)]
33pub 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)]
47struct ModelsEnvelope {
48    models: Vec<OndeModel>,
49}
50
51// POST /apps body shape: { "gresiq_app": { "name": "..." } }
52#[derive(Serialize)]
53struct CreateAppBody<'a> {
54    gresiq_app: CreateAppParams<'a>,
55}
56
57#[derive(Serialize)]
58struct CreateAppParams<'a> {
59    name: &'a str,
60}
61
62fn 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
73fn 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.
79async 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`
94pub 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": "..." } }`
113pub 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": "..." }`
137pub 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": [...] }`
162pub 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).await?.json::<ModelsEnvelope>().await?.models)
176}
177
178
179/// Rename an existing app.
180///
181/// `PATCH /v1/client/gresiq/apps/{onde_app_id}` — body: `{ "gresiq_app": { "name": "..." } }`
182pub async fn rename_app(
183    environment: &Environment,
184    app_id: &str,
185    app_secret: &str,
186    access_token: &str,
187    onde_app_id: &str,
188    new_name: &str,
189) -> Result<OndeApp, GresiqError> {
190    let path = format!("apps/{}", onde_app_id);
191    let url = endpoint(environment, &path, app_id, app_secret);
192    let body = CreateAppBody {
193        gresiq_app: CreateAppParams { name: new_name },
194    };
195    let response = reqwest::Client::new()
196        .patch(&url)
197        .header("Authorization", bearer(access_token))
198        .header("Content-Type", "application/json")
199        .json(&body)
200        .send()
201        .await?;
202    Ok(check(response).await?.json::<OndeApp>().await?)
203}