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.
- examples/analyze_semantic_words.sh +2 -2
- examples/blog_posts/pokemon_vl/README.md +98 -0
- examples/blog_posts/pokemon_vl/configs/eval_qwen3_vl.toml +25 -0
- examples/blog_posts/pokemon_vl/configs/eval_rl_final.toml +24 -0
- examples/blog_posts/pokemon_vl/configs/filter_high_reward.toml +10 -0
- examples/blog_posts/pokemon_vl/configs/train_rl_from_sft.toml +42 -0
- examples/blog_posts/pokemon_vl/configs/train_sft_qwen4b_vl.toml +40 -0
- examples/blog_posts/warming_up_to_rl/README.md +158 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b.toml +25 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_groq_qwen32b.toml +25 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_openai_gpt_oss_120b.toml +29 -0
- examples/blog_posts/warming_up_to_rl/configs/filter_high_reward_dataset.toml +10 -0
- examples/blog_posts/warming_up_to_rl/configs/train_rl_from_sft.toml +41 -0
- examples/blog_posts/warming_up_to_rl/configs/train_sft_qwen4b.toml +40 -0
- examples/dev/qwen3_32b_qlora_4xh100.toml +5 -0
- examples/multi_step/configs/crafter_rl_outcome.toml +1 -1
- examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +65 -107
- examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +1 -1
- examples/multi_step/configs/crafter_rl_stepwise_simple.toml +1 -1
- examples/multi_step/configs/crafter_rl_stepwise_simple_NEW_FORMAT.toml +105 -0
- examples/multi_step/configs/verilog_rl_lora.toml +80 -123
- examples/qwen_coder/configs/coder_lora_30b.toml +1 -3
- examples/qwen_coder/configs/coder_lora_4b.toml +4 -1
- examples/qwen_coder/configs/coder_lora_small.toml +1 -3
- examples/qwen_vl/README.md +10 -12
- examples/qwen_vl/SETUP_COMPLETE.md +7 -8
- examples/qwen_vl/VISION_TESTS_COMPLETE.md +2 -3
- examples/qwen_vl/collect_data_via_cli.md +76 -84
- examples/qwen_vl/collect_vision_traces.py +4 -4
- examples/qwen_vl/configs/crafter_rl_vision_qwen3vl4b.toml +40 -57
- examples/qwen_vl/configs/crafter_vlm_sft_example.toml +1 -2
- examples/qwen_vl/configs/eval_gpt4o_mini_vision.toml +20 -37
- examples/qwen_vl/configs/eval_gpt5nano_vision.toml +21 -40
- examples/qwen_vl/configs/eval_qwen3vl_vision.toml +26 -0
- examples/qwen_vl/configs/{filter_qwen2vl_sft.toml → filter_qwen3vl_sft.toml} +4 -5
- examples/qwen_vl/configs/filter_vision_sft.toml +2 -3
- examples/qwen_vl/crafter_qwen_vl_agent.py +5 -5
- examples/qwen_vl/run_vision_comparison.sh +6 -7
- examples/rl/README.md +5 -5
- examples/rl/configs/rl_from_base_qwen.toml +26 -1
- examples/rl/configs/rl_from_base_qwen17.toml +5 -2
- examples/rl/task_app/README.md +1 -2
- examples/rl/task_app/math_single_step.py +2 -2
- examples/run_crafter_demo.sh +2 -2
- examples/sft/README.md +1 -1
- examples/sft/configs/crafter_fft_qwen0p6b.toml +4 -1
- examples/sft/configs/crafter_lora_qwen0p6b.toml +4 -1
- examples/swe/task_app/README.md +32 -2
- examples/swe/task_app/grpo_swe_mini.py +4 -0
- examples/swe/task_app/hosted/envs/crafter/react_agent.py +1 -1
- examples/swe/task_app/hosted/envs/mini_swe/environment.py +37 -10
- examples/swe/task_app/hosted/inference/openai_client.py +4 -4
- examples/swe/task_app/morph_backend.py +178 -0
- examples/task_apps/crafter/task_app/README.md +1 -1
- examples/task_apps/crafter/task_app/grpo_crafter.py +66 -3
- examples/task_apps/crafter/task_app/grpo_crafter_task_app.py +1 -1
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/policy.py +4 -26
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/react_agent.py +1 -2
- examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +17 -49
- examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +13 -5
- examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +15 -1
- examples/task_apps/enron/task_app/grpo_enron_task_app.py +1 -1
- examples/task_apps/math/README.md +1 -2
- examples/task_apps/pokemon_red/README.md +3 -4
- examples/task_apps/pokemon_red/eval_image_only_gpt4o.toml +6 -5
- examples/task_apps/pokemon_red/eval_pokemon_red_policy.py +1 -2
- examples/task_apps/pokemon_red/task_app.py +36 -5
- examples/task_apps/sokoban/README.md +2 -3
- examples/task_apps/verilog/eval_groq_qwen32b.toml +12 -14
- examples/task_apps/verilog/task_app/grpo_verilog_task_app.py +1 -1
- examples/vlm/configs/crafter_vlm_gpt4o.toml +4 -1
- examples/warming_up_to_rl/configs/crafter_fft.toml +4 -1
- examples/warming_up_to_rl/configs/crafter_fft_4b.toml +0 -2
- examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +2 -2
- examples/warming_up_to_rl/run_local_rollout_traced.py +1 -1
- examples/warming_up_to_rl/task_app/README.md +1 -1
- examples/warming_up_to_rl/task_app/grpo_crafter.py +134 -3
- examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +1 -1
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +3 -27
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +1 -1
- examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +4 -4
- examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +6 -3
- examples/workflows/math_rl/configs/rl_from_base_qwen.toml +27 -0
- examples/workflows/math_rl/configs/rl_from_base_qwen17.toml +5 -0
- synth_ai/api/train/builders.py +9 -3
- synth_ai/api/train/cli.py +125 -10
- synth_ai/api/train/configs/__init__.py +8 -1
- synth_ai/api/train/configs/rl.py +32 -7
- synth_ai/api/train/configs/sft.py +6 -2
- synth_ai/api/train/configs/shared.py +59 -2
- synth_ai/auth/credentials.py +119 -0
- synth_ai/cli/__init__.py +12 -4
- synth_ai/cli/commands/__init__.py +17 -0
- synth_ai/cli/commands/demo/__init__.py +6 -0
- synth_ai/cli/commands/demo/core.py +163 -0
- synth_ai/cli/commands/deploy/__init__.py +23 -0
- synth_ai/cli/commands/deploy/core.py +614 -0
- synth_ai/cli/commands/deploy/errors.py +72 -0
- synth_ai/cli/commands/deploy/validation.py +11 -0
- synth_ai/cli/commands/eval/__init__.py +19 -0
- synth_ai/cli/commands/eval/core.py +1109 -0
- synth_ai/cli/commands/eval/errors.py +81 -0
- synth_ai/cli/commands/eval/validation.py +133 -0
- synth_ai/cli/commands/filter/__init__.py +12 -0
- synth_ai/cli/commands/filter/core.py +388 -0
- synth_ai/cli/commands/filter/errors.py +55 -0
- synth_ai/cli/commands/filter/validation.py +77 -0
- synth_ai/cli/commands/help/__init__.py +177 -0
- synth_ai/cli/commands/help/core.py +73 -0
- synth_ai/cli/commands/status/__init__.py +64 -0
- synth_ai/cli/commands/status/client.py +192 -0
- synth_ai/cli/commands/status/config.py +92 -0
- synth_ai/cli/commands/status/errors.py +20 -0
- synth_ai/cli/commands/status/formatters.py +164 -0
- synth_ai/cli/commands/status/subcommands/__init__.py +9 -0
- synth_ai/cli/commands/status/subcommands/files.py +79 -0
- synth_ai/cli/commands/status/subcommands/jobs.py +334 -0
- synth_ai/cli/commands/status/subcommands/models.py +79 -0
- synth_ai/cli/commands/status/subcommands/runs.py +81 -0
- synth_ai/cli/commands/status/subcommands/summary.py +47 -0
- synth_ai/cli/commands/status/utils.py +114 -0
- synth_ai/cli/commands/train/__init__.py +53 -0
- synth_ai/cli/commands/train/core.py +21 -0
- synth_ai/cli/commands/train/errors.py +117 -0
- synth_ai/cli/commands/train/judge_schemas.py +199 -0
- synth_ai/cli/commands/train/judge_validation.py +304 -0
- synth_ai/cli/commands/train/validation.py +443 -0
- synth_ai/cli/demo.py +2 -162
- synth_ai/cli/deploy/__init__.py +28 -0
- synth_ai/cli/deploy/core.py +5 -0
- synth_ai/cli/deploy/errors.py +23 -0
- synth_ai/cli/deploy/validation.py +5 -0
- synth_ai/cli/eval/__init__.py +36 -0
- synth_ai/cli/eval/core.py +5 -0
- synth_ai/cli/eval/errors.py +31 -0
- synth_ai/cli/eval/validation.py +5 -0
- synth_ai/cli/filter/__init__.py +28 -0
- synth_ai/cli/filter/core.py +5 -0
- synth_ai/cli/filter/errors.py +23 -0
- synth_ai/cli/filter/validation.py +5 -0
- synth_ai/cli/modal_serve/__init__.py +12 -0
- synth_ai/cli/modal_serve/core.py +14 -0
- synth_ai/cli/modal_serve/errors.py +8 -0
- synth_ai/cli/modal_serve/validation.py +11 -0
- synth_ai/cli/serve/__init__.py +12 -0
- synth_ai/cli/serve/core.py +14 -0
- synth_ai/cli/serve/errors.py +8 -0
- synth_ai/cli/serve/validation.py +11 -0
- synth_ai/cli/setup.py +20 -265
- synth_ai/cli/status.py +7 -126
- synth_ai/cli/task_app_deploy.py +1 -10
- synth_ai/cli/task_app_modal_serve.py +4 -9
- synth_ai/cli/task_app_serve.py +4 -11
- synth_ai/cli/task_apps.py +58 -1487
- synth_ai/cli/train/__init__.py +12 -0
- synth_ai/cli/train/core.py +21 -0
- synth_ai/cli/train/errors.py +8 -0
- synth_ai/cli/train/validation.py +24 -0
- synth_ai/cli/train.py +1 -14
- synth_ai/demos/crafter/grpo_crafter_task_app.py +1 -1
- synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +1 -1
- synth_ai/environments/examples/red/engine.py +33 -12
- synth_ai/environments/examples/red/engine_helpers/reward_components.py +151 -179
- synth_ai/environments/examples/red/environment.py +26 -0
- synth_ai/environments/examples/red/trace_hooks_v3.py +168 -0
- synth_ai/http.py +12 -0
- synth_ai/judge_schemas.py +10 -11
- synth_ai/learning/rl/client.py +3 -1
- synth_ai/streaming/__init__.py +29 -0
- synth_ai/streaming/config.py +94 -0
- synth_ai/streaming/handlers.py +469 -0
- synth_ai/streaming/streamer.py +301 -0
- synth_ai/streaming/types.py +95 -0
- synth_ai/task/validators.py +2 -2
- synth_ai/tracing_v3/migration_helper.py +1 -2
- synth_ai/utils/env.py +25 -18
- synth_ai/utils/http.py +4 -1
- synth_ai/utils/modal.py +2 -2
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/METADATA +8 -3
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/RECORD +184 -109
- examples/qwen_vl/configs/eval_qwen2vl_vision.toml +0 -44
- synth_ai/cli/tui.py +0 -62
- synth_ai/tui/__init__.py +0 -5
- synth_ai/tui/__main__.py +0 -13
- synth_ai/tui/cli/__init__.py +0 -1
- synth_ai/tui/cli/query_experiments.py +0 -164
- synth_ai/tui/cli/query_experiments_v3.py +0 -164
- synth_ai/tui/dashboard.py +0 -911
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/licenses/LICENSE +0 -0
- {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())
|