synth-ai 0.2.16__py3-none-any.whl → 0.2.17__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of synth-ai might be problematic. Click here for more details.

Files changed (192) hide show
  1. examples/analyze_semantic_words.sh +2 -2
  2. examples/blog_posts/pokemon_vl/README.md +98 -0
  3. examples/blog_posts/pokemon_vl/configs/eval_qwen3_vl.toml +25 -0
  4. examples/blog_posts/pokemon_vl/configs/eval_rl_final.toml +24 -0
  5. examples/blog_posts/pokemon_vl/configs/filter_high_reward.toml +10 -0
  6. examples/blog_posts/pokemon_vl/configs/train_rl_from_sft.toml +42 -0
  7. examples/blog_posts/pokemon_vl/configs/train_sft_qwen4b_vl.toml +40 -0
  8. examples/blog_posts/warming_up_to_rl/README.md +158 -0
  9. examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b.toml +25 -0
  10. examples/blog_posts/warming_up_to_rl/configs/eval_groq_qwen32b.toml +25 -0
  11. examples/blog_posts/warming_up_to_rl/configs/eval_openai_gpt_oss_120b.toml +29 -0
  12. examples/blog_posts/warming_up_to_rl/configs/filter_high_reward_dataset.toml +10 -0
  13. examples/blog_posts/warming_up_to_rl/configs/train_rl_from_sft.toml +41 -0
  14. examples/blog_posts/warming_up_to_rl/configs/train_sft_qwen4b.toml +40 -0
  15. examples/dev/qwen3_32b_qlora_4xh100.toml +5 -0
  16. examples/multi_step/configs/crafter_rl_outcome.toml +1 -1
  17. examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +65 -107
  18. examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +1 -1
  19. examples/multi_step/configs/crafter_rl_stepwise_simple.toml +1 -1
  20. examples/multi_step/configs/crafter_rl_stepwise_simple_NEW_FORMAT.toml +105 -0
  21. examples/multi_step/configs/verilog_rl_lora.toml +80 -123
  22. examples/qwen_coder/configs/coder_lora_30b.toml +1 -3
  23. examples/qwen_coder/configs/coder_lora_4b.toml +4 -1
  24. examples/qwen_coder/configs/coder_lora_small.toml +1 -3
  25. examples/qwen_vl/README.md +10 -12
  26. examples/qwen_vl/SETUP_COMPLETE.md +7 -8
  27. examples/qwen_vl/VISION_TESTS_COMPLETE.md +2 -3
  28. examples/qwen_vl/collect_data_via_cli.md +76 -84
  29. examples/qwen_vl/collect_vision_traces.py +4 -4
  30. examples/qwen_vl/configs/crafter_rl_vision_qwen3vl4b.toml +40 -57
  31. examples/qwen_vl/configs/crafter_vlm_sft_example.toml +1 -2
  32. examples/qwen_vl/configs/eval_gpt4o_mini_vision.toml +20 -37
  33. examples/qwen_vl/configs/eval_gpt5nano_vision.toml +21 -40
  34. examples/qwen_vl/configs/eval_qwen3vl_vision.toml +26 -0
  35. examples/qwen_vl/configs/{filter_qwen2vl_sft.toml → filter_qwen3vl_sft.toml} +4 -5
  36. examples/qwen_vl/configs/filter_vision_sft.toml +2 -3
  37. examples/qwen_vl/crafter_qwen_vl_agent.py +5 -5
  38. examples/qwen_vl/run_vision_comparison.sh +6 -7
  39. examples/rl/README.md +5 -5
  40. examples/rl/configs/rl_from_base_qwen.toml +26 -1
  41. examples/rl/configs/rl_from_base_qwen17.toml +5 -2
  42. examples/rl/task_app/README.md +1 -2
  43. examples/rl/task_app/math_single_step.py +2 -2
  44. examples/run_crafter_demo.sh +2 -2
  45. examples/sft/README.md +1 -1
  46. examples/sft/configs/crafter_fft_qwen0p6b.toml +4 -1
  47. examples/sft/configs/crafter_lora_qwen0p6b.toml +4 -1
  48. examples/swe/task_app/README.md +32 -2
  49. examples/swe/task_app/grpo_swe_mini.py +4 -0
  50. examples/swe/task_app/hosted/envs/crafter/react_agent.py +1 -1
  51. examples/swe/task_app/hosted/envs/mini_swe/environment.py +37 -10
  52. examples/swe/task_app/hosted/inference/openai_client.py +4 -4
  53. examples/swe/task_app/morph_backend.py +178 -0
  54. examples/task_apps/crafter/task_app/README.md +1 -1
  55. examples/task_apps/crafter/task_app/grpo_crafter.py +66 -3
  56. examples/task_apps/crafter/task_app/grpo_crafter_task_app.py +1 -1
  57. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/policy.py +4 -26
  58. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/react_agent.py +1 -2
  59. examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +17 -49
  60. examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +13 -5
  61. examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +15 -1
  62. examples/task_apps/enron/task_app/grpo_enron_task_app.py +1 -1
  63. examples/task_apps/math/README.md +1 -2
  64. examples/task_apps/pokemon_red/README.md +3 -4
  65. examples/task_apps/pokemon_red/eval_image_only_gpt4o.toml +6 -5
  66. examples/task_apps/pokemon_red/eval_pokemon_red_policy.py +1 -2
  67. examples/task_apps/pokemon_red/task_app.py +36 -5
  68. examples/task_apps/sokoban/README.md +2 -3
  69. examples/task_apps/verilog/eval_groq_qwen32b.toml +12 -14
  70. examples/task_apps/verilog/task_app/grpo_verilog_task_app.py +1 -1
  71. examples/vlm/configs/crafter_vlm_gpt4o.toml +4 -1
  72. examples/warming_up_to_rl/configs/crafter_fft.toml +4 -1
  73. examples/warming_up_to_rl/configs/crafter_fft_4b.toml +0 -2
  74. examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +2 -2
  75. examples/warming_up_to_rl/run_local_rollout_traced.py +1 -1
  76. examples/warming_up_to_rl/task_app/README.md +1 -1
  77. examples/warming_up_to_rl/task_app/grpo_crafter.py +134 -3
  78. examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +1 -1
  79. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +3 -27
  80. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +1 -1
  81. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +4 -4
  82. examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +6 -3
  83. examples/workflows/math_rl/configs/rl_from_base_qwen.toml +27 -0
  84. examples/workflows/math_rl/configs/rl_from_base_qwen17.toml +5 -0
  85. synth_ai/api/train/builders.py +9 -3
  86. synth_ai/api/train/cli.py +125 -10
  87. synth_ai/api/train/configs/__init__.py +8 -1
  88. synth_ai/api/train/configs/rl.py +32 -7
  89. synth_ai/api/train/configs/sft.py +6 -2
  90. synth_ai/api/train/configs/shared.py +59 -2
  91. synth_ai/auth/credentials.py +119 -0
  92. synth_ai/cli/__init__.py +12 -4
  93. synth_ai/cli/commands/__init__.py +17 -0
  94. synth_ai/cli/commands/demo/__init__.py +6 -0
  95. synth_ai/cli/commands/demo/core.py +163 -0
  96. synth_ai/cli/commands/deploy/__init__.py +23 -0
  97. synth_ai/cli/commands/deploy/core.py +614 -0
  98. synth_ai/cli/commands/deploy/errors.py +72 -0
  99. synth_ai/cli/commands/deploy/validation.py +11 -0
  100. synth_ai/cli/commands/eval/__init__.py +19 -0
  101. synth_ai/cli/commands/eval/core.py +1109 -0
  102. synth_ai/cli/commands/eval/errors.py +81 -0
  103. synth_ai/cli/commands/eval/validation.py +133 -0
  104. synth_ai/cli/commands/filter/__init__.py +12 -0
  105. synth_ai/cli/commands/filter/core.py +388 -0
  106. synth_ai/cli/commands/filter/errors.py +55 -0
  107. synth_ai/cli/commands/filter/validation.py +77 -0
  108. synth_ai/cli/commands/help/__init__.py +177 -0
  109. synth_ai/cli/commands/help/core.py +73 -0
  110. synth_ai/cli/commands/status/__init__.py +64 -0
  111. synth_ai/cli/commands/status/client.py +192 -0
  112. synth_ai/cli/commands/status/config.py +92 -0
  113. synth_ai/cli/commands/status/errors.py +20 -0
  114. synth_ai/cli/commands/status/formatters.py +164 -0
  115. synth_ai/cli/commands/status/subcommands/__init__.py +9 -0
  116. synth_ai/cli/commands/status/subcommands/files.py +79 -0
  117. synth_ai/cli/commands/status/subcommands/jobs.py +334 -0
  118. synth_ai/cli/commands/status/subcommands/models.py +79 -0
  119. synth_ai/cli/commands/status/subcommands/runs.py +81 -0
  120. synth_ai/cli/commands/status/subcommands/summary.py +47 -0
  121. synth_ai/cli/commands/status/utils.py +114 -0
  122. synth_ai/cli/commands/train/__init__.py +53 -0
  123. synth_ai/cli/commands/train/core.py +21 -0
  124. synth_ai/cli/commands/train/errors.py +117 -0
  125. synth_ai/cli/commands/train/judge_schemas.py +199 -0
  126. synth_ai/cli/commands/train/judge_validation.py +304 -0
  127. synth_ai/cli/commands/train/validation.py +443 -0
  128. synth_ai/cli/demo.py +2 -162
  129. synth_ai/cli/deploy/__init__.py +28 -0
  130. synth_ai/cli/deploy/core.py +5 -0
  131. synth_ai/cli/deploy/errors.py +23 -0
  132. synth_ai/cli/deploy/validation.py +5 -0
  133. synth_ai/cli/eval/__init__.py +36 -0
  134. synth_ai/cli/eval/core.py +5 -0
  135. synth_ai/cli/eval/errors.py +31 -0
  136. synth_ai/cli/eval/validation.py +5 -0
  137. synth_ai/cli/filter/__init__.py +28 -0
  138. synth_ai/cli/filter/core.py +5 -0
  139. synth_ai/cli/filter/errors.py +23 -0
  140. synth_ai/cli/filter/validation.py +5 -0
  141. synth_ai/cli/modal_serve/__init__.py +12 -0
  142. synth_ai/cli/modal_serve/core.py +14 -0
  143. synth_ai/cli/modal_serve/errors.py +8 -0
  144. synth_ai/cli/modal_serve/validation.py +11 -0
  145. synth_ai/cli/serve/__init__.py +12 -0
  146. synth_ai/cli/serve/core.py +14 -0
  147. synth_ai/cli/serve/errors.py +8 -0
  148. synth_ai/cli/serve/validation.py +11 -0
  149. synth_ai/cli/setup.py +20 -265
  150. synth_ai/cli/status.py +7 -126
  151. synth_ai/cli/task_app_deploy.py +1 -10
  152. synth_ai/cli/task_app_modal_serve.py +4 -9
  153. synth_ai/cli/task_app_serve.py +4 -11
  154. synth_ai/cli/task_apps.py +58 -1487
  155. synth_ai/cli/train/__init__.py +12 -0
  156. synth_ai/cli/train/core.py +21 -0
  157. synth_ai/cli/train/errors.py +8 -0
  158. synth_ai/cli/train/validation.py +24 -0
  159. synth_ai/cli/train.py +1 -14
  160. synth_ai/demos/crafter/grpo_crafter_task_app.py +1 -1
  161. synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +1 -1
  162. synth_ai/environments/examples/red/engine.py +33 -12
  163. synth_ai/environments/examples/red/engine_helpers/reward_components.py +151 -179
  164. synth_ai/environments/examples/red/environment.py +26 -0
  165. synth_ai/environments/examples/red/trace_hooks_v3.py +168 -0
  166. synth_ai/http.py +12 -0
  167. synth_ai/judge_schemas.py +10 -11
  168. synth_ai/learning/rl/client.py +3 -1
  169. synth_ai/streaming/__init__.py +29 -0
  170. synth_ai/streaming/config.py +94 -0
  171. synth_ai/streaming/handlers.py +469 -0
  172. synth_ai/streaming/streamer.py +301 -0
  173. synth_ai/streaming/types.py +95 -0
  174. synth_ai/task/validators.py +2 -2
  175. synth_ai/tracing_v3/migration_helper.py +1 -2
  176. synth_ai/utils/env.py +25 -18
  177. synth_ai/utils/http.py +4 -1
  178. synth_ai/utils/modal.py +2 -2
  179. {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/METADATA +8 -3
  180. {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/RECORD +184 -109
  181. examples/qwen_vl/configs/eval_qwen2vl_vision.toml +0 -44
  182. synth_ai/cli/tui.py +0 -62
  183. synth_ai/tui/__init__.py +0 -5
  184. synth_ai/tui/__main__.py +0 -13
  185. synth_ai/tui/cli/__init__.py +0 -1
  186. synth_ai/tui/cli/query_experiments.py +0 -164
  187. synth_ai/tui/cli/query_experiments_v3.py +0 -164
  188. synth_ai/tui/dashboard.py +0 -911
  189. {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/WHEEL +0 -0
  190. {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/entry_points.txt +0 -0
  191. {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/licenses/LICENSE +0 -0
  192. {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,164 @@
1
+ """Rich-based formatting helpers for status commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import Iterable
7
+ from datetime import datetime
8
+ from typing import Any
9
+
10
+ from rich import box
11
+ from rich.console import Console
12
+ from rich.panel import Panel
13
+ from rich.table import Table
14
+ from rich.text import Text
15
+
16
+ console = Console()
17
+
18
+
19
+ def _format_timestamp(value: Any) -> str:
20
+ if value in (None, "", 0):
21
+ return ""
22
+ if isinstance(value, int | float):
23
+ try:
24
+ return datetime.fromtimestamp(float(value)).isoformat()
25
+ except Exception:
26
+ return str(value)
27
+ if isinstance(value, str):
28
+ try:
29
+ return datetime.fromisoformat(value.replace("Z", "+00:00")).strftime("%Y-%m-%d %H:%M:%S")
30
+ except Exception:
31
+ return value
32
+ return str(value)
33
+
34
+
35
+ def print_json(data: Any) -> None:
36
+ console.print_json(data=data)
37
+
38
+
39
+ def jobs_table(jobs: Iterable[dict[str, Any]]) -> Table:
40
+ table = Table(title="Training Jobs", box=box.SIMPLE, header_style="bold")
41
+ table.add_column("ID", style="cyan", overflow="fold")
42
+ table.add_column("Type", style="magenta")
43
+ table.add_column("Status")
44
+ table.add_column("Created", style="green")
45
+ table.add_column("Updated", style="green")
46
+ table.add_column("Model", style="yellow", overflow="fold")
47
+ for job in jobs:
48
+ status = job.get("status", "unknown")
49
+ status_color = {
50
+ "running": "green",
51
+ "queued": "cyan",
52
+ "succeeded": "bright_green",
53
+ "failed": "red",
54
+ "cancelled": "yellow",
55
+ }.get(status, "white")
56
+ table.add_row(
57
+ str(job.get("job_id") or job.get("id", "")),
58
+ str(job.get("training_type") or job.get("type", "")),
59
+ f"[{status_color}]{status}[/{status_color}]",
60
+ _format_timestamp(job.get("created_at")),
61
+ _format_timestamp(job.get("updated_at")),
62
+ str(job.get("model_id") or job.get("model", "")),
63
+ )
64
+ return table
65
+
66
+
67
+ def job_panel(job: dict[str, Any]) -> Panel:
68
+ lines = [f"[bold cyan]Job[/bold cyan] {job.get('job_id') or job.get('id')}"]
69
+ if job.get("name"):
70
+ lines.append(f"Name: {job['name']}")
71
+ lines.append(f"Type: {job.get('training_type', job.get('type', ''))}")
72
+ lines.append(f"Status: {job.get('status', 'unknown')}")
73
+ if job.get("model_id"):
74
+ lines.append(f"Model: {job['model_id']}")
75
+ if job.get("base_model"):
76
+ lines.append(f"Base Model: {job['base_model']}")
77
+ lines.append(f"Created: {_format_timestamp(job.get('created_at'))}")
78
+ lines.append(f"Updated: {_format_timestamp(job.get('updated_at'))}")
79
+ if config := job.get("config"):
80
+ lines.append("")
81
+ lines.append(f"[dim]{json.dumps(config, indent=2, sort_keys=True)}[/dim]")
82
+ return Panel("\n".join(lines), title="Job Details", border_style="cyan")
83
+
84
+
85
+ def runs_table(runs: Iterable[dict[str, Any]]) -> Table:
86
+ table = Table(title="Job Runs", box=box.SIMPLE, header_style="bold")
87
+ table.add_column("Run #", justify="right")
88
+ table.add_column("Engine")
89
+ table.add_column("Status")
90
+ table.add_column("Created")
91
+ table.add_column("Started")
92
+ table.add_column("Ended")
93
+ table.add_column("Duration", justify="right")
94
+ for run in runs:
95
+ table.add_row(
96
+ str(run.get("run_number") or run.get("id", "")),
97
+ str(run.get("engine", "")),
98
+ str(run.get("status", "unknown")),
99
+ _format_timestamp(run.get("created_at")),
100
+ _format_timestamp(run.get("started_at")),
101
+ _format_timestamp(run.get("ended_at")),
102
+ str(run.get("duration_seconds") or run.get("duration", "")),
103
+ )
104
+ return table
105
+
106
+
107
+ def events_panel(events: Iterable[dict[str, Any]]) -> Panel:
108
+ rendered = []
109
+ for event in events:
110
+ ts = _format_timestamp(event.get("timestamp") or event.get("created_at"))
111
+ level = event.get("level") or event.get("severity", "info")
112
+ message = event.get("message") or event.get("detail") or ""
113
+ rendered.append(f"[dim]{ts}[/dim] [{level}] {message}")
114
+ if not rendered:
115
+ rendered.append("[dim]No events found.[/dim]")
116
+ return Panel("\n".join(rendered), title="Job Events", border_style="green")
117
+
118
+
119
+ def metrics_table(metrics: dict[str, Any]) -> Table:
120
+ table = Table(title="Job Metrics", box=box.SIMPLE, header_style="bold")
121
+ table.add_column("Metric")
122
+ table.add_column("Value", justify="right")
123
+ for key, value in metrics.items():
124
+ if isinstance(value, dict):
125
+ table.add_row(key, Text(json.dumps(value), overflow="fold"))
126
+ else:
127
+ table.add_row(key, str(value))
128
+ return table
129
+
130
+
131
+ def files_table(files: Iterable[dict[str, Any]]) -> Table:
132
+ table = Table(title="Training Files", box=box.SIMPLE, header_style="bold")
133
+ table.add_column("ID", overflow="fold")
134
+ table.add_column("Purpose")
135
+ table.add_column("Size", justify="right")
136
+ table.add_column("Created")
137
+ table.add_column("Filename", overflow="fold")
138
+ for file in files:
139
+ table.add_row(
140
+ str(file.get("file_id") or file.get("id", "")),
141
+ str(file.get("purpose", "")),
142
+ str(file.get("bytes", "")),
143
+ _format_timestamp(file.get("created_at")),
144
+ str(file.get("filename", "")),
145
+ )
146
+ return table
147
+
148
+
149
+ def models_table(models: Iterable[dict[str, Any]]) -> Table:
150
+ table = Table(title="Fine-tuned Models", box=box.SIMPLE, header_style="bold")
151
+ table.add_column("ID", overflow="fold")
152
+ table.add_column("Base")
153
+ table.add_column("Created")
154
+ table.add_column("Owner")
155
+ table.add_column("Status")
156
+ for model in models:
157
+ table.add_row(
158
+ str(model.get("id", model.get("name", ""))),
159
+ str(model.get("base_model") or model.get("base", "")),
160
+ _format_timestamp(model.get("created_at")),
161
+ str(model.get("owner") or model.get("organization", "")),
162
+ str(model.get("status", "")),
163
+ )
164
+ return table
@@ -0,0 +1,9 @@
1
+ """
2
+ Subcommands for the status CLI namespace.
3
+ """
4
+
5
+ from .files import files_group # noqa: F401
6
+ from .jobs import jobs_group # noqa: F401
7
+ from .models import models_group # noqa: F401
8
+ from .runs import runs_group # noqa: F401
9
+ from .summary import summary_command # noqa: F401
@@ -0,0 +1,79 @@
1
+ """`synth files` command group."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+
7
+ import click
8
+ from rich.json import JSON
9
+
10
+ from ..client import StatusAPIClient
11
+ from ..errors import StatusAPIError
12
+ from ..formatters import console, files_table, print_json
13
+ from ..utils import bail, common_options, resolve_context_config
14
+
15
+
16
+ @click.group("files", help="Manage training files.")
17
+ @click.pass_context
18
+ def files_group(ctx: click.Context) -> None: # pragma: no cover - Click wiring
19
+ ctx.ensure_object(dict)
20
+
21
+
22
+ @files_group.command("list")
23
+ @common_options()
24
+ @click.option("--purpose", type=click.Choice(["fine-tune", "validation"]))
25
+ @click.option("--limit", type=int, default=20, show_default=True)
26
+ @click.option("--json", "output_json", is_flag=True)
27
+ @click.pass_context
28
+ def list_files(
29
+ ctx: click.Context,
30
+ base_url: str | None,
31
+ api_key: str | None,
32
+ timeout: float,
33
+ purpose: str | None,
34
+ limit: int,
35
+ output_json: bool,
36
+ ) -> None:
37
+ cfg = resolve_context_config(ctx, base_url=base_url, api_key=api_key, timeout=timeout)
38
+
39
+ async def _run() -> None:
40
+ try:
41
+ async with StatusAPIClient(cfg) as client:
42
+ files = await client.list_files(purpose=purpose, limit=limit)
43
+ if output_json:
44
+ print_json(files)
45
+ else:
46
+ console.print(files_table(files))
47
+ except StatusAPIError as exc:
48
+ bail(f"Backend error: {exc}")
49
+
50
+ asyncio.run(_run())
51
+
52
+
53
+ @files_group.command("get")
54
+ @common_options()
55
+ @click.argument("file_id")
56
+ @click.option("--json", "output_json", is_flag=True)
57
+ @click.pass_context
58
+ def get_file(
59
+ ctx: click.Context,
60
+ base_url: str | None,
61
+ api_key: str | None,
62
+ timeout: float,
63
+ file_id: str,
64
+ output_json: bool,
65
+ ) -> None:
66
+ cfg = resolve_context_config(ctx, base_url=base_url, api_key=api_key, timeout=timeout)
67
+
68
+ async def _run() -> None:
69
+ try:
70
+ async with StatusAPIClient(cfg) as client:
71
+ file_info = await client.get_file(file_id)
72
+ if output_json:
73
+ print_json(file_info)
74
+ else:
75
+ console.print(JSON.from_data(file_info))
76
+ except StatusAPIError as exc:
77
+ bail(f"Backend error: {exc}")
78
+
79
+ asyncio.run(_run())
@@ -0,0 +1,334 @@
1
+ """`synth jobs` command group implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import Any
7
+
8
+ import click
9
+
10
+ from ..client import StatusAPIClient
11
+ from ..errors import StatusAPIError
12
+ from ..formatters import (
13
+ console,
14
+ events_panel,
15
+ job_panel,
16
+ jobs_table,
17
+ metrics_table,
18
+ print_json,
19
+ runs_table,
20
+ )
21
+ from ..utils import bail, common_options, parse_relative_time, resolve_context_config
22
+
23
+
24
+ @click.group("jobs", help="Manage training jobs.")
25
+ @click.pass_context
26
+ def jobs_group(ctx: click.Context) -> None: # pragma: no cover - Click wiring
27
+ ctx.ensure_object(dict)
28
+
29
+
30
+ def _print_or_json(items: Any, output_json: bool) -> None:
31
+ if output_json:
32
+ print_json(items)
33
+ elif isinstance(items, list):
34
+ console.print(jobs_table(items))
35
+ else:
36
+ console.print(job_panel(items))
37
+
38
+
39
+ @jobs_group.command("list")
40
+ @common_options()
41
+ @click.option(
42
+ "--status",
43
+ type=click.Choice(["queued", "running", "succeeded", "failed", "cancelled"]),
44
+ help="Filter by job status.",
45
+ )
46
+ @click.option(
47
+ "--type",
48
+ "job_type",
49
+ type=click.Choice(["sft_offline", "sft_online", "rl_online", "dpo", "sft"]),
50
+ help="Filter by training job type.",
51
+ )
52
+ @click.option("--created-after", help="Filter by creation date (ISO8601 or relative like '24h').")
53
+ @click.option("--limit", default=20, show_default=True, type=int)
54
+ @click.option("--json", "output_json", is_flag=True, help="Emit raw JSON.")
55
+ @click.pass_context
56
+ def list_jobs(
57
+ ctx: click.Context,
58
+ base_url: str | None,
59
+ api_key: str | None,
60
+ timeout: float,
61
+ status: str | None,
62
+ job_type: str | None,
63
+ created_after: str | None,
64
+ limit: int,
65
+ output_json: bool,
66
+ ) -> None:
67
+ cfg = resolve_context_config(ctx, base_url=base_url, api_key=api_key, timeout=timeout)
68
+ created_filter = parse_relative_time(created_after)
69
+
70
+ async def _run() -> None:
71
+ try:
72
+ async with StatusAPIClient(cfg) as client:
73
+ jobs = await client.list_jobs(
74
+ status=status,
75
+ job_type=job_type,
76
+ created_after=created_filter,
77
+ limit=limit,
78
+ )
79
+ _print_or_json(jobs, output_json)
80
+ except StatusAPIError as exc:
81
+ bail(f"Backend error: {exc}")
82
+
83
+ asyncio.run(_run())
84
+
85
+
86
+ @jobs_group.command("get")
87
+ @common_options()
88
+ @click.argument("job_id")
89
+ @click.option("--json", "output_json", is_flag=True)
90
+ @click.pass_context
91
+ def get_job(
92
+ ctx: click.Context,
93
+ base_url: str | None,
94
+ api_key: str | None,
95
+ timeout: float,
96
+ job_id: str,
97
+ output_json: bool,
98
+ ) -> None:
99
+ cfg = resolve_context_config(ctx, base_url=base_url, api_key=api_key, timeout=timeout)
100
+
101
+ async def _run() -> None:
102
+ try:
103
+ async with StatusAPIClient(cfg) as client:
104
+ job = await client.get_job(job_id)
105
+ _print_or_json(job, output_json)
106
+ except StatusAPIError as exc:
107
+ bail(f"Backend error: {exc}")
108
+
109
+ asyncio.run(_run())
110
+
111
+
112
+ @jobs_group.command("history")
113
+ @common_options()
114
+ @click.argument("job_id")
115
+ @click.option("--json", "output_json", is_flag=True)
116
+ @click.pass_context
117
+ def job_history(
118
+ ctx: click.Context,
119
+ base_url: str | None,
120
+ api_key: str | None,
121
+ timeout: float,
122
+ job_id: str,
123
+ output_json: bool,
124
+ ) -> None:
125
+ cfg = resolve_context_config(ctx, base_url=base_url, api_key=api_key, timeout=timeout)
126
+
127
+ async def _run() -> None:
128
+ try:
129
+ async with StatusAPIClient(cfg) as client:
130
+ runs = await client.list_job_runs(job_id)
131
+ if output_json:
132
+ print_json(runs)
133
+ else:
134
+ console.print(runs_table(runs))
135
+ except StatusAPIError as exc:
136
+ bail(f"Backend error: {exc}")
137
+
138
+ asyncio.run(_run())
139
+
140
+
141
+ @jobs_group.command("timeline")
142
+ @common_options()
143
+ @click.argument("job_id")
144
+ @click.option("--json", "output_json", is_flag=True)
145
+ @click.pass_context
146
+ def job_timeline(
147
+ ctx: click.Context,
148
+ base_url: str | None,
149
+ api_key: str | None,
150
+ timeout: float,
151
+ job_id: str,
152
+ output_json: bool,
153
+ ) -> None:
154
+ cfg = resolve_context_config(ctx, base_url=base_url, api_key=api_key, timeout=timeout)
155
+
156
+ async def _run() -> None:
157
+ try:
158
+ async with StatusAPIClient(cfg) as client:
159
+ timeline = await client.get_job_timeline(job_id)
160
+ if output_json:
161
+ print_json(timeline)
162
+ else:
163
+ console.print(events_panel(timeline))
164
+ except StatusAPIError as exc:
165
+ bail(f"Backend error: {exc}")
166
+
167
+ asyncio.run(_run())
168
+
169
+
170
+ @jobs_group.command("metrics")
171
+ @common_options()
172
+ @click.argument("job_id")
173
+ @click.option("--json", "output_json", is_flag=True)
174
+ @click.pass_context
175
+ def job_metrics(
176
+ ctx: click.Context,
177
+ base_url: str | None,
178
+ api_key: str | None,
179
+ timeout: float,
180
+ job_id: str,
181
+ output_json: bool,
182
+ ) -> None:
183
+ cfg = resolve_context_config(ctx, base_url=base_url, api_key=api_key, timeout=timeout)
184
+
185
+ async def _run() -> None:
186
+ try:
187
+ async with StatusAPIClient(cfg) as client:
188
+ metrics = await client.get_job_metrics(job_id)
189
+ if output_json:
190
+ print_json(metrics)
191
+ else:
192
+ console.print(metrics_table(metrics))
193
+ except StatusAPIError as exc:
194
+ bail(f"Backend error: {exc}")
195
+
196
+ asyncio.run(_run())
197
+
198
+
199
+ @jobs_group.command("config")
200
+ @common_options()
201
+ @click.argument("job_id")
202
+ @click.option("--json", "output_json", is_flag=True)
203
+ @click.pass_context
204
+ def job_config(
205
+ ctx: click.Context,
206
+ base_url: str | None,
207
+ api_key: str | None,
208
+ timeout: float,
209
+ job_id: str,
210
+ output_json: bool,
211
+ ) -> None:
212
+ cfg = resolve_context_config(ctx, base_url=base_url, api_key=api_key, timeout=timeout)
213
+
214
+ async def _run() -> None:
215
+ try:
216
+ async with StatusAPIClient(cfg) as client:
217
+ config = await client.get_job_config(job_id)
218
+ if output_json:
219
+ print_json(config)
220
+ else:
221
+ console.print(job_panel({"job_id": job_id, "config": config}))
222
+ except StatusAPIError as exc:
223
+ bail(f"Backend error: {exc}")
224
+
225
+ asyncio.run(_run())
226
+
227
+
228
+ @jobs_group.command("status")
229
+ @common_options()
230
+ @click.argument("job_id")
231
+ @click.option("--json", "output_json", is_flag=True)
232
+ @click.pass_context
233
+ def job_status(
234
+ ctx: click.Context,
235
+ base_url: str | None,
236
+ api_key: str | None,
237
+ timeout: float,
238
+ job_id: str,
239
+ output_json: bool,
240
+ ) -> None:
241
+ cfg = resolve_context_config(ctx, base_url=base_url, api_key=api_key, timeout=timeout)
242
+
243
+ async def _run() -> None:
244
+ try:
245
+ async with StatusAPIClient(cfg) as client:
246
+ status = await client.get_job_status(job_id)
247
+ if output_json:
248
+ print_json(status)
249
+ else:
250
+ console.print(f"[bold]{job_id}[/bold]: {status.get('status', 'unknown')}")
251
+ except StatusAPIError as exc:
252
+ bail(f"Backend error: {exc}")
253
+
254
+ asyncio.run(_run())
255
+
256
+
257
+ @jobs_group.command("cancel")
258
+ @common_options()
259
+ @click.argument("job_id")
260
+ @click.pass_context
261
+ def cancel_job(
262
+ ctx: click.Context,
263
+ base_url: str | None,
264
+ api_key: str | None,
265
+ timeout: float,
266
+ job_id: str,
267
+ ) -> None:
268
+ cfg = resolve_context_config(ctx, base_url=base_url, api_key=api_key, timeout=timeout)
269
+
270
+ async def _run() -> None:
271
+ try:
272
+ async with StatusAPIClient(cfg) as client:
273
+ resp = await client.cancel_job(job_id)
274
+ console.print(resp.get("message") or f"[yellow]Cancellation requested for {job_id}[/yellow]")
275
+ except StatusAPIError as exc:
276
+ bail(f"Backend error: {exc}")
277
+
278
+ asyncio.run(_run())
279
+
280
+
281
+ @jobs_group.command("logs")
282
+ @common_options()
283
+ @click.argument("job_id")
284
+ @click.option("--since", help="Only show events emitted after the provided timestamp/relative offset.")
285
+ @click.option("--tail", type=int, help="Show only the last N events.")
286
+ @click.option("--follow/--no-follow", default=False, help="Poll for new events.")
287
+ @click.option("--json", "output_json", is_flag=True)
288
+ @click.pass_context
289
+ def job_logs(
290
+ ctx: click.Context,
291
+ base_url: str | None,
292
+ api_key: str | None,
293
+ timeout: float,
294
+ job_id: str,
295
+ since: str | None,
296
+ tail: int | None,
297
+ follow: bool,
298
+ output_json: bool,
299
+ ) -> None:
300
+ cfg = resolve_context_config(ctx, base_url=base_url, api_key=api_key, timeout=timeout)
301
+ since_filter = parse_relative_time(since)
302
+
303
+ async def _loop() -> None:
304
+ seen_ids: set[str] = set()
305
+ cursor: str | None = None
306
+ try:
307
+ async with StatusAPIClient(cfg) as client:
308
+ while True:
309
+ events = await client.get_job_events(
310
+ job_id,
311
+ since=cursor or since_filter,
312
+ limit=tail,
313
+ after=cursor,
314
+ )
315
+ new_events: list[dict[str, Any]] = []
316
+ for event in events:
317
+ event_id = str(event.get("event_id") or event.get("id") or event.get("timestamp"))
318
+ if event_id in seen_ids:
319
+ continue
320
+ seen_ids.add(event_id)
321
+ new_events.append(event)
322
+ if new_events:
323
+ cursor = str(new_events[-1].get("event_id") or new_events[-1].get("id") or "")
324
+ if output_json:
325
+ print_json(new_events)
326
+ else:
327
+ console.print(events_panel(new_events))
328
+ if not follow:
329
+ break
330
+ await asyncio.sleep(2.0)
331
+ except StatusAPIError as exc:
332
+ bail(f"Backend error: {exc}")
333
+
334
+ asyncio.run(_loop())
@@ -0,0 +1,79 @@
1
+ """`synth models` command group."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+
7
+ import click
8
+ from rich.json import JSON
9
+
10
+ from ..client import StatusAPIClient
11
+ from ..errors import StatusAPIError
12
+ from ..formatters import console, models_table, print_json
13
+ from ..utils import bail, common_options, resolve_context_config
14
+
15
+
16
+ @click.group("models", help="Inspect fine-tuned models.")
17
+ @click.pass_context
18
+ def models_group(ctx: click.Context) -> None: # pragma: no cover - Click wiring
19
+ ctx.ensure_object(dict)
20
+
21
+
22
+ @models_group.command("list")
23
+ @common_options()
24
+ @click.option("--limit", type=int, default=None, help="Maximum number of models to return.")
25
+ @click.option("--type", "model_type", type=click.Choice(["rl", "sft"]), default=None, help="Filter by model type.")
26
+ @click.option("--json", "output_json", is_flag=True)
27
+ @click.pass_context
28
+ def list_models(
29
+ ctx: click.Context,
30
+ base_url: str | None,
31
+ api_key: str | None,
32
+ timeout: float,
33
+ limit: int | None,
34
+ model_type: str | None,
35
+ output_json: bool,
36
+ ) -> None:
37
+ cfg = resolve_context_config(ctx, base_url=base_url, api_key=api_key, timeout=timeout)
38
+
39
+ async def _run() -> None:
40
+ try:
41
+ async with StatusAPIClient(cfg) as client:
42
+ models = await client.list_models(limit=limit, model_type=model_type)
43
+ if output_json:
44
+ print_json(models)
45
+ else:
46
+ console.print(models_table(models))
47
+ except StatusAPIError as exc:
48
+ bail(f"Backend error: {exc}")
49
+
50
+ asyncio.run(_run())
51
+
52
+
53
+ @models_group.command("get")
54
+ @common_options()
55
+ @click.argument("model_id")
56
+ @click.option("--json", "output_json", is_flag=True)
57
+ @click.pass_context
58
+ def get_model(
59
+ ctx: click.Context,
60
+ base_url: str | None,
61
+ api_key: str | None,
62
+ timeout: float,
63
+ model_id: str,
64
+ output_json: bool,
65
+ ) -> None:
66
+ cfg = resolve_context_config(ctx, base_url=base_url, api_key=api_key, timeout=timeout)
67
+
68
+ async def _run() -> None:
69
+ try:
70
+ async with StatusAPIClient(cfg) as client:
71
+ model = await client.get_model(model_id)
72
+ if output_json:
73
+ print_json(model)
74
+ else:
75
+ console.print(JSON.from_data(model))
76
+ except StatusAPIError as exc:
77
+ bail(f"Backend error: {exc}")
78
+
79
+ asyncio.run(_run())