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}
46
47// The models endpoint wraps its array: { "models": [...] }.
48#[derive(Deserialize)]
49struct ModelsEnvelope {
50    models: Vec<OndeModel>,
51}
52
53// POST /apps body shape: { "gresiq_app": { "name": "..." } }
54#[derive(Serialize)]
55struct CreateAppBody<'a> {
56    gresiq_app: CreateAppParams<'a>,
57}
58
59#[derive(Serialize)]
60struct CreateAppParams<'a> {
61    name: &'a str,
62}
63
64fn 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
75fn 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.
81async 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`
96pub 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": "..." } }`
115pub 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": "..." }`
139pub 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": [...] }`
164pub 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": "..." } }`
187pub 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}