Skip to main content

quantum_sdk/
jobs.rs

1use serde::{Deserialize, Serialize};
2
3use crate::chat::ChatRequest;
4use crate::client::Client;
5use crate::error::Result;
6
7/// Request to create an async job.
8#[derive(Debug, Clone, Serialize)]
9pub struct JobCreateRequest {
10    /// Job type (e.g. "video/generate", "audio/music").
11    #[serde(rename = "type")]
12    pub job_type: String,
13
14    /// Job parameters (model-specific).
15    pub params: serde_json::Value,
16}
17
18/// Response from job creation.
19#[derive(Debug, Clone, Deserialize)]
20pub struct JobCreateResponse {
21    pub job_id: String,
22    #[serde(default)]
23    pub status: String,
24}
25
26/// Response from job status check.
27#[derive(Debug, Clone, Deserialize)]
28pub struct JobStatusResponse {
29    pub job_id: String,
30    pub status: String,
31    #[serde(default)]
32    pub result: Option<serde_json::Value>,
33    #[serde(default)]
34    pub error: Option<String>,
35    #[serde(default)]
36    pub cost_ticks: i64,
37}
38
39/// Summary of a job in the list response.
40#[derive(Debug, Clone, Deserialize)]
41pub struct JobSummary {
42    pub job_id: String,
43    pub status: String,
44    #[serde(rename = "type", default)]
45    pub job_type: Option<String>,
46    #[serde(default)]
47    pub created_at: Option<String>,
48    #[serde(default)]
49    pub completed_at: Option<String>,
50    #[serde(default)]
51    pub cost_ticks: i64,
52}
53
54/// Response from listing jobs.
55#[derive(Debug, Clone, Deserialize)]
56pub struct ListJobsResponse {
57    pub jobs: Vec<JobSummary>,
58}
59
60impl Client {
61    /// Creates an async job. Returns the job ID for polling.
62    pub async fn create_job(&self, req: &JobCreateRequest) -> Result<JobCreateResponse> {
63        let (resp, _meta) = self
64            .post_json::<JobCreateRequest, JobCreateResponse>("/qai/v1/jobs", req)
65            .await?;
66        Ok(resp)
67    }
68
69    /// Checks the status of an async job.
70    pub async fn get_job(&self, job_id: &str) -> Result<JobStatusResponse> {
71        let path = format!("/qai/v1/jobs/{job_id}");
72        let (resp, _meta) = self.get_json::<JobStatusResponse>(&path).await?;
73        Ok(resp)
74    }
75
76    /// Lists all jobs for the account.
77    pub async fn list_jobs(&self) -> Result<ListJobsResponse> {
78        let (resp, _meta) = self
79            .get_json::<ListJobsResponse>("/qai/v1/jobs")
80            .await?;
81        Ok(resp)
82    }
83
84    /// Opens an SSE stream for a job, returning the raw response.
85    /// Events: progress, complete, error. Auto-closes on terminal state.
86    pub async fn stream_job(
87        &self,
88        job_id: &str,
89    ) -> Result<reqwest::Response> {
90        let path = format!("/qai/v1/jobs/{job_id}/stream");
91        let (resp, _meta) = self.get_stream_raw(&path).await?;
92        Ok(resp)
93    }
94
95    /// Polls a job until completion or timeout.
96    /// Returns the final status response.
97    pub async fn poll_job(
98        &self,
99        job_id: &str,
100        poll_interval: std::time::Duration,
101        max_attempts: usize,
102    ) -> Result<JobStatusResponse> {
103        for _ in 0..max_attempts {
104            tokio::time::sleep(poll_interval).await;
105            let status = self.get_job(job_id).await?;
106            match status.status.as_str() {
107                "completed" | "failed" => return Ok(status),
108                _ => continue,
109            }
110        }
111        Ok(JobStatusResponse {
112            job_id: job_id.to_string(),
113            status: "timeout".to_string(),
114            result: None,
115            error: Some(format!("Job polling timed out after {max_attempts} attempts")),
116            cost_ticks: 0,
117        })
118    }
119
120    /// Convenience method for 3D model generation via the async jobs system.
121    ///
122    /// Submits a job with type `"3d/generate"` and the given parameters.
123    /// Returns the job creation response -- use `poll_job` to wait for completion.
124    pub async fn generate_3d(
125        &self,
126        model: &str,
127        prompt: Option<&str>,
128        image_url: Option<&str>,
129    ) -> Result<JobCreateResponse> {
130        let mut params = serde_json::json!({ "model": model });
131        if let Some(p) = prompt {
132            params["prompt"] = serde_json::Value::String(p.to_string());
133        }
134        if let Some(u) = image_url {
135            params["image_url"] = serde_json::Value::String(u.to_string());
136        }
137        let req = JobCreateRequest {
138            job_type: "3d/generate".to_string(),
139            params,
140        };
141        self.create_job(&req).await
142    }
143
144    /// Submits a chat completion as an async job.
145    ///
146    /// Useful for long-running models (e.g. Opus) where synchronous `/qai/v1/chat`
147    /// may time out. Params are the same shape as [`ChatRequest`].
148    /// Use [`stream_job()`] or [`poll_job()`] to get the result.
149    pub async fn chat_job(&self, req: &ChatRequest) -> Result<JobCreateResponse> {
150        let params = serde_json::to_value(req)?;
151        let job_req = JobCreateRequest {
152            job_type: "chat".to_string(),
153            params,
154        };
155        self.create_job(&job_req).await
156    }
157}