1use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash)]
10#[serde(rename_all = "snake_case")]
11pub enum TaskKind {
12 Image,
13 Llm,
14 AudioStt,
15 AudioTts,
16 Video,
17}
18
19impl TaskKind {
20 pub const ALL: [TaskKind; 5] = [
21 TaskKind::Image,
22 TaskKind::Llm,
23 TaskKind::AudioStt,
24 TaskKind::AudioTts,
25 TaskKind::Video,
26 ];
27
28 pub fn as_str(&self) -> &'static str {
29 match self {
30 TaskKind::Image => "image",
31 TaskKind::Llm => "llm",
32 TaskKind::AudioStt => "audio_stt",
33 TaskKind::AudioTts => "audio_tts",
34 TaskKind::Video => "video",
35 }
36 }
37}
38
39impl std::fmt::Display for TaskKind {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 f.write_str(self.as_str())
42 }
43}
44
45#[derive(Debug, Clone, Default, Serialize, Deserialize)]
50#[serde(rename_all = "camelCase")]
51pub struct ImageParams {
52 pub prompt: String,
53 #[serde(default)]
57 pub negative_prompt: Option<String>,
58 #[serde(default)]
63 pub init_image_url: Option<String>,
64 #[serde(default)]
69 pub mask_url: Option<String>,
70 #[serde(default)]
78 pub ref_image_url: Option<String>,
79 #[serde(default)]
82 pub denoise: Option<f32>,
83 #[serde(default)]
86 pub cfg_scale: Option<f32>,
87 #[serde(default)]
90 pub sampling_method: Option<String>,
91 #[serde(default = "default_image_dim")]
92 pub width: u32,
93 #[serde(default = "default_image_dim")]
94 pub height: u32,
95 #[serde(default = "default_steps")]
96 pub steps: u32,
97 #[serde(default)]
98 pub seed: Option<u64>,
99 #[serde(default = "default_image_ext")]
100 pub ext: String,
101}
102
103fn default_image_dim() -> u32 {
104 512
105}
106fn default_steps() -> u32 {
107 20
108}
109fn default_image_ext() -> String {
110 "webp".into()
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct ChatMessage {
115 pub role: String,
116 pub content: String,
117}
118
119#[derive(Debug, Clone, Default, Serialize, Deserialize)]
120#[serde(rename_all = "camelCase")]
121pub struct LlmParams {
122 pub messages: Vec<ChatMessage>,
123 #[serde(default)]
127 pub system: Option<String>,
128 #[serde(default = "default_max_tokens")]
129 pub max_tokens: u32,
130 #[serde(default = "default_temperature")]
131 pub temperature: f32,
132 #[serde(default)]
133 pub top_p: Option<f32>,
134 #[serde(default)]
135 pub stop: Option<Vec<String>>,
136 #[serde(default)]
139 pub json_schema: Option<serde_json::Value>,
140 #[serde(default)]
143 pub reasoning: Option<String>,
144 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub chat_template_kwargs: Option<serde_json::Map<String, serde_json::Value>>,
148}
149
150fn default_max_tokens() -> u32 {
151 512
152}
153fn default_temperature() -> f32 {
154 0.7
155}
156
157#[derive(Debug, Clone, Default, Serialize, Deserialize)]
158#[serde(rename_all = "camelCase")]
159pub struct AudioSttParams {
160 pub input_url: String,
162 #[serde(default)]
163 pub language: Option<String>,
164 #[serde(default)]
166 pub translate: Option<bool>,
167 #[serde(default)]
169 pub prompt: Option<String>,
170 #[serde(default)]
172 pub vad: Option<bool>,
173 #[serde(default)]
175 pub timestamps: Option<String>,
176}
177
178#[derive(Debug, Clone, Default, Serialize, Deserialize)]
179#[serde(rename_all = "camelCase")]
180pub struct AudioTtsParams {
181 pub text: String,
182 #[serde(default = "default_voice")]
183 pub voice: String,
184 #[serde(default)]
186 pub speed: Option<f32>,
187 #[serde(default)]
189 pub language: Option<String>,
190 #[serde(default = "default_audio_ext")]
191 pub ext: String,
192}
193
194fn default_voice() -> String {
195 "default".into()
196}
197fn default_audio_ext() -> String {
198 "wav".into()
199}
200
201#[derive(Debug, Clone, Default, Serialize, Deserialize)]
202#[serde(rename_all = "camelCase")]
203pub struct VideoParams {
204 pub prompt: String,
205 #[serde(default)]
206 pub negative_prompt: Option<String>,
207 #[serde(default)]
209 pub init_image_url: Option<String>,
210 #[serde(default = "default_video_seconds")]
211 pub seconds: f32,
212 #[serde(default)]
214 pub fps: Option<u32>,
215 #[serde(default = "default_image_dim")]
216 pub width: u32,
217 #[serde(default = "default_image_dim")]
218 pub height: u32,
219 #[serde(default = "default_video_ext")]
220 pub ext: String,
221}
222
223fn default_video_seconds() -> f32 {
224 2.0
225}
226fn default_video_ext() -> String {
227 "mp4".into()
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
231#[serde(tag = "kind", rename_all = "snake_case")]
232pub enum Task {
233 Image(ImageParams),
234 Llm(LlmParams),
235 AudioStt(AudioSttParams),
236 AudioTts(AudioTtsParams),
237 Video(VideoParams),
238}
239
240impl Task {
241 pub fn kind(&self) -> TaskKind {
242 match self {
243 Task::Image(_) => TaskKind::Image,
244 Task::Llm(_) => TaskKind::Llm,
245 Task::AudioStt(_) => TaskKind::AudioStt,
246 Task::AudioTts(_) => TaskKind::AudioTts,
247 Task::Video(_) => TaskKind::Video,
248 }
249 }
250}
251
252#[derive(Debug, Clone)]
257pub enum TaskResult {
258 Image { bytes: Vec<u8>, ext: String },
260 Llm { json: serde_json::Value },
262 AudioStt { json: serde_json::Value },
264 AudioTts { bytes: Vec<u8>, ext: String },
266 Video { bytes: Vec<u8>, ext: String },
268}
269
270impl TaskResult {
271 pub fn kind(&self) -> TaskKind {
272 match self {
273 TaskResult::Image { .. } => TaskKind::Image,
274 TaskResult::Llm { .. } => TaskKind::Llm,
275 TaskResult::AudioStt { .. } => TaskKind::AudioStt,
276 TaskResult::AudioTts { .. } => TaskKind::AudioTts,
277 TaskResult::Video { .. } => TaskKind::Video,
278 }
279 }
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct WorkerCapabilities {
288 #[serde(rename = "machineName")]
289 pub machine_name: String,
290 pub username: String,
291 #[serde(rename = "agentVersion")]
292 pub agent_version: String,
293 pub engine: String,
294 #[serde(rename = "vramTotalGb")]
295 pub vram_total_gb: f32,
296 #[serde(rename = "vramThresholdGb")]
297 pub vram_threshold_gb: f32,
298 #[serde(rename = "autoEnabled")]
299 pub auto_enabled: bool,
300 #[serde(rename = "autoStart")]
301 pub auto_start: bool,
302 #[serde(rename = "supportedModels")]
306 pub supported_models: Vec<String>,
307 #[serde(rename = "taskKinds", default)]
309 pub task_kinds: Vec<TaskKind>,
310 #[serde(rename = "supportedModelsPerKind", default)]
312 pub supported_models_per_kind: BTreeMap<TaskKind, Vec<String>>,
313}
314
315#[derive(Debug, Clone, Serialize)]
325pub struct AutoRegisterRequest {
326 #[serde(rename = "installId")]
329 pub install_id: String,
330 #[serde(rename = "registrationSecretHash")]
334 pub registration_secret_hash: String,
335 pub capabilities: WorkerCapabilities,
338 #[serde(rename = "userAgent")]
340 pub user_agent: String,
341}
342
343#[derive(Debug, Clone, Deserialize)]
344pub struct AutoRegisterRequestResponse {
345 #[serde(rename = "requestId")]
346 pub request_id: String,
347 pub status: String,
351}
352
353#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
355#[serde(rename_all = "snake_case", tag = "status")]
356pub enum RegisterStatus {
357 Pending,
358 Approved {
359 #[serde(rename = "workerId")]
360 worker_id: String,
361 #[serde(rename = "authToken")]
362 auth_token: String,
363 },
364 Rejected {
365 #[serde(default)]
366 reason: String,
367 },
368}
369
370#[derive(Debug, Clone, Serialize)]
371pub struct HeartbeatRequest {
372 pub capabilities: WorkerCapabilities,
373 #[serde(rename = "currentJobId", skip_serializing_if = "Option::is_none")]
374 pub current_job_id: Option<String>,
375}
376
377#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
384#[serde(rename_all = "kebab-case")]
385pub enum ModelFileRole {
386 DiffusionModel,
387 TextEncoder,
388 TextEncoderVision,
392 Vae,
393 Lora,
394 Model,
395}
396
397#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
398#[serde(rename_all = "kebab-case")]
399pub enum ModelEngine {
400 SdCpp,
401 LlamaCpp,
402 Onnx,
405 Parakeet,
408 Synthetic,
409}
410
411#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
412#[serde(rename_all = "camelCase")]
413pub struct ModelFile {
414 pub role: ModelFileRole,
415 pub url: String,
416 pub filename: String,
417 #[serde(default, skip_serializing_if = "Option::is_none")]
418 pub approx_bytes: Option<u64>,
419 #[serde(default, skip_serializing_if = "Option::is_none")]
424 pub sha256: Option<String>,
425}
426
427#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
428#[serde(rename_all = "camelCase")]
429pub struct ModelCliDefaults {
430 pub cfg_scale: f32,
431 pub steps: u32,
432 pub width: u32,
433 pub height: u32,
434 #[serde(default, skip_serializing_if = "Option::is_none")]
435 pub sampling_method: Option<String>,
436 #[serde(default, skip_serializing_if = "Option::is_none")]
438 pub flow_shift: Option<f32>,
439 #[serde(default, skip_serializing_if = "Option::is_none")]
441 pub zero_cond_t: Option<bool>,
442 #[serde(default, skip_serializing_if = "Option::is_none")]
444 pub offload_to_cpu: Option<bool>,
445 #[serde(default, skip_serializing_if = "Option::is_none")]
447 pub context_size: Option<u32>,
448 #[serde(default, skip_serializing_if = "Option::is_none")]
451 pub chat_template_kwargs: Option<serde_json::Map<String, serde_json::Value>>,
452}
453
454#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
455#[serde(rename_all = "camelCase")]
456pub struct ModelSource {
457 pub engine: ModelEngine,
458 pub files: Vec<ModelFile>,
459 pub cli_defaults: ModelCliDefaults,
460}
461
462#[derive(Debug, Clone, Deserialize)]
468#[serde(rename_all = "camelCase")]
469pub struct JobClaim {
470 pub job_id: String,
471 #[allow(dead_code)]
472 pub game_id: String,
473 pub asset_name: String,
474 pub model: String,
475 pub vram_gb_estimate: f32,
476 pub task: Task,
480 pub model_source: ModelSource,
484}
485
486#[derive(Debug, Clone, Serialize)]
487pub struct FailRequest {
488 pub error: String,
489 pub retryable: bool,
490}
491
492#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
493pub struct LogEntry {
494 pub ts: String,
495 pub level: String,
496 pub category: String,
497 pub message: String,
498 #[serde(rename = "jobId", default, skip_serializing_if = "Option::is_none")]
499 pub job_id: Option<String>,
500}
501
502#[derive(Debug, Clone, Serialize, Deserialize)]
503pub struct LogBatch {
504 pub entries: Vec<LogEntry>,
505}
506
507#[derive(Debug, Clone, Deserialize)]
513pub struct GithubRelease {
514 pub tag_name: String,
515 #[serde(default)]
516 pub prerelease: bool,
517 #[serde(default)]
518 pub draft: bool,
519 #[serde(default)]
520 pub assets: Vec<GithubReleaseAsset>,
521}
522
523#[derive(Debug, Clone, Deserialize)]
524pub struct GithubReleaseAsset {
525 pub name: String,
526 pub browser_download_url: String,
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532
533 fn synthetic_model_source_json() -> serde_json::Value {
534 serde_json::json!({
535 "engine": "synthetic",
536 "files": [],
537 "cliDefaults": {
538 "cfgScale": 1.0,
539 "steps": 8,
540 "width": 1024,
541 "height": 1024,
542 },
543 })
544 }
545
546 #[test]
547 fn job_claim_requires_task_and_model_source() {
548 let bare = serde_json::json!({
551 "jobId": "j-1",
552 "gameId": "g-1",
553 "assetName": "g-1/creatures/x",
554 "model": "synthetic-image",
555 "vramGbEstimate": 1.0,
556 });
557 assert!(
558 serde_json::from_value::<JobClaim>(bare).is_err(),
559 "JobClaim must reject missing task + modelSource"
560 );
561 }
562
563 #[test]
564 fn job_claim_with_explicit_llm_task() {
565 let json = serde_json::json!({
566 "jobId": "j-2",
567 "gameId": "g-1",
568 "assetName": "g-1/conversations/x",
569 "model": "llama-3.1-8b",
570 "vramGbEstimate": 8.0,
571 "task": {
572 "kind": "llm",
573 "messages": [{"role": "user", "content": "hi"}],
574 "maxTokens": 32,
575 "temperature": 0.5,
576 },
577 "modelSource": synthetic_model_source_json(),
578 });
579 let claim: JobClaim = serde_json::from_value(json).unwrap();
580 match claim.task {
581 Task::Llm(p) => {
582 assert_eq!(p.messages.len(), 1);
583 assert_eq!(p.max_tokens, 32);
584 }
585 other => panic!("expected llm, got {:?}", other),
586 }
587 }
588
589 #[test]
590 fn job_claim_with_explicit_image_task() {
591 let json = serde_json::json!({
592 "jobId": "j-3",
593 "gameId": "g-1",
594 "assetName": "g-1/creatures/y",
595 "model": "synthetic-image",
596 "vramGbEstimate": 8.0,
597 "task": {
598 "kind": "image",
599 "prompt": "a koi",
600 "width": 1024,
601 "height": 1024,
602 "steps": 30,
603 "ext": "png",
604 },
605 "modelSource": synthetic_model_source_json(),
606 });
607 let claim: JobClaim = serde_json::from_value(json).unwrap();
608 match claim.task {
609 Task::Image(p) => {
610 assert_eq!(p.prompt, "a koi");
611 assert_eq!(p.width, 1024);
612 assert_eq!(p.ext, "png");
613 }
614 other => panic!("expected image, got {:?}", other),
615 }
616 }
617
618 #[test]
619 fn image_params_round_trips_with_new_fields() {
620 let json = serde_json::json!({
621 "kind": "image",
622 "prompt": "a stone golem",
623 "negativePrompt": "text, watermark, low quality",
624 "initImageUrl": "https://example.invalid/t2-golem-stone/latest.webp",
625 "denoise": 0.55,
626 "cfgScale": 7.5,
627 "samplingMethod": "dpm++2m",
628 "width": 768,
629 "height": 512,
630 "steps": 30,
631 "seed": 1234,
632 "ext": "webp",
633 });
634 let task: Task = serde_json::from_value(json).unwrap();
635 match task {
636 Task::Image(p) => {
637 assert_eq!(p.prompt, "a stone golem");
638 assert_eq!(
639 p.negative_prompt.as_deref(),
640 Some("text, watermark, low quality")
641 );
642 assert_eq!(
643 p.init_image_url.as_deref(),
644 Some("https://example.invalid/t2-golem-stone/latest.webp")
645 );
646 assert!((p.denoise.unwrap() - 0.55).abs() < 1e-6);
647 assert!((p.cfg_scale.unwrap() - 7.5).abs() < 1e-6);
648 assert_eq!(p.sampling_method.as_deref(), Some("dpm++2m"));
649 assert_eq!(p.width, 768);
650 assert_eq!(p.height, 512);
651 assert_eq!(p.steps, 30);
652 assert_eq!(p.seed, Some(1234));
653 }
654 other => panic!("expected image, got {:?}", other),
655 }
656 }
657
658 #[test]
659 fn image_params_defaults_when_optional_fields_absent() {
660 let json = serde_json::json!({
661 "kind": "image",
662 "prompt": "a fox"
663 });
664 let task: Task = serde_json::from_value(json).unwrap();
665 match task {
666 Task::Image(p) => {
667 assert_eq!(p.prompt, "a fox");
668 assert!(p.negative_prompt.is_none());
669 assert!(p.init_image_url.is_none());
670 assert!(p.denoise.is_none());
671 assert!(p.cfg_scale.is_none());
672 assert!(p.sampling_method.is_none());
673 assert_eq!(p.width, 512);
674 assert_eq!(p.height, 512);
675 assert_eq!(p.steps, 20);
676 assert_eq!(p.ext, "webp");
677 }
678 other => panic!("expected image, got {:?}", other),
679 }
680 }
681
682 #[test]
683 fn task_kinds_round_trip_via_json() {
684 for kind in TaskKind::ALL {
685 let s = serde_json::to_string(&kind).unwrap();
686 let back: TaskKind = serde_json::from_str(&s).unwrap();
687 assert_eq!(kind, back);
688 }
689 }
690}