Skip to main content

objectiveai_sdk/viewer/
api_call.rs

1//! Types for the viewer's `Event::ApiCall` bridge.
2//!
3//! In viewer mode, the JS SDK's `ObjectiveAI` client diverts every HTTP
4//! method through a Tauri postMessage channel instead of `fetch()`. The
5//! iframe posts `{kind: "api-call-invoke", sub_type, body}` to the host,
6//! which dispatches `sub_type` to the matching upstream call and emits
7//! [`Event::ApiCall`](super::Event::ApiCall) chunks back to the iframe.
8//!
9//! [`ApiCallSubType`] enumerates every (HTTP method, path) tuple the
10//! `objectiveai-api` crate exposes. The serde rename of each variant is
11//! `"<METHOD>_<PATH>"` (e.g. `"POST_/agent/completions"`,
12//! `"GET_/auth/keys"`) so the wire format is unambiguous when the same
13//! path serves multiple methods (notably `/auth/keys`, which has POST,
14//! DELETE, and GET).
15//!
16//! An AST coverage test (`tests/api_routes_coverage.rs`) diffs this
17//! enum against `objectiveai-api/src/run.rs`'s `Router::new()` chain;
18//! adding a new route to the api crate without a matching variant
19//! fails the test.
20
21use schemars::JsonSchema;
22use serde::{Deserialize, Serialize};
23
24/// One `(method, path)` tuple — one variant per route the
25/// `objectiveai-api` crate's `Router::new()` chain declares.
26///
27/// Serializes as `"<METHOD>_<PATH>"`. The underscore separator (rather
28/// than space) keeps the value safe to use as a Tauri event channel
29/// suffix and as a JS object key.
30#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash)]
31#[schemars(rename = "viewer.ApiCallSubType")]
32pub enum ApiCallSubType {
33    #[serde(rename = "POST_/agent/completions")]
34    PostAgentCompletions,
35    #[serde(rename = "POST_/vector/completions")]
36    PostVectorCompletions,
37    #[serde(rename = "POST_/vector/completions/votes")]
38    PostVectorCompletionsVotes,
39    #[serde(rename = "POST_/vector/completions/cache")]
40    PostVectorCompletionsCache,
41    #[serde(rename = "POST_/functions/list")]
42    PostFunctionsList,
43    #[serde(rename = "POST_/functions")]
44    PostFunctions,
45    #[serde(rename = "POST_/functions/usage")]
46    PostFunctionsUsage,
47    #[serde(rename = "POST_/functions/executions")]
48    PostFunctionsExecutions,
49    #[serde(rename = "POST_/functions/profiles/list")]
50    PostFunctionsProfilesList,
51    #[serde(rename = "POST_/functions/profiles")]
52    PostFunctionsProfiles,
53    #[serde(rename = "POST_/functions/profiles/usage")]
54    PostFunctionsProfilesUsage,
55    #[serde(rename = "POST_/functions/profiles/pairs/list")]
56    PostFunctionsProfilesPairsList,
57    #[serde(rename = "POST_/functions/profiles/pairs/usage")]
58    PostFunctionsProfilesPairsUsage,
59    #[serde(rename = "POST_/functions/inventions")]
60    PostFunctionsInventions,
61    #[serde(rename = "POST_/functions/inventions/recursive")]
62    PostFunctionsInventionsRecursive,
63    #[serde(rename = "POST_/functions/inventions/prompts/list")]
64    PostFunctionsInventionsPromptsList,
65    #[serde(rename = "POST_/functions/inventions/prompts")]
66    PostFunctionsInventionsPrompts,
67    #[serde(rename = "POST_/functions/inventions/prompts/usage")]
68    PostFunctionsInventionsPromptsUsage,
69    #[serde(rename = "POST_/functions/inventions/state")]
70    PostFunctionsInventionsState,
71    #[serde(rename = "POST_/functions/profiles/compute")]
72    PostFunctionsProfilesCompute,
73    #[serde(rename = "POST_/auth/keys")]
74    PostAuthKeys,
75    #[serde(rename = "POST_/auth/keys/openrouter")]
76    PostAuthKeysOpenrouter,
77    #[serde(rename = "DELETE_/auth/keys")]
78    DeleteAuthKeys,
79    #[serde(rename = "DELETE_/auth/keys/openrouter")]
80    DeleteAuthKeysOpenrouter,
81    #[serde(rename = "GET_/auth/keys")]
82    GetAuthKeys,
83    #[serde(rename = "GET_/auth/keys/openrouter")]
84    GetAuthKeysOpenrouter,
85    #[serde(rename = "GET_/auth/credits")]
86    GetAuthCredits,
87    #[serde(rename = "POST_/swarms/list")]
88    PostSwarmsList,
89    #[serde(rename = "POST_/swarms")]
90    PostSwarms,
91    #[serde(rename = "POST_/swarms/usage")]
92    PostSwarmsUsage,
93    #[serde(rename = "POST_/agents/list")]
94    PostAgentsList,
95    #[serde(rename = "POST_/agents")]
96    PostAgents,
97    #[serde(rename = "POST_/agents/usage")]
98    PostAgentsUsage,
99    #[serde(rename = "POST_/error")]
100    PostError,
101    #[serde(rename = "POST_/laboratories/executions")]
102    PostLaboratoriesExecutions,
103}
104
105/// HTTP method an [`ApiCallSubType`] maps to. Mirrors the methods
106/// `objectiveai-api`'s router uses (`POST`, `GET`, `DELETE`).
107#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash)]
108#[serde(rename_all = "UPPERCASE")]
109#[schemars(rename = "viewer.HttpMethod")]
110pub enum HttpMethod {
111    Get,
112    Post,
113    Delete,
114}
115
116impl HttpMethod {
117    /// Uppercase method name as it appears in the
118    /// [`ApiCallSubType`] serde rename and HTTP wire format.
119    pub fn as_str(&self) -> &'static str {
120        match self {
121            HttpMethod::Get => "GET",
122            HttpMethod::Post => "POST",
123            HttpMethod::Delete => "DELETE",
124        }
125    }
126}
127
128impl ApiCallSubType {
129    /// HTTP method the viewer host should dispatch this sub-type with.
130    pub fn method(&self) -> HttpMethod {
131        match self {
132            ApiCallSubType::GetAuthKeys
133            | ApiCallSubType::GetAuthKeysOpenrouter
134            | ApiCallSubType::GetAuthCredits => HttpMethod::Get,
135            ApiCallSubType::DeleteAuthKeys
136            | ApiCallSubType::DeleteAuthKeysOpenrouter => HttpMethod::Delete,
137            _ => HttpMethod::Post,
138        }
139    }
140
141    /// URL path this sub-type maps to on the `objectiveai-api`
142    /// server. Always begins with a leading `/`.
143    pub fn path(&self) -> &'static str {
144        match self {
145            ApiCallSubType::PostAgentCompletions => "/agent/completions",
146            ApiCallSubType::PostVectorCompletions => "/vector/completions",
147            ApiCallSubType::PostVectorCompletionsVotes => "/vector/completions/votes",
148            ApiCallSubType::PostVectorCompletionsCache => "/vector/completions/cache",
149            ApiCallSubType::PostFunctionsList => "/functions/list",
150            ApiCallSubType::PostFunctions => "/functions",
151            ApiCallSubType::PostFunctionsUsage => "/functions/usage",
152            ApiCallSubType::PostFunctionsExecutions => "/functions/executions",
153            ApiCallSubType::PostFunctionsProfilesList => "/functions/profiles/list",
154            ApiCallSubType::PostFunctionsProfiles => "/functions/profiles",
155            ApiCallSubType::PostFunctionsProfilesUsage => "/functions/profiles/usage",
156            ApiCallSubType::PostFunctionsProfilesPairsList => "/functions/profiles/pairs/list",
157            ApiCallSubType::PostFunctionsProfilesPairsUsage => "/functions/profiles/pairs/usage",
158            ApiCallSubType::PostFunctionsInventions => "/functions/inventions",
159            ApiCallSubType::PostFunctionsInventionsRecursive => "/functions/inventions/recursive",
160            ApiCallSubType::PostFunctionsInventionsPromptsList => "/functions/inventions/prompts/list",
161            ApiCallSubType::PostFunctionsInventionsPrompts => "/functions/inventions/prompts",
162            ApiCallSubType::PostFunctionsInventionsPromptsUsage => "/functions/inventions/prompts/usage",
163            ApiCallSubType::PostFunctionsInventionsState => "/functions/inventions/state",
164            ApiCallSubType::PostFunctionsProfilesCompute => "/functions/profiles/compute",
165            ApiCallSubType::PostAuthKeys => "/auth/keys",
166            ApiCallSubType::PostAuthKeysOpenrouter => "/auth/keys/openrouter",
167            ApiCallSubType::DeleteAuthKeys => "/auth/keys",
168            ApiCallSubType::DeleteAuthKeysOpenrouter => "/auth/keys/openrouter",
169            ApiCallSubType::GetAuthKeys => "/auth/keys",
170            ApiCallSubType::GetAuthKeysOpenrouter => "/auth/keys/openrouter",
171            ApiCallSubType::GetAuthCredits => "/auth/credits",
172            ApiCallSubType::PostSwarmsList => "/swarms/list",
173            ApiCallSubType::PostSwarms => "/swarms",
174            ApiCallSubType::PostSwarmsUsage => "/swarms/usage",
175            ApiCallSubType::PostAgentsList => "/agents/list",
176            ApiCallSubType::PostAgents => "/agents",
177            ApiCallSubType::PostAgentsUsage => "/agents/usage",
178            ApiCallSubType::PostError => "/error",
179            ApiCallSubType::PostLaboratoriesExecutions => "/laboratories/executions",
180        }
181    }
182}
183
184/// Wire-format envelope for each value the viewer host emits as
185/// [`Event::ApiCall.value`](super::Event::ApiCall) while servicing one
186/// `api-call-invoke` request.
187///
188/// Mirrors the cli's [`Output<T>`](crate::cli::output::Output)
189/// envelope shape so iframe consumers can apply the same JSONL state
190/// machine to both:
191///
192/// 1. `Begin` — exactly one, emitted before any data.
193/// 2. `Chunk { chunk }` — one per SSE event for streaming endpoints,
194///    or one total for unary endpoints (carrying the parsed response
195///    body).
196/// 3. `Error { error }` — only on dispatch failure; replaces any
197///    further `Chunk`s for that invocation.
198/// 4. `End` — exactly one, emitted last; signals the AsyncIterable on
199///    the JS side to terminate.
200#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
201#[serde(tag = "type", rename_all = "snake_case")]
202#[schemars(rename = "viewer.ApiCallEnvelope")]
203pub enum ApiCallEnvelope {
204    #[schemars(title = "Begin")]
205    Begin,
206    #[schemars(title = "Chunk")]
207    Chunk { chunk: serde_json::Value },
208    #[schemars(title = "Error")]
209    Error { error: serde_json::Value },
210    #[schemars(title = "End")]
211    End,
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use serde_json::json;
218
219    #[test]
220    fn sub_type_roundtrip_with_underscore_separator() {
221        let s = ApiCallSubType::PostAgentCompletions;
222        let json = serde_json::to_value(&s).unwrap();
223        assert_eq!(json, json!("POST_/agent/completions"));
224        let back: ApiCallSubType = serde_json::from_value(json).unwrap();
225        assert_eq!(back, ApiCallSubType::PostAgentCompletions);
226    }
227
228    #[test]
229    fn auth_keys_disambiguated_by_method() {
230        let post = ApiCallSubType::PostAuthKeys;
231        let del = ApiCallSubType::DeleteAuthKeys;
232        let get = ApiCallSubType::GetAuthKeys;
233        assert_eq!(post.path(), "/auth/keys");
234        assert_eq!(del.path(), "/auth/keys");
235        assert_eq!(get.path(), "/auth/keys");
236        assert_eq!(post.method(), HttpMethod::Post);
237        assert_eq!(del.method(), HttpMethod::Delete);
238        assert_eq!(get.method(), HttpMethod::Get);
239        assert_eq!(
240            serde_json::to_value(&post).unwrap(),
241            json!("POST_/auth/keys")
242        );
243        assert_eq!(
244            serde_json::to_value(&del).unwrap(),
245            json!("DELETE_/auth/keys")
246        );
247        assert_eq!(serde_json::to_value(&get).unwrap(), json!("GET_/auth/keys"));
248    }
249
250    #[test]
251    fn envelope_serializes_as_tagged_enum() {
252        let begin = serde_json::to_value(ApiCallEnvelope::Begin).unwrap();
253        assert_eq!(begin, json!({"type": "begin"}));
254
255        let chunk = serde_json::to_value(ApiCallEnvelope::Chunk {
256            chunk: json!({"x": 1}),
257        })
258        .unwrap();
259        assert_eq!(chunk, json!({"type": "chunk", "chunk": {"x": 1}}));
260
261        let end = serde_json::to_value(ApiCallEnvelope::End).unwrap();
262        assert_eq!(end, json!({"type": "end"}));
263
264        let err = serde_json::to_value(ApiCallEnvelope::Error {
265            error: json!({"message": "boom"}),
266        })
267        .unwrap();
268        assert_eq!(
269            err,
270            json!({"type": "error", "error": {"message": "boom"}})
271        );
272    }
273
274    #[test]
275    fn method_str_matches_serde_rename_prefix() {
276        assert_eq!(HttpMethod::Get.as_str(), "GET");
277        assert_eq!(HttpMethod::Post.as_str(), "POST");
278        assert_eq!(HttpMethod::Delete.as_str(), "DELETE");
279        for variant in [
280            ApiCallSubType::PostAgentCompletions,
281            ApiCallSubType::GetAuthKeys,
282            ApiCallSubType::DeleteAuthKeysOpenrouter,
283        ] {
284            let rename = serde_json::to_string(&variant).unwrap();
285            // rename string is `"METHOD_/path"`; the first quote-stripped
286            // prefix up to `_` is the method's as_str().
287            let prefix = rename.trim_matches('"').split('_').next().unwrap();
288            assert_eq!(prefix, variant.method().as_str());
289        }
290    }
291}