Skip to main content

stakpak_server/
openapi.rs

1#![allow(dead_code)]
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use utoipa::{
6    Modify, OpenApi, ToSchema,
7    openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme},
8};
9use uuid::Uuid;
10
11#[derive(Debug, Serialize, ToSchema)]
12pub struct HealthResponseDoc {
13    pub status: String,
14    pub version: String,
15    pub uptime_seconds: u64,
16}
17
18#[derive(Debug, Serialize, ToSchema)]
19pub struct ErrorResponseDoc {
20    pub error: String,
21    pub code: String,
22    pub request_id: String,
23}
24
25#[derive(Debug, Serialize, Deserialize, ToSchema)]
26#[serde(rename_all = "snake_case")]
27pub enum RunStateDoc {
28    Idle,
29    Starting,
30    Running,
31    Failed,
32}
33
34#[derive(Debug, Serialize, Deserialize, ToSchema)]
35pub struct RunStatusDoc {
36    pub state: RunStateDoc,
37    pub run_id: Option<Uuid>,
38}
39
40#[derive(Debug, Serialize, Deserialize, ToSchema)]
41pub struct SessionDoc {
42    pub id: Uuid,
43    pub title: String,
44    pub cwd: Option<String>,
45    pub created_at: chrono::DateTime<chrono::Utc>,
46    pub updated_at: chrono::DateTime<chrono::Utc>,
47    pub run_status: RunStatusDoc,
48}
49
50#[derive(Debug, Serialize, Deserialize, ToSchema)]
51pub struct SessionsResponseDoc {
52    pub sessions: Vec<SessionDoc>,
53    pub total: usize,
54}
55
56#[derive(Debug, Serialize, Deserialize, ToSchema)]
57pub struct SessionDetailResponseDoc {
58    pub session: SessionDoc,
59    pub config: ConfigResponseDoc,
60}
61
62#[derive(Debug, Serialize, Deserialize, ToSchema)]
63pub struct CreateSessionBodyDoc {
64    pub title: String,
65    pub cwd: Option<String>,
66}
67
68#[derive(Debug, Serialize, Deserialize, ToSchema)]
69#[serde(rename_all = "UPPERCASE")]
70pub enum SessionVisibilityDoc {
71    Private,
72    Public,
73}
74
75#[derive(Debug, Serialize, Deserialize, ToSchema)]
76pub struct UpdateSessionBodyDoc {
77    pub title: Option<String>,
78    pub visibility: Option<SessionVisibilityDoc>,
79}
80
81#[derive(Debug, Serialize, Deserialize, ToSchema)]
82#[serde(rename_all = "snake_case")]
83pub enum SessionMessageTypeDoc {
84    Message,
85    Steering,
86    FollowUp,
87}
88
89#[derive(Debug, Serialize, Deserialize, ToSchema)]
90#[serde(rename_all = "lowercase")]
91pub enum MessageRoleDoc {
92    System,
93    User,
94    Assistant,
95    Tool,
96}
97
98#[derive(Debug, Serialize, Deserialize, ToSchema)]
99#[serde(rename_all = "lowercase")]
100pub enum ImageDetailDoc {
101    Low,
102    High,
103    Auto,
104}
105
106#[derive(Debug, Serialize, Deserialize, ToSchema)]
107#[serde(tag = "type", rename_all = "lowercase")]
108pub enum CacheControlDoc {
109    Ephemeral {
110        #[serde(skip_serializing_if = "Option::is_none")]
111        ttl: Option<String>,
112    },
113}
114
115#[derive(Debug, Serialize, Deserialize, ToSchema)]
116pub struct AnthropicMessageOptionsDoc {
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub cache_control: Option<CacheControlDoc>,
119}
120
121#[derive(Debug, Serialize, Deserialize, ToSchema)]
122pub struct MessageProviderOptionsDoc {
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub anthropic: Option<AnthropicMessageOptionsDoc>,
125}
126
127#[derive(Debug, Serialize, Deserialize, ToSchema)]
128pub struct AnthropicContentPartOptionsDoc {
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub cache_control: Option<CacheControlDoc>,
131}
132
133#[derive(Debug, Serialize, Deserialize, ToSchema)]
134pub struct ContentPartProviderOptionsDoc {
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub anthropic: Option<AnthropicContentPartOptionsDoc>,
137}
138
139#[derive(Debug, Serialize, Deserialize, ToSchema)]
140#[serde(tag = "type", rename_all = "snake_case")]
141pub enum ContentPartDoc {
142    Text {
143        text: String,
144        #[serde(skip_serializing_if = "Option::is_none")]
145        provider_options: Option<ContentPartProviderOptionsDoc>,
146    },
147    Image {
148        url: String,
149        #[serde(skip_serializing_if = "Option::is_none")]
150        detail: Option<ImageDetailDoc>,
151        #[serde(skip_serializing_if = "Option::is_none")]
152        provider_options: Option<ContentPartProviderOptionsDoc>,
153    },
154    ToolCall {
155        id: String,
156        name: String,
157        #[schema(value_type = Object)]
158        arguments: serde_json::Value,
159        #[serde(skip_serializing_if = "Option::is_none")]
160        provider_options: Option<ContentPartProviderOptionsDoc>,
161        #[serde(skip_serializing_if = "Option::is_none")]
162        #[schema(value_type = Object)]
163        metadata: Option<serde_json::Value>,
164    },
165    ToolResult {
166        tool_call_id: String,
167        #[schema(value_type = Object)]
168        content: serde_json::Value,
169        #[serde(skip_serializing_if = "Option::is_none")]
170        provider_options: Option<ContentPartProviderOptionsDoc>,
171    },
172}
173
174#[derive(Debug, Serialize, Deserialize, ToSchema)]
175#[serde(untagged)]
176pub enum MessageContentDoc {
177    Text(String),
178    Parts(Vec<ContentPartDoc>),
179}
180
181#[derive(Debug, Serialize, Deserialize, ToSchema)]
182pub struct StakaiMessageDoc {
183    pub role: MessageRoleDoc,
184    pub content: MessageContentDoc,
185    pub name: Option<String>,
186    pub provider_options: Option<MessageProviderOptionsDoc>,
187}
188
189#[derive(Debug, Serialize, Deserialize, ToSchema)]
190pub struct SessionMessageRequestDoc {
191    pub message: StakaiMessageDoc,
192    #[serde(rename = "type")]
193    pub message_type: Option<SessionMessageTypeDoc>,
194    pub run_id: Option<Uuid>,
195    pub model: Option<String>,
196}
197
198#[derive(Debug, Serialize, Deserialize, ToSchema)]
199pub struct SessionMessageResponseDoc {
200    pub run_id: Uuid,
201}
202
203#[derive(Debug, Serialize, Deserialize, ToSchema)]
204pub struct SessionMessagesResponseDoc {
205    pub messages: Vec<StakaiMessageDoc>,
206    pub total: usize,
207}
208
209#[derive(Debug, Serialize, Deserialize, ToSchema)]
210pub struct ProposedToolCallDoc {
211    pub id: String,
212    pub name: String,
213    #[schema(value_type = Object)]
214    pub arguments: serde_json::Value,
215    #[schema(value_type = Object)]
216    pub metadata: Option<serde_json::Value>,
217}
218
219#[derive(Debug, Serialize, Deserialize, ToSchema)]
220pub struct PendingToolsResponseDoc {
221    pub run_id: Option<Uuid>,
222    pub tool_calls: Vec<ProposedToolCallDoc>,
223}
224
225#[derive(Debug, Serialize, Deserialize, ToSchema)]
226#[serde(rename_all = "snake_case")]
227pub enum DecisionActionDoc {
228    Accept,
229    Reject,
230    CustomResult,
231}
232
233#[derive(Debug, Serialize, Deserialize, ToSchema)]
234pub struct DecisionInputDoc {
235    pub action: DecisionActionDoc,
236    pub content: Option<String>,
237}
238
239#[derive(Debug, Serialize, Deserialize, ToSchema)]
240pub struct ToolDecisionRequestDoc {
241    pub run_id: Uuid,
242    #[serde(flatten)]
243    pub decision: DecisionInputDoc,
244}
245
246#[derive(Debug, Serialize, Deserialize, ToSchema)]
247pub struct ToolDecisionsRequestDoc {
248    pub run_id: Uuid,
249    pub decisions: HashMap<String, DecisionInputDoc>,
250}
251
252#[derive(Debug, Serialize, Deserialize, ToSchema)]
253pub struct ToolDecisionResponseDoc {
254    pub accepted: bool,
255    pub run_id: Uuid,
256}
257
258#[derive(Debug, Serialize, Deserialize, ToSchema)]
259pub struct CancelRequestDoc {
260    pub run_id: Uuid,
261}
262
263#[derive(Debug, Serialize, Deserialize, ToSchema)]
264pub struct CancelResponseDoc {
265    pub cancelled: bool,
266    pub run_id: Uuid,
267}
268
269#[derive(Debug, Serialize, Deserialize, ToSchema)]
270pub struct ModelSwitchRequestDoc {
271    pub run_id: Uuid,
272    pub model: String,
273}
274
275#[derive(Debug, Serialize, Deserialize, ToSchema)]
276pub struct ModelSwitchResponseDoc {
277    pub accepted: bool,
278    pub run_id: Uuid,
279    pub model: String,
280}
281
282#[derive(Debug, Serialize, Deserialize, ToSchema)]
283#[serde(rename_all = "snake_case")]
284pub enum AutoApproveModeDoc {
285    None,
286    All,
287    Custom,
288}
289
290#[derive(Debug, Serialize, Deserialize, ToSchema)]
291pub struct ConfigResponseDoc {
292    pub default_model: Option<String>,
293    pub auto_approve_mode: AutoApproveModeDoc,
294}
295
296#[derive(Debug, Serialize, Deserialize, ToSchema)]
297pub struct ModelCostDoc {
298    pub input: f64,
299    pub output: f64,
300    pub cache_read: Option<f64>,
301    pub cache_write: Option<f64>,
302}
303
304#[derive(Debug, Serialize, Deserialize, ToSchema)]
305pub struct ModelLimitDoc {
306    pub context: u64,
307    pub output: u64,
308}
309
310#[derive(Debug, Serialize, Deserialize, ToSchema)]
311pub struct ModelDoc {
312    pub id: String,
313    pub name: String,
314    pub provider: String,
315    pub reasoning: bool,
316    pub cost: Option<ModelCostDoc>,
317    pub limit: ModelLimitDoc,
318    pub release_date: Option<String>,
319}
320
321#[derive(Debug, Serialize, Deserialize, ToSchema)]
322pub struct ModelsResponseDoc {
323    pub models: Vec<ModelDoc>,
324}
325
326#[derive(Debug, Serialize, Deserialize, ToSchema)]
327pub struct EventEnvelopeDoc {
328    pub id: u64,
329    pub session_id: Uuid,
330    pub run_id: Option<Uuid>,
331    pub timestamp: chrono::DateTime<chrono::Utc>,
332    #[schema(value_type = Object)]
333    pub event: serde_json::Value,
334}
335
336#[derive(Debug, Serialize, Deserialize, ToSchema)]
337pub struct GapDetectedDoc {
338    pub requested_after_id: u64,
339    pub oldest_available_id: u64,
340    pub newest_available_id: u64,
341    pub resume_hint: String,
342}
343
344#[utoipa::path(
345    get,
346    path = "/v1/health",
347    responses((status = 200, body = HealthResponseDoc))
348)]
349fn health_doc() {}
350
351#[utoipa::path(
352    get,
353    path = "/v1/openapi.json",
354    responses((status = 200, description = "OpenAPI 3.1 specification document"))
355)]
356fn openapi_doc() {}
357
358#[utoipa::path(
359    get,
360    path = "/v1/sessions",
361    security(("bearer_auth" = [])),
362    params(
363        ("limit" = Option<u32>, Query, description = "page size"),
364        ("offset" = Option<u32>, Query, description = "page offset"),
365        ("search" = Option<String>, Query, description = "title query"),
366        ("status" = Option<String>, Query, description = "ACTIVE or DELETED")
367    ),
368    responses(
369        (status = 200, body = SessionsResponseDoc),
370        (status = 401, body = ErrorResponseDoc)
371    )
372)]
373fn list_sessions_doc() {}
374
375#[utoipa::path(
376    post,
377    path = "/v1/sessions",
378    security(("bearer_auth" = [])),
379    request_body = CreateSessionBodyDoc,
380    responses(
381        (status = 201, body = SessionDoc),
382        (status = 401, body = ErrorResponseDoc),
383        (status = 409, body = ErrorResponseDoc)
384    )
385)]
386fn create_session_doc() {}
387
388#[utoipa::path(
389    get,
390    path = "/v1/sessions/{id}",
391    security(("bearer_auth" = [])),
392    params(("id" = Uuid, Path, description = "session id")),
393    responses(
394        (status = 200, body = SessionDetailResponseDoc),
395        (status = 401, body = ErrorResponseDoc),
396        (status = 404, body = ErrorResponseDoc)
397    )
398)]
399fn get_session_doc() {}
400
401#[utoipa::path(
402    patch,
403    path = "/v1/sessions/{id}",
404    security(("bearer_auth" = [])),
405    params(("id" = Uuid, Path, description = "session id")),
406    request_body = UpdateSessionBodyDoc,
407    responses(
408        (status = 200, body = SessionDetailResponseDoc),
409        (status = 401, body = ErrorResponseDoc),
410        (status = 404, body = ErrorResponseDoc)
411    )
412)]
413fn update_session_doc() {}
414
415#[utoipa::path(
416    delete,
417    path = "/v1/sessions/{id}",
418    security(("bearer_auth" = [])),
419    params(("id" = Uuid, Path, description = "session id")),
420    responses(
421        (status = 204, description = "Session deleted"),
422        (status = 401, body = ErrorResponseDoc),
423        (status = 404, body = ErrorResponseDoc)
424    )
425)]
426fn delete_session_doc() {}
427
428#[utoipa::path(
429    post,
430    path = "/v1/sessions/{id}/messages",
431    security(("bearer_auth" = [])),
432    params(("id" = Uuid, Path, description = "session id")),
433    request_body = SessionMessageRequestDoc,
434    responses(
435        (status = 200, body = SessionMessageResponseDoc),
436        (status = 401, body = ErrorResponseDoc),
437        (status = 404, body = ErrorResponseDoc),
438        (status = 409, body = ErrorResponseDoc)
439    )
440)]
441fn post_messages_doc() {}
442
443#[utoipa::path(
444    get,
445    path = "/v1/sessions/{id}/messages",
446    security(("bearer_auth" = [])),
447    params(
448        ("id" = Uuid, Path, description = "session id"),
449        ("limit" = Option<usize>, Query, description = "page size"),
450        ("offset" = Option<usize>, Query, description = "page offset")
451    ),
452    responses(
453        (status = 200, body = SessionMessagesResponseDoc),
454        (status = 401, body = ErrorResponseDoc),
455        (status = 404, body = ErrorResponseDoc)
456    )
457)]
458fn get_messages_doc() {}
459
460#[utoipa::path(
461    get,
462    path = "/v1/sessions/{id}/events",
463    security(("bearer_auth" = [])),
464    params(
465        ("id" = Uuid, Path, description = "session id"),
466        ("Last-Event-ID" = Option<u64>, Header, description = "Replay cursor; stream events with id > Last-Event-ID")
467    ),
468    responses((
469        status = 200,
470        description = "text/event-stream of EventEnvelope frames and optional gap_detected control event",
471        content_type = "text/event-stream"
472    ))
473)]
474fn events_doc() {}
475
476#[utoipa::path(
477    get,
478    path = "/v1/sessions/{id}/tools/pending",
479    security(("bearer_auth" = [])),
480    params(("id" = Uuid, Path, description = "session id")),
481    responses(
482        (status = 200, body = PendingToolsResponseDoc),
483        (status = 401, body = ErrorResponseDoc)
484    )
485)]
486fn pending_tools_doc() {}
487
488#[utoipa::path(
489    post,
490    path = "/v1/sessions/{id}/tools/{tool_call_id}/decision",
491    security(("bearer_auth" = [])),
492    params(
493        ("id" = Uuid, Path, description = "session id"),
494        ("tool_call_id" = String, Path, description = "tool call id")
495    ),
496    request_body = ToolDecisionRequestDoc,
497    responses(
498        (status = 200, body = ToolDecisionResponseDoc),
499        (status = 401, body = ErrorResponseDoc),
500        (status = 409, body = ErrorResponseDoc)
501    )
502)]
503fn tool_decision_doc() {}
504
505#[utoipa::path(
506    post,
507    path = "/v1/sessions/{id}/tools/decisions",
508    security(("bearer_auth" = [])),
509    params(("id" = Uuid, Path, description = "session id")),
510    request_body = ToolDecisionsRequestDoc,
511    responses(
512        (status = 200, body = ToolDecisionResponseDoc),
513        (status = 401, body = ErrorResponseDoc),
514        (status = 409, body = ErrorResponseDoc)
515    )
516)]
517fn tool_decisions_doc() {}
518
519#[utoipa::path(
520    post,
521    path = "/v1/sessions/{id}/tools/resolve",
522    security(("bearer_auth" = [])),
523    params(("id" = Uuid, Path, description = "session id")),
524    request_body = ToolDecisionsRequestDoc,
525    responses(
526        (status = 200, body = ToolDecisionResponseDoc),
527        (status = 401, body = ErrorResponseDoc),
528        (status = 409, body = ErrorResponseDoc)
529    )
530)]
531fn tool_resolve_doc() {}
532
533#[utoipa::path(
534    post,
535    path = "/v1/sessions/{id}/cancel",
536    security(("bearer_auth" = [])),
537    params(("id" = Uuid, Path, description = "session id")),
538    request_body = CancelRequestDoc,
539    responses(
540        (status = 200, body = CancelResponseDoc),
541        (status = 401, body = ErrorResponseDoc),
542        (status = 409, body = ErrorResponseDoc)
543    )
544)]
545fn cancel_doc() {}
546
547#[utoipa::path(
548    post,
549    path = "/v1/sessions/{id}/model",
550    security(("bearer_auth" = [])),
551    params(("id" = Uuid, Path, description = "session id")),
552    request_body = ModelSwitchRequestDoc,
553    responses(
554        (status = 200, body = ModelSwitchResponseDoc),
555        (status = 401, body = ErrorResponseDoc),
556        (status = 409, body = ErrorResponseDoc)
557    )
558)]
559fn switch_model_doc() {}
560
561#[utoipa::path(
562    get,
563    path = "/v1/models",
564    security(("bearer_auth" = [])),
565    responses(
566        (status = 200, body = ModelsResponseDoc),
567        (status = 401, body = ErrorResponseDoc)
568    )
569)]
570fn models_doc() {}
571
572#[utoipa::path(
573    get,
574    path = "/v1/config",
575    security(("bearer_auth" = [])),
576    responses(
577        (status = 200, body = ConfigResponseDoc),
578        (status = 401, body = ErrorResponseDoc)
579    )
580)]
581fn config_doc() {}
582
583struct SecurityAddon;
584
585impl Modify for SecurityAddon {
586    fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
587        let components = openapi.components.get_or_insert_with(Default::default);
588        components.add_security_scheme(
589            "bearer_auth",
590            SecurityScheme::Http(
591                HttpBuilder::new()
592                    .scheme(HttpAuthScheme::Bearer)
593                    .bearer_format("Bearer")
594                    .build(),
595            ),
596        );
597    }
598}
599
600#[derive(OpenApi)]
601#[openapi(
602    paths(
603        health_doc,
604        openapi_doc,
605        list_sessions_doc,
606        create_session_doc,
607        get_session_doc,
608        update_session_doc,
609        delete_session_doc,
610        post_messages_doc,
611        get_messages_doc,
612        events_doc,
613        pending_tools_doc,
614        tool_decision_doc,
615        tool_decisions_doc,
616        tool_resolve_doc,
617        cancel_doc,
618        switch_model_doc,
619        models_doc,
620        config_doc,
621    ),
622    components(
623        schemas(
624            HealthResponseDoc,
625            ErrorResponseDoc,
626            RunStateDoc,
627            RunStatusDoc,
628            SessionDoc,
629            SessionsResponseDoc,
630            SessionDetailResponseDoc,
631            CreateSessionBodyDoc,
632            SessionVisibilityDoc,
633            UpdateSessionBodyDoc,
634            SessionMessageTypeDoc,
635            MessageRoleDoc,
636            ImageDetailDoc,
637            CacheControlDoc,
638            AnthropicMessageOptionsDoc,
639            MessageProviderOptionsDoc,
640            AnthropicContentPartOptionsDoc,
641            ContentPartProviderOptionsDoc,
642            ContentPartDoc,
643            MessageContentDoc,
644            StakaiMessageDoc,
645            SessionMessageRequestDoc,
646            SessionMessageResponseDoc,
647            SessionMessagesResponseDoc,
648            ProposedToolCallDoc,
649            PendingToolsResponseDoc,
650            DecisionActionDoc,
651            DecisionInputDoc,
652            ToolDecisionRequestDoc,
653            ToolDecisionsRequestDoc,
654            ToolDecisionResponseDoc,
655            CancelRequestDoc,
656            CancelResponseDoc,
657            ModelSwitchRequestDoc,
658            ModelSwitchResponseDoc,
659            AutoApproveModeDoc,
660            ConfigResponseDoc,
661            ModelCostDoc,
662            ModelLimitDoc,
663            ModelDoc,
664            ModelsResponseDoc,
665            EventEnvelopeDoc,
666            GapDetectedDoc,
667        )
668    ),
669    modifiers(&SecurityAddon),
670    tags(
671        (name = "server", description = "Stakpak server runtime APIs")
672    )
673)]
674pub struct ApiDoc;
675
676pub fn generate_openapi() -> utoipa::openapi::OpenApi {
677    ApiDoc::openapi()
678}