synth-ai 0.2.16__py3-none-any.whl → 0.2.19__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/baseline/banking77_baseline.py +204 -0
- examples/baseline/crafter_baseline.py +407 -0
- examples/baseline/pokemon_red_baseline.py +326 -0
- examples/baseline/simple_baseline.py +56 -0
- examples/baseline/warming_up_to_rl_baseline.py +239 -0
- examples/blog_posts/gepa/README.md +355 -0
- examples/blog_posts/gepa/configs/banking77_gepa_local.toml +95 -0
- examples/blog_posts/gepa/configs/banking77_gepa_test.toml +82 -0
- examples/blog_posts/gepa/configs/banking77_mipro_local.toml +52 -0
- examples/blog_posts/gepa/configs/hotpotqa_gepa_local.toml +59 -0
- examples/blog_posts/gepa/configs/hotpotqa_gepa_qwen.toml +36 -0
- examples/blog_posts/gepa/configs/hotpotqa_mipro_local.toml +53 -0
- examples/blog_posts/gepa/configs/hover_gepa_local.toml +59 -0
- examples/blog_posts/gepa/configs/hover_gepa_qwen.toml +36 -0
- examples/blog_posts/gepa/configs/hover_mipro_local.toml +53 -0
- examples/blog_posts/gepa/configs/ifbench_gepa_local.toml +59 -0
- examples/blog_posts/gepa/configs/ifbench_gepa_qwen.toml +36 -0
- examples/blog_posts/gepa/configs/ifbench_mipro_local.toml +53 -0
- examples/blog_posts/gepa/configs/pupa_gepa_local.toml +60 -0
- examples/blog_posts/gepa/configs/pupa_mipro_local.toml +54 -0
- examples/blog_posts/gepa/deploy_banking77_task_app.sh +41 -0
- examples/blog_posts/gepa/gepa_baseline.py +204 -0
- examples/blog_posts/gepa/query_prompts_example.py +97 -0
- examples/blog_posts/gepa/run_gepa_banking77.sh +87 -0
- examples/blog_posts/gepa/task_apps.py +105 -0
- examples/blog_posts/gepa/test_gepa_local.sh +67 -0
- examples/blog_posts/gepa/verify_banking77_setup.sh +123 -0
- examples/blog_posts/pokemon_vl/README.md +98 -0
- examples/blog_posts/pokemon_vl/configs/eval_gpt5nano.toml +26 -0
- examples/blog_posts/pokemon_vl/configs/eval_qwen3_vl.toml +27 -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 +43 -0
- examples/blog_posts/pokemon_vl/configs/train_sft_qwen4b_vl.toml +40 -0
- examples/blog_posts/pokemon_vl/extract_images.py +239 -0
- examples/blog_posts/pokemon_vl/pokemon_vl_baseline.py +326 -0
- examples/blog_posts/pokemon_vl/run_eval_extract_images.py +209 -0
- examples/blog_posts/pokemon_vl/run_qwen_eval_extract_images.py +212 -0
- examples/blog_posts/pokemon_vl/text_box_analysis.md +106 -0
- examples/blog_posts/warming_up_to_rl/ARCHITECTURE.md +195 -0
- examples/blog_posts/warming_up_to_rl/FINAL_TEST_RESULTS.md +127 -0
- examples/blog_posts/warming_up_to_rl/INFERENCE_SUCCESS.md +132 -0
- examples/blog_posts/warming_up_to_rl/README.md +158 -0
- examples/blog_posts/warming_up_to_rl/SMOKE_TESTING.md +164 -0
- examples/blog_posts/warming_up_to_rl/SMOKE_TEST_COMPLETE.md +253 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_baseline_qwen32b_10x20.toml +25 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b.toml +25 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b_10x20.toml +26 -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/smoke_test.toml +75 -0
- examples/blog_posts/warming_up_to_rl/configs/train_rl_from_sft.toml +91 -0
- examples/blog_posts/warming_up_to_rl/configs/train_sft_qwen4b.toml +40 -0
- examples/blog_posts/warming_up_to_rl/warming_up_to_rl_baseline.py +187 -0
- examples/dev/qwen3_32b_qlora_4xh100.toml +5 -0
- examples/multi_step/configs/VERILOG_REWARDS.md +4 -0
- examples/multi_step/configs/VERILOG_RL_CHECKLIST.md +4 -0
- examples/multi_step/configs/crafter_rl_outcome.toml +2 -1
- examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +65 -107
- examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +2 -1
- examples/multi_step/configs/crafter_rl_stepwise_simple.toml +2 -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 +6 -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 -38
- examples/swe/task_app/hosted/policy_routes.py +17 -0
- examples/swe/task_app/hosted/rollout.py +4 -2
- examples/swe/task_app/morph_backend.py +178 -0
- examples/task_apps/banking77/__init__.py +6 -0
- examples/task_apps/banking77/banking77_task_app.py +841 -0
- examples/task_apps/banking77/deploy_wrapper.py +46 -0
- examples/task_apps/crafter/CREATE_SFT_DATASET.md +4 -0
- examples/task_apps/crafter/FILTER_COMMAND_STATUS.md +4 -0
- examples/task_apps/crafter/FILTER_COMMAND_SUCCESS.md +4 -0
- examples/task_apps/crafter/task_app/README.md +1 -1
- examples/task_apps/crafter/task_app/grpo_crafter.py +90 -5
- 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/hosted_app.py +49 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +372 -107
- examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +81 -12
- examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +82 -11
- examples/task_apps/crafter/task_app/synth_envs_hosted/utils.py +194 -1
- examples/task_apps/enron/task_app/grpo_enron_task_app.py +1 -1
- examples/task_apps/gepa_benchmarks/__init__.py +7 -0
- examples/task_apps/gepa_benchmarks/common.py +260 -0
- examples/task_apps/gepa_benchmarks/hotpotqa_task_app.py +507 -0
- examples/task_apps/gepa_benchmarks/hover_task_app.py +436 -0
- examples/task_apps/gepa_benchmarks/ifbench_task_app.py +563 -0
- examples/task_apps/gepa_benchmarks/pupa_task_app.py +460 -0
- examples/task_apps/math/README.md +1 -2
- examples/task_apps/pokemon_red/README.md +3 -4
- examples/task_apps/pokemon_red/README_IMAGE_ONLY_EVAL.md +4 -0
- 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 +288 -39
- 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 +3 -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 +185 -5
- 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/hosted_app.py +49 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +156 -45
- examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +37 -4
- examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +33 -3
- examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +67 -0
- examples/workflows/math_rl/configs/rl_from_base_qwen.toml +27 -0
- examples/workflows/math_rl/configs/rl_from_base_qwen17.toml +6 -0
- synth_ai/api/train/builders.py +99 -4
- synth_ai/api/train/cli.py +516 -26
- synth_ai/api/train/config_finder.py +13 -2
- synth_ai/api/train/configs/__init__.py +23 -2
- synth_ai/api/train/configs/prompt_learning.py +442 -0
- synth_ai/api/train/configs/rl.py +61 -7
- synth_ai/api/train/configs/sft.py +6 -2
- synth_ai/api/train/configs/shared.py +59 -2
- synth_ai/api/train/task_app.py +1 -1
- synth_ai/api/train/validators.py +277 -0
- synth_ai/auth/credentials.py +119 -0
- synth_ai/baseline/__init__.py +25 -0
- synth_ai/baseline/config.py +209 -0
- synth_ai/baseline/discovery.py +214 -0
- synth_ai/baseline/execution.py +146 -0
- synth_ai/cli/__init__.py +94 -18
- synth_ai/cli/__main__.py +0 -0
- synth_ai/cli/claude.py +70 -0
- synth_ai/cli/codex.py +84 -0
- synth_ai/cli/commands/__init__.py +18 -0
- synth_ai/cli/commands/baseline/__init__.py +12 -0
- synth_ai/cli/commands/baseline/core.py +637 -0
- synth_ai/cli/commands/baseline/list.py +93 -0
- synth_ai/cli/commands/demo/__init__.py +6 -0
- synth_ai/cli/commands/demo/core.py +163 -0
- synth_ai/cli/commands/eval/__init__.py +19 -0
- synth_ai/cli/commands/eval/core.py +1112 -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 +424 -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 +72 -0
- synth_ai/cli/commands/smoke/__init__.py +7 -0
- synth_ai/cli/commands/smoke/core.py +1436 -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/pricing.py +22 -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/subcommands/usage.py +203 -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 +200 -0
- synth_ai/cli/commands/train/judge_validation.py +305 -0
- synth_ai/cli/commands/train/validation.py +386 -0
- synth_ai/cli/demo.py +30 -158
- synth_ai/cli/deploy/__init__.py +43 -0
- synth_ai/cli/deploy.py +162 -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/legacy_root_backup.py +14 -8
- 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/opencode.py +107 -0
- synth_ai/cli/root.py +9 -5
- 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 +51 -1480
- 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/crafter_classic/engine_deterministic_patch.py +7 -4
- synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +9 -5
- synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +4 -3
- 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 -10
- synth_ai/learning/__init__.py +10 -0
- synth_ai/learning/prompt_learning_client.py +276 -0
- synth_ai/learning/prompt_learning_types.py +184 -0
- synth_ai/learning/rl/client.py +3 -1
- synth_ai/pricing/__init__.py +2 -0
- synth_ai/pricing/model_pricing.py +57 -0
- synth_ai/streaming/__init__.py +29 -0
- synth_ai/streaming/config.py +94 -0
- synth_ai/streaming/handlers.py +518 -0
- synth_ai/streaming/streamer.py +320 -0
- synth_ai/streaming/types.py +95 -0
- synth_ai/task/apps/__init__.py +1 -0
- synth_ai/task/config.py +2 -0
- synth_ai/task/tracing_utils.py +25 -25
- synth_ai/task/validators.py +45 -9
- synth_ai/task_app_cfgs.py +21 -0
- synth_ai/tracing_v3/config.py +162 -19
- synth_ai/tracing_v3/constants.py +1 -1
- synth_ai/tracing_v3/db_config.py +24 -38
- synth_ai/tracing_v3/migration_helper.py +1 -2
- synth_ai/tracing_v3/storage/config.py +47 -13
- synth_ai/tracing_v3/storage/factory.py +3 -3
- synth_ai/tracing_v3/turso/daemon.py +113 -11
- synth_ai/tracing_v3/turso/native_manager.py +92 -16
- synth_ai/types.py +8 -0
- synth_ai/urls.py +11 -0
- synth_ai/utils/__init__.py +30 -1
- synth_ai/utils/agents.py +74 -0
- synth_ai/utils/bin.py +39 -0
- synth_ai/utils/cli.py +149 -5
- synth_ai/utils/env.py +40 -33
- synth_ai/utils/http.py +4 -1
- synth_ai/utils/json.py +72 -0
- synth_ai/utils/modal.py +285 -3
- synth_ai/utils/paths.py +48 -0
- synth_ai/utils/uvicorn.py +113 -0
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/METADATA +109 -6
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/RECORD +291 -142
- 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.19.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/licenses/LICENSE +0 -0
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Status and listing commands for the Synth CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from .config import resolve_backend_config
|
|
8
|
+
from .subcommands.files import files_group
|
|
9
|
+
from .subcommands.jobs import jobs_group
|
|
10
|
+
from .subcommands.models import models_group
|
|
11
|
+
from .subcommands.runs import runs_group
|
|
12
|
+
from .subcommands.summary import summary_command
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _attach_group(cli: click.Group, group: click.Group, name: str) -> None:
|
|
16
|
+
"""Attach the provided Click group to the CLI if not already present."""
|
|
17
|
+
if name in cli.commands:
|
|
18
|
+
return
|
|
19
|
+
cli.add_command(group, name=name)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def register(cli: click.Group) -> None:
|
|
23
|
+
"""Register all status command groups on the provided CLI root."""
|
|
24
|
+
|
|
25
|
+
@click.group(help="Inspect training jobs, models, files, and job runs.")
|
|
26
|
+
@click.option(
|
|
27
|
+
"--base-url",
|
|
28
|
+
envvar="SYNTH_STATUS_BASE_URL",
|
|
29
|
+
default=None,
|
|
30
|
+
help="Synth backend base URL (defaults to environment configuration).",
|
|
31
|
+
)
|
|
32
|
+
@click.option(
|
|
33
|
+
"--api-key",
|
|
34
|
+
envvar="SYNTH_STATUS_API_KEY",
|
|
35
|
+
default=None,
|
|
36
|
+
help="API key for authenticated requests (falls back to Synth defaults).",
|
|
37
|
+
)
|
|
38
|
+
@click.option(
|
|
39
|
+
"--timeout",
|
|
40
|
+
default=30.0,
|
|
41
|
+
show_default=True,
|
|
42
|
+
type=float,
|
|
43
|
+
help="HTTP request timeout in seconds.",
|
|
44
|
+
)
|
|
45
|
+
@click.pass_context
|
|
46
|
+
def status(ctx: click.Context, base_url: str | None, api_key: str | None, timeout: float) -> None:
|
|
47
|
+
"""Populate shared backend configuration for subcommands."""
|
|
48
|
+
cfg = resolve_backend_config(base_url=base_url, api_key=api_key, timeout=timeout)
|
|
49
|
+
ctx.ensure_object(dict)
|
|
50
|
+
ctx.obj["status_backend_config"] = cfg
|
|
51
|
+
|
|
52
|
+
status.add_command(jobs_group, name="jobs")
|
|
53
|
+
status.add_command(models_group, name="models")
|
|
54
|
+
status.add_command(files_group, name="files")
|
|
55
|
+
status.add_command(runs_group, name="runs")
|
|
56
|
+
status.add_command(summary_command, name="summary")
|
|
57
|
+
|
|
58
|
+
cli.add_command(status, name="status")
|
|
59
|
+
_attach_group(cli, jobs_group, "jobs")
|
|
60
|
+
_attach_group(cli, models_group, "models")
|
|
61
|
+
_attach_group(cli, files_group, "files")
|
|
62
|
+
_attach_group(cli, runs_group, "runs")
|
|
63
|
+
if "status-summary" not in cli.commands:
|
|
64
|
+
cli.add_command(summary_command, name="status-summary")
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Async HTTP client for Synth status and listing endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .config import BackendConfig
|
|
10
|
+
from .errors import StatusAPIError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class StatusAPIClient:
|
|
14
|
+
"""Thin wrapper around httpx.AsyncClient with convenience methods."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, config: BackendConfig) -> None:
|
|
17
|
+
self._config = config
|
|
18
|
+
timeout = httpx.Timeout(config.timeout)
|
|
19
|
+
self._client = httpx.AsyncClient(
|
|
20
|
+
base_url=config.base_url,
|
|
21
|
+
headers=config.headers,
|
|
22
|
+
timeout=timeout,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
async def __aenter__(self) -> StatusAPIClient:
|
|
26
|
+
await self._client.__aenter__()
|
|
27
|
+
return self
|
|
28
|
+
|
|
29
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
30
|
+
await self._client.__aexit__(*args)
|
|
31
|
+
|
|
32
|
+
async def close(self) -> None:
|
|
33
|
+
await self._client.aclose()
|
|
34
|
+
|
|
35
|
+
# Jobs -----------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
async def list_jobs(
|
|
38
|
+
self,
|
|
39
|
+
*,
|
|
40
|
+
status: str | None = None,
|
|
41
|
+
job_type: str | None = None,
|
|
42
|
+
created_after: str | None = None,
|
|
43
|
+
limit: int | None = None,
|
|
44
|
+
) -> list[dict[str, Any]]:
|
|
45
|
+
params: dict[str, Any] = {}
|
|
46
|
+
if status:
|
|
47
|
+
params["status"] = status
|
|
48
|
+
if job_type:
|
|
49
|
+
params["type"] = job_type
|
|
50
|
+
if created_after:
|
|
51
|
+
params["created_after"] = created_after
|
|
52
|
+
if limit:
|
|
53
|
+
params["limit"] = limit
|
|
54
|
+
resp = await self._client.get("/learning/jobs", params=params)
|
|
55
|
+
return self._json_list(resp, key="jobs")
|
|
56
|
+
|
|
57
|
+
async def get_job(self, job_id: str) -> dict[str, Any]:
|
|
58
|
+
resp = await self._client.get(f"/learning/jobs/{job_id}")
|
|
59
|
+
return self._json(resp)
|
|
60
|
+
|
|
61
|
+
async def get_job_status(self, job_id: str) -> dict[str, Any]:
|
|
62
|
+
resp = await self._client.get(f"/learning/jobs/{job_id}/status")
|
|
63
|
+
return self._json(resp)
|
|
64
|
+
|
|
65
|
+
async def cancel_job(self, job_id: str) -> dict[str, Any]:
|
|
66
|
+
resp = await self._client.post(f"/learning/jobs/{job_id}/cancel")
|
|
67
|
+
return self._json(resp)
|
|
68
|
+
|
|
69
|
+
async def get_job_config(self, job_id: str) -> dict[str, Any]:
|
|
70
|
+
resp = await self._client.get(f"/learning/jobs/{job_id}/config")
|
|
71
|
+
return self._json(resp)
|
|
72
|
+
|
|
73
|
+
async def get_job_metrics(self, job_id: str) -> dict[str, Any]:
|
|
74
|
+
resp = await self._client.get(f"/learning/jobs/{job_id}/metrics")
|
|
75
|
+
return self._json(resp)
|
|
76
|
+
|
|
77
|
+
async def get_job_timeline(self, job_id: str) -> list[dict[str, Any]]:
|
|
78
|
+
resp = await self._client.get(f"/learning/jobs/{job_id}/timeline")
|
|
79
|
+
return self._json_list(resp, key="timeline")
|
|
80
|
+
|
|
81
|
+
async def list_job_runs(self, job_id: str) -> list[dict[str, Any]]:
|
|
82
|
+
resp = await self._client.get(f"/jobs/{job_id}/runs")
|
|
83
|
+
return self._json_list(resp, key="runs")
|
|
84
|
+
|
|
85
|
+
async def get_job_events(
|
|
86
|
+
self,
|
|
87
|
+
job_id: str,
|
|
88
|
+
*,
|
|
89
|
+
since: str | None = None,
|
|
90
|
+
limit: int | None = None,
|
|
91
|
+
after: str | None = None,
|
|
92
|
+
run_id: str | None = None,
|
|
93
|
+
) -> list[dict[str, Any]]:
|
|
94
|
+
params: dict[str, Any] = {}
|
|
95
|
+
if since:
|
|
96
|
+
params["since"] = since
|
|
97
|
+
if limit:
|
|
98
|
+
params["limit"] = limit
|
|
99
|
+
if after:
|
|
100
|
+
params["after"] = after
|
|
101
|
+
if run_id:
|
|
102
|
+
params["run"] = run_id
|
|
103
|
+
resp = await self._client.get(f"/learning/jobs/{job_id}/events", params=params)
|
|
104
|
+
return self._json_list(resp, key="events")
|
|
105
|
+
|
|
106
|
+
# Files ----------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
async def list_files(
|
|
109
|
+
self,
|
|
110
|
+
*,
|
|
111
|
+
purpose: str | None = None,
|
|
112
|
+
limit: int | None = None,
|
|
113
|
+
) -> list[dict[str, Any]]:
|
|
114
|
+
params: dict[str, Any] = {}
|
|
115
|
+
if purpose:
|
|
116
|
+
params["purpose"] = purpose
|
|
117
|
+
if limit:
|
|
118
|
+
params["limit"] = limit
|
|
119
|
+
resp = await self._client.get("/files", params=params)
|
|
120
|
+
data = self._json(resp)
|
|
121
|
+
if isinstance(data, dict):
|
|
122
|
+
for key in ("files", "data", "items"):
|
|
123
|
+
if isinstance(data.get(key), list):
|
|
124
|
+
return list(data[key])
|
|
125
|
+
if isinstance(data, list):
|
|
126
|
+
return list(data)
|
|
127
|
+
return []
|
|
128
|
+
|
|
129
|
+
async def get_file(self, file_id: str) -> dict[str, Any]:
|
|
130
|
+
resp = await self._client.get(f"/files/{file_id}")
|
|
131
|
+
return self._json(resp)
|
|
132
|
+
|
|
133
|
+
# Models ---------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
async def list_models(
|
|
136
|
+
self,
|
|
137
|
+
*,
|
|
138
|
+
limit: int | None = None,
|
|
139
|
+
model_type: str | None = None,
|
|
140
|
+
) -> list[dict[str, Any]]:
|
|
141
|
+
params: dict[str, Any] = {}
|
|
142
|
+
if limit:
|
|
143
|
+
params["limit"] = limit
|
|
144
|
+
endpoint = "/learning/models/rl" if model_type == "rl" else "/learning/models"
|
|
145
|
+
resp = await self._client.get(endpoint, params=params)
|
|
146
|
+
return self._json_list(resp, key="models")
|
|
147
|
+
|
|
148
|
+
async def get_model(self, model_id: str) -> dict[str, Any]:
|
|
149
|
+
resp = await self._client.get(f"/learning/models/{model_id}")
|
|
150
|
+
return self._json(resp)
|
|
151
|
+
|
|
152
|
+
# Helpers --------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
def _json(self, response: httpx.Response) -> dict[str, Any]:
|
|
155
|
+
try:
|
|
156
|
+
response.raise_for_status()
|
|
157
|
+
except httpx.HTTPStatusError as exc:
|
|
158
|
+
detail = self._extract_detail(exc.response)
|
|
159
|
+
raise StatusAPIError(detail, exc.response.status_code if exc.response else None) from exc
|
|
160
|
+
try:
|
|
161
|
+
data = response.json()
|
|
162
|
+
except ValueError as exc:
|
|
163
|
+
raise StatusAPIError("Backend response was not valid JSON") from exc
|
|
164
|
+
if isinstance(data, dict):
|
|
165
|
+
return data
|
|
166
|
+
return {"data": data}
|
|
167
|
+
|
|
168
|
+
def _json_list(self, response: httpx.Response, *, key: str | None = None) -> list[dict[str, Any]]:
|
|
169
|
+
payload = self._json(response)
|
|
170
|
+
if key and isinstance(payload.get(key), list):
|
|
171
|
+
return list(payload[key])
|
|
172
|
+
if isinstance(payload.get("data"), list):
|
|
173
|
+
return list(payload["data"])
|
|
174
|
+
if isinstance(payload.get("results"), list):
|
|
175
|
+
return list(payload["results"])
|
|
176
|
+
if isinstance(payload, list):
|
|
177
|
+
return list(payload)
|
|
178
|
+
return []
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
def _extract_detail(response: httpx.Response | None) -> str:
|
|
182
|
+
if response is None:
|
|
183
|
+
return "Backend request failed"
|
|
184
|
+
try:
|
|
185
|
+
data = response.json()
|
|
186
|
+
if isinstance(data, dict):
|
|
187
|
+
for key in ("detail", "message", "error"):
|
|
188
|
+
if data.get(key):
|
|
189
|
+
return str(data[key])
|
|
190
|
+
return response.text
|
|
191
|
+
except ValueError:
|
|
192
|
+
return response.text
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Configuration utilities for the status command suite.
|
|
2
|
+
|
|
3
|
+
Provides helpers to resolve backend URLs, API keys, and request timeouts
|
|
4
|
+
from CLI options and environment variables.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import importlib
|
|
10
|
+
import os
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
DEFAULT_TIMEOUT = 30.0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _load_backend_helpers() -> tuple[str, Callable[[], tuple[str, str]] | None]:
|
|
18
|
+
"""Attempt to load shared backend helpers from synth_ai.config.base_url."""
|
|
19
|
+
try:
|
|
20
|
+
module = importlib.import_module("synth_ai.config.base_url")
|
|
21
|
+
except Exception:
|
|
22
|
+
return "https://agent-learning.onrender.com", None
|
|
23
|
+
|
|
24
|
+
default = getattr(module, "PROD_BASE_URL_DEFAULT", "https://agent-learning.onrender.com")
|
|
25
|
+
getter = getattr(module, "get_backend_from_env", None)
|
|
26
|
+
return str(default), getter if callable(getter) else None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
PROD_BASE_URL_DEFAULT, _GET_BACKEND_FROM_ENV = _load_backend_helpers()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _normalize_base_url(raw: str) -> str:
|
|
33
|
+
"""Ensure the configured base URL includes the /api/v1 prefix."""
|
|
34
|
+
base = raw.rstrip("/") if raw else ""
|
|
35
|
+
if not base:
|
|
36
|
+
return raw
|
|
37
|
+
if base.endswith("/api") or base.endswith("/api/v1") or "/api/" in base:
|
|
38
|
+
return base
|
|
39
|
+
return f"{base}/api/v1"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _default_base_url() -> str:
|
|
43
|
+
"""Compute the default backend base URL using env vars or helper module."""
|
|
44
|
+
for var in ("SYNTH_BACKEND_BASE_URL", "BACKEND_BASE_URL", "SYNTH_BASE_URL"):
|
|
45
|
+
val = os.getenv(var)
|
|
46
|
+
if val:
|
|
47
|
+
return _normalize_base_url(val)
|
|
48
|
+
if _GET_BACKEND_FROM_ENV:
|
|
49
|
+
try:
|
|
50
|
+
base, _ = _GET_BACKEND_FROM_ENV()
|
|
51
|
+
return _normalize_base_url(base)
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
return _normalize_base_url(PROD_BASE_URL_DEFAULT)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _resolve_api_key(cli_key: str | None) -> tuple[str | None, str | None]:
|
|
58
|
+
"""Resolve the API key from CLI input or known environment variables."""
|
|
59
|
+
if cli_key:
|
|
60
|
+
return cli_key, "--api-key"
|
|
61
|
+
for var in ("SYNTH_BACKEND_API_KEY", "SYNTH_API_KEY", "DEFAULT_DEV_API_KEY"):
|
|
62
|
+
val = os.getenv(var)
|
|
63
|
+
if val:
|
|
64
|
+
return val, var
|
|
65
|
+
return None, None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass()
|
|
69
|
+
class BackendConfig:
|
|
70
|
+
"""Configuration bundle shared across status commands."""
|
|
71
|
+
|
|
72
|
+
base_url: str
|
|
73
|
+
api_key: str | None
|
|
74
|
+
timeout: float = DEFAULT_TIMEOUT
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def headers(self) -> dict[str, str]:
|
|
78
|
+
if not self.api_key:
|
|
79
|
+
return {}
|
|
80
|
+
return {"Authorization": f"Bearer {self.api_key}"}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def resolve_backend_config(
|
|
84
|
+
*,
|
|
85
|
+
base_url: str | None,
|
|
86
|
+
api_key: str | None,
|
|
87
|
+
timeout: float | None = None,
|
|
88
|
+
) -> BackendConfig:
|
|
89
|
+
"""Resolve the backend configuration from CLI options/environment."""
|
|
90
|
+
resolved_url = _normalize_base_url(base_url) if base_url else _default_base_url()
|
|
91
|
+
key, _ = _resolve_api_key(api_key)
|
|
92
|
+
return BackendConfig(base_url=resolved_url, api_key=key, timeout=timeout or DEFAULT_TIMEOUT)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Custom error hierarchy for status CLI commands.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class StatusAPIError(RuntimeError):
|
|
10
|
+
"""Raised when the backend returns a non-success response."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, message: str, status_code: int | None = None):
|
|
13
|
+
super().__init__(message)
|
|
14
|
+
self.status_code = status_code
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class StatusCLIError(RuntimeError):
|
|
18
|
+
"""Raised for client-side validation errors."""
|
|
19
|
+
|
|
20
|
+
pass
|
|
@@ -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())
|