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
60/// A single SSE event from a job stream.
61#[derive(Debug, Clone, Deserialize)]
62pub struct JobStreamEvent {
63    /// Event type (e.g. "progress", "complete", "error").
64    #[serde(rename = "type", default)]
65    pub event_type: String,
66
67    /// Job identifier.
68    #[serde(default)]
69    pub job_id: Option<String>,
70
71    /// Job status.
72    #[serde(default)]
73    pub status: Option<String>,
74
75    /// Job result (on completion).
76    #[serde(default)]
77    pub result: Option<serde_json::Value>,
78
79    /// Error message (on failure).
80    #[serde(default)]
81    pub error: Option<String>,
82
83    /// Total cost in ticks.
84    #[serde(default)]
85    pub cost_ticks: i64,
86
87    /// Completion timestamp.
88    #[serde(default)]
89    pub completed_at: Option<String>,
90}
91
92/// Response from async job submission (HeyGen, 3D, etc.).
93/// The client should poll the job status using `get_job`.
94#[derive(Debug, Clone, Deserialize)]
95pub struct JobAcceptedResponse {
96    /// Unique job identifier for polling.
97    pub job_id: String,
98
99    /// Initial job status (e.g. "pending").
100    #[serde(default)]
101    pub status: String,
102
103    /// Job type.
104    #[serde(rename = "type", default)]
105    pub job_type: Option<String>,
106
107    /// Unique request identifier.
108    #[serde(default)]
109    pub request_id: Option<String>,
110}
111
112/// A single job entry in the detailed job list response.
113#[derive(Debug, Clone, Deserialize)]
114pub struct JobListEntry {
115    /// Unique job identifier.
116    pub job_id: String,
117
118    /// Job type (e.g. "video/generate", "audio/tts").
119    #[serde(rename = "type", default)]
120    pub job_type: Option<String>,
121
122    /// Job status ("pending", "processing", "completed", "failed").
123    pub status: String,
124
125    /// Job output when completed.
126    #[serde(default)]
127    pub result: Option<serde_json::Value>,
128
129    /// Error message if the job failed.
130    #[serde(default)]
131    pub error: Option<String>,
132
133    /// Total cost in ticks.
134    #[serde(default)]
135    pub cost_ticks: i64,
136
137    /// Job creation timestamp.
138    #[serde(default)]
139    pub created_at: Option<String>,
140
141    /// When processing began.
142    #[serde(default)]
143    pub started_at: Option<String>,
144
145    /// When the job finished.
146    #[serde(default)]
147    pub completed_at: Option<String>,
148
149    /// Originating request identifier.
150    #[serde(default)]
151    pub request_id: Option<String>,
152}
153
154/// Response from listing jobs (detailed variant).
155#[derive(Debug, Clone, Deserialize)]
156pub struct JobListResponse {
157    /// The list of jobs.
158    pub jobs: Vec<JobListEntry>,
159
160    /// Unique request identifier.
161    #[serde(default)]
162    pub request_id: Option<String>,
163}
164
165impl Client {
166    /// Creates an async job. Returns the job ID for polling.
167    pub async fn create_job(&self, req: &JobCreateRequest) -> Result<JobCreateResponse> {
168        let (resp, _meta) = self
169            .post_json::<JobCreateRequest, JobCreateResponse>("/qai/v1/jobs", req)
170            .await?;
171        Ok(resp)
172    }
173
174    /// Checks the status of an async job.
175    pub async fn get_job(&self, job_id: &str) -> Result<JobStatusResponse> {
176        let path = format!("/qai/v1/jobs/{job_id}");
177        let (resp, _meta) = self.get_json::<JobStatusResponse>(&path).await?;
178        Ok(resp)
179    }
180
181    /// Lists all jobs for the account.
182    pub async fn list_jobs(&self) -> Result<ListJobsResponse> {
183        let (resp, _meta) = self
184            .get_json::<ListJobsResponse>("/qai/v1/jobs")
185            .await?;
186        Ok(resp)
187    }
188
189    /// Opens an SSE stream for a job, returning the raw response.
190    /// Events: progress, complete, error. Auto-closes on terminal state.
191    pub async fn stream_job(
192        &self,
193        job_id: &str,
194    ) -> Result<reqwest::Response> {
195        let path = format!("/qai/v1/jobs/{job_id}/stream");
196        let (resp, _meta) = self.get_stream_raw(&path).await?;
197        Ok(resp)
198    }
199
200    /// Polls a job until completion or timeout.
201    /// Returns the final status response.
202    pub async fn poll_job(
203        &self,
204        job_id: &str,
205        poll_interval: std::time::Duration,
206        max_attempts: usize,
207    ) -> Result<JobStatusResponse> {
208        for _ in 0..max_attempts {
209            tokio::time::sleep(poll_interval).await;
210            let status = self.get_job(job_id).await?;
211            match status.status.as_str() {
212                "completed" | "failed" => return Ok(status),
213                _ => continue,
214            }
215        }
216        Ok(JobStatusResponse {
217            job_id: job_id.to_string(),
218            status: "timeout".to_string(),
219            result: None,
220            error: Some(format!("Job polling timed out after {max_attempts} attempts")),
221            cost_ticks: 0,
222        })
223    }
224
225    /// Convenience method for 3D model generation via the async jobs system.
226    ///
227    /// Submits a job with type `"3d/generate"` and the given parameters.
228    /// Returns the job creation response -- use `poll_job` to wait for completion.
229    pub async fn generate_3d(
230        &self,
231        model: &str,
232        prompt: Option<&str>,
233        image_url: Option<&str>,
234    ) -> Result<JobCreateResponse> {
235        let mut params = serde_json::json!({ "model": model });
236        if let Some(p) = prompt {
237            params["prompt"] = serde_json::Value::String(p.to_string());
238        }
239        if let Some(u) = image_url {
240            params["image_url"] = serde_json::Value::String(u.to_string());
241        }
242        let req = JobCreateRequest {
243            job_type: "3d/generate".to_string(),
244            params,
245        };
246        self.create_job(&req).await
247    }
248
249    /// Submits a chat completion as an async job.
250    ///
251    /// Useful for long-running models (e.g. Opus) where synchronous `/qai/v1/chat`
252    /// may time out. Params are the same shape as [`ChatRequest`].
253    /// Use [`stream_job()`] or [`poll_job()`] to get the result.
254    pub async fn chat_job(&self, req: &ChatRequest) -> Result<JobCreateResponse> {
255        let params = serde_json::to_value(req)?;
256        let job_req = JobCreateRequest {
257            job_type: "chat".to_string(),
258            params,
259        };
260        self.create_job(&job_req).await
261    }
262}