synth-ai 0.2.14__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 (354) hide show
  1. examples/README.md +1 -0
  2. examples/analyze_semantic_words.sh +2 -2
  3. examples/blog_posts/pokemon_vl/README.md +98 -0
  4. examples/blog_posts/pokemon_vl/configs/eval_qwen3_vl.toml +25 -0
  5. examples/blog_posts/pokemon_vl/configs/eval_rl_final.toml +24 -0
  6. examples/blog_posts/pokemon_vl/configs/filter_high_reward.toml +10 -0
  7. examples/blog_posts/pokemon_vl/configs/train_rl_from_sft.toml +42 -0
  8. examples/blog_posts/pokemon_vl/configs/train_sft_qwen4b_vl.toml +40 -0
  9. examples/blog_posts/warming_up_to_rl/README.md +158 -0
  10. examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b.toml +25 -0
  11. examples/blog_posts/warming_up_to_rl/configs/eval_groq_qwen32b.toml +25 -0
  12. examples/blog_posts/warming_up_to_rl/configs/eval_openai_gpt_oss_120b.toml +29 -0
  13. examples/blog_posts/warming_up_to_rl/configs/filter_high_reward_dataset.toml +10 -0
  14. examples/blog_posts/warming_up_to_rl/configs/train_rl_from_sft.toml +41 -0
  15. examples/blog_posts/warming_up_to_rl/configs/train_sft_qwen4b.toml +40 -0
  16. examples/dev/qwen3_32b_qlora_4xh100.toml +5 -0
  17. examples/multi_step/SFT_README.md +147 -0
  18. examples/multi_step/configs/crafter_rl_outcome.toml +1 -1
  19. examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +73 -115
  20. examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +1 -1
  21. examples/multi_step/configs/crafter_rl_stepwise_simple.toml +1 -1
  22. examples/multi_step/configs/crafter_rl_stepwise_simple_NEW_FORMAT.toml +105 -0
  23. examples/multi_step/configs/crafter_sft_qwen30b_lora.toml +62 -0
  24. examples/multi_step/configs/verilog_rl_lora.toml +80 -123
  25. examples/multi_step/convert_traces_to_sft.py +84 -0
  26. examples/multi_step/run_sft_qwen30b.sh +45 -0
  27. examples/qwen_coder/configs/coder_lora_30b.toml +1 -2
  28. examples/qwen_coder/configs/coder_lora_4b.toml +5 -1
  29. examples/qwen_coder/configs/coder_lora_small.toml +1 -2
  30. examples/qwen_vl/BUGS_AND_FIXES.md +232 -0
  31. examples/qwen_vl/IMAGE_VALIDATION_COMPLETE.md +271 -0
  32. examples/qwen_vl/IMAGE_VALIDATION_SUMMARY.md +260 -0
  33. examples/qwen_vl/INFERENCE_SFT_TESTS.md +412 -0
  34. examples/qwen_vl/NEXT_STEPS_2B.md +325 -0
  35. examples/qwen_vl/QUICKSTART.md +327 -0
  36. examples/qwen_vl/QUICKSTART_RL_VISION.md +110 -0
  37. examples/qwen_vl/README.md +152 -0
  38. examples/qwen_vl/RL_VISION_COMPLETE.md +475 -0
  39. examples/qwen_vl/RL_VISION_TESTING.md +333 -0
  40. examples/qwen_vl/SDK_VISION_INTEGRATION.md +328 -0
  41. examples/qwen_vl/SETUP_COMPLETE.md +274 -0
  42. examples/qwen_vl/VISION_TESTS_COMPLETE.md +489 -0
  43. examples/qwen_vl/VLM_PIPELINE_COMPLETE.md +242 -0
  44. examples/qwen_vl/__init__.py +2 -0
  45. examples/qwen_vl/collect_data_via_cli.md +415 -0
  46. examples/qwen_vl/collect_vision_traces.py +368 -0
  47. examples/qwen_vl/configs/crafter_rl_vision_qwen3vl4b.toml +110 -0
  48. examples/qwen_vl/configs/crafter_vlm_sft_example.toml +59 -0
  49. examples/qwen_vl/configs/eval_gpt4o_mini_vision.toml +26 -0
  50. examples/qwen_vl/configs/eval_gpt4o_vision_proper.toml +29 -0
  51. examples/qwen_vl/configs/eval_gpt5nano_vision.toml +26 -0
  52. examples/qwen_vl/configs/eval_qwen3vl_vision.toml +26 -0
  53. examples/qwen_vl/configs/filter_qwen3vl_sft.toml +49 -0
  54. examples/qwen_vl/configs/filter_vision_sft.toml +52 -0
  55. examples/qwen_vl/configs/filter_vision_test.toml +8 -0
  56. examples/qwen_vl/configs/sft_qwen3_vl_2b_test.toml +54 -0
  57. examples/qwen_vl/crafter_gpt5nano_agent.py +308 -0
  58. examples/qwen_vl/crafter_qwen_vl_agent.py +300 -0
  59. examples/qwen_vl/run_vision_comparison.sh +61 -0
  60. examples/qwen_vl/run_vision_sft_pipeline.sh +175 -0
  61. examples/qwen_vl/test_image_validation.py +201 -0
  62. examples/qwen_vl/test_sft_vision_data.py +110 -0
  63. examples/rl/README.md +6 -6
  64. examples/rl/configs/eval_base_qwen.toml +17 -0
  65. examples/rl/configs/eval_rl_qwen.toml +13 -0
  66. examples/rl/configs/rl_from_base_qwen.toml +62 -0
  67. examples/rl/configs/rl_from_base_qwen17.toml +79 -0
  68. examples/rl/configs/rl_from_ft_qwen.toml +37 -0
  69. examples/rl/run_eval.py +436 -0
  70. examples/rl/run_rl_and_save.py +111 -0
  71. examples/rl/task_app/README.md +21 -0
  72. examples/rl/task_app/math_single_step.py +990 -0
  73. examples/rl/task_app/math_task_app.py +111 -0
  74. examples/run_crafter_demo.sh +2 -2
  75. examples/sft/README.md +6 -6
  76. examples/sft/configs/crafter_fft_qwen0p6b.toml +7 -2
  77. examples/sft/configs/crafter_lora_qwen0p6b.toml +7 -3
  78. examples/sft/evaluate.py +2 -4
  79. examples/sft/export_dataset.py +7 -4
  80. examples/swe/task_app/README.md +33 -3
  81. examples/swe/task_app/grpo_swe_mini.py +4 -1
  82. examples/swe/task_app/grpo_swe_mini_task_app.py +0 -12
  83. examples/swe/task_app/hosted/envs/crafter/react_agent.py +1 -1
  84. examples/swe/task_app/hosted/envs/mini_swe/environment.py +50 -23
  85. examples/swe/task_app/hosted/inference/openai_client.py +4 -4
  86. examples/swe/task_app/hosted/policy_routes.py +0 -2
  87. examples/swe/task_app/hosted/rollout.py +0 -8
  88. examples/swe/task_app/morph_backend.py +178 -0
  89. examples/task_apps/crafter/task_app/README.md +1 -1
  90. examples/task_apps/crafter/task_app/grpo_crafter.py +70 -10
  91. examples/task_apps/crafter/task_app/grpo_crafter_task_app.py +1 -1
  92. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/policy.py +63 -27
  93. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/react_agent.py +1 -2
  94. examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +48 -50
  95. examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +75 -36
  96. examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +31 -15
  97. examples/task_apps/enron/__init__.py +1 -0
  98. examples/task_apps/enron/task_app/grpo_enron_task_app.py +1 -1
  99. examples/task_apps/math/README.md +1 -2
  100. examples/task_apps/pokemon_red/README.md +3 -4
  101. examples/task_apps/pokemon_red/eval_image_only_gpt4o.toml +6 -5
  102. examples/task_apps/pokemon_red/eval_pokemon_red_policy.py +1 -2
  103. examples/task_apps/pokemon_red/task_app.py +36 -5
  104. examples/task_apps/sokoban/README.md +2 -3
  105. examples/task_apps/verilog/eval_groq_qwen32b.toml +12 -14
  106. examples/task_apps/verilog/task_app/grpo_verilog_task_app.py +1 -1
  107. examples/vlm/README.md +3 -3
  108. examples/vlm/configs/crafter_vlm_gpt4o.toml +5 -0
  109. examples/vlm/crafter_openai_vlm_agent.py +3 -5
  110. examples/vlm/filter_image_rows.py +1 -1
  111. examples/vlm/run_crafter_vlm_benchmark.py +2 -2
  112. examples/warming_up_to_rl/_utils.py +92 -0
  113. examples/warming_up_to_rl/analyze_trace_db.py +1 -1
  114. examples/warming_up_to_rl/configs/crafter_fft.toml +5 -0
  115. examples/warming_up_to_rl/configs/eval_fft_qwen4b.toml +2 -0
  116. examples/warming_up_to_rl/configs/eval_groq_qwen32b.toml +2 -0
  117. examples/warming_up_to_rl/configs/eval_modal_qwen4b.toml +2 -1
  118. examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +2 -1
  119. examples/warming_up_to_rl/configs/rl_from_ft.toml +2 -0
  120. examples/warming_up_to_rl/export_trace_sft.py +174 -60
  121. examples/warming_up_to_rl/readme.md +63 -132
  122. examples/warming_up_to_rl/run_fft_and_save.py +1 -1
  123. examples/warming_up_to_rl/run_local_rollout_traced.py +1 -1
  124. examples/warming_up_to_rl/run_rl_and_save.py +1 -1
  125. examples/warming_up_to_rl/task_app/README.md +42 -0
  126. examples/warming_up_to_rl/task_app/grpo_crafter.py +827 -0
  127. examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +135 -0
  128. examples/warming_up_to_rl/task_app/synth_envs_hosted/README.md +173 -0
  129. examples/warming_up_to_rl/task_app/synth_envs_hosted/__init__.py +5 -0
  130. examples/warming_up_to_rl/task_app/synth_envs_hosted/branching.py +143 -0
  131. examples/warming_up_to_rl/task_app/synth_envs_hosted/environment_routes.py +1226 -0
  132. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/__init__.py +1 -0
  133. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/__init__.py +6 -0
  134. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/app.py +1 -0
  135. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/environment.py +522 -0
  136. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +454 -0
  137. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +108 -0
  138. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/shared.py +305 -0
  139. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/tools.py +47 -0
  140. examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +204 -0
  141. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/__init__.py +5 -0
  142. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +618 -0
  143. examples/warming_up_to_rl/task_app/synth_envs_hosted/main.py +100 -0
  144. examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +1084 -0
  145. examples/warming_up_to_rl/task_app/synth_envs_hosted/registry.py +195 -0
  146. examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +1861 -0
  147. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/__init__.py +5 -0
  148. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/volume.py +211 -0
  149. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_agents.py +161 -0
  150. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_service.py +137 -0
  151. examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +62 -0
  152. examples/workflows/math_rl/configs/rl_from_base_qwen.toml +27 -0
  153. examples/workflows/math_rl/configs/rl_from_base_qwen17.toml +5 -0
  154. synth_ai/__init__.py +44 -30
  155. synth_ai/_utils/__init__.py +47 -0
  156. synth_ai/_utils/base_url.py +10 -0
  157. synth_ai/_utils/http.py +10 -0
  158. synth_ai/_utils/prompts.py +10 -0
  159. synth_ai/_utils/task_app_state.py +12 -0
  160. synth_ai/_utils/user_config.py +10 -0
  161. synth_ai/api/models/supported.py +144 -7
  162. synth_ai/api/train/__init__.py +13 -1
  163. synth_ai/api/train/builders.py +9 -3
  164. synth_ai/api/train/cli.py +155 -17
  165. synth_ai/api/train/config_finder.py +18 -11
  166. synth_ai/api/train/configs/__init__.py +8 -1
  167. synth_ai/api/train/configs/rl.py +32 -7
  168. synth_ai/api/train/configs/sft.py +6 -2
  169. synth_ai/api/train/configs/shared.py +59 -2
  170. synth_ai/api/train/env_resolver.py +13 -10
  171. synth_ai/auth/credentials.py +119 -0
  172. synth_ai/cli/__init__.py +61 -69
  173. synth_ai/cli/_modal_wrapper.py +7 -5
  174. synth_ai/cli/_typer_patch.py +0 -2
  175. synth_ai/cli/_validate_task_app.py +22 -4
  176. synth_ai/cli/commands/__init__.py +17 -0
  177. synth_ai/cli/commands/demo/__init__.py +6 -0
  178. synth_ai/cli/commands/demo/core.py +163 -0
  179. synth_ai/cli/commands/deploy/__init__.py +23 -0
  180. synth_ai/cli/commands/deploy/core.py +614 -0
  181. synth_ai/cli/commands/deploy/errors.py +72 -0
  182. synth_ai/cli/commands/deploy/validation.py +11 -0
  183. synth_ai/cli/commands/eval/__init__.py +19 -0
  184. synth_ai/cli/commands/eval/core.py +1109 -0
  185. synth_ai/cli/commands/eval/errors.py +81 -0
  186. synth_ai/cli/commands/eval/validation.py +133 -0
  187. synth_ai/cli/commands/filter/__init__.py +12 -0
  188. synth_ai/cli/commands/filter/core.py +388 -0
  189. synth_ai/cli/commands/filter/errors.py +55 -0
  190. synth_ai/cli/commands/filter/validation.py +77 -0
  191. synth_ai/cli/commands/help/__init__.py +177 -0
  192. synth_ai/cli/commands/help/core.py +73 -0
  193. synth_ai/cli/commands/status/__init__.py +64 -0
  194. synth_ai/cli/commands/status/client.py +192 -0
  195. synth_ai/cli/commands/status/config.py +92 -0
  196. synth_ai/cli/commands/status/errors.py +20 -0
  197. synth_ai/cli/commands/status/formatters.py +164 -0
  198. synth_ai/cli/commands/status/subcommands/__init__.py +9 -0
  199. synth_ai/cli/commands/status/subcommands/files.py +79 -0
  200. synth_ai/cli/commands/status/subcommands/jobs.py +334 -0
  201. synth_ai/cli/commands/status/subcommands/models.py +79 -0
  202. synth_ai/cli/commands/status/subcommands/runs.py +81 -0
  203. synth_ai/cli/commands/status/subcommands/summary.py +47 -0
  204. synth_ai/cli/commands/status/utils.py +114 -0
  205. synth_ai/cli/commands/train/__init__.py +53 -0
  206. synth_ai/cli/commands/train/core.py +21 -0
  207. synth_ai/cli/commands/train/errors.py +117 -0
  208. synth_ai/cli/commands/train/judge_schemas.py +199 -0
  209. synth_ai/cli/commands/train/judge_validation.py +304 -0
  210. synth_ai/cli/commands/train/validation.py +443 -0
  211. synth_ai/cli/demo.py +2 -162
  212. synth_ai/cli/deploy/__init__.py +28 -0
  213. synth_ai/cli/deploy/core.py +5 -0
  214. synth_ai/cli/deploy/errors.py +23 -0
  215. synth_ai/cli/deploy/validation.py +5 -0
  216. synth_ai/cli/eval/__init__.py +36 -0
  217. synth_ai/cli/eval/core.py +5 -0
  218. synth_ai/cli/eval/errors.py +31 -0
  219. synth_ai/cli/eval/validation.py +5 -0
  220. synth_ai/cli/filter/__init__.py +28 -0
  221. synth_ai/cli/filter/core.py +5 -0
  222. synth_ai/cli/filter/errors.py +23 -0
  223. synth_ai/cli/filter/validation.py +5 -0
  224. synth_ai/cli/legacy_root_backup.py +3 -1
  225. synth_ai/cli/lib/__init__.py +10 -0
  226. synth_ai/cli/lib/task_app_discovery.py +7 -0
  227. synth_ai/cli/lib/task_app_env.py +518 -0
  228. synth_ai/cli/modal_serve/__init__.py +12 -0
  229. synth_ai/cli/modal_serve/core.py +14 -0
  230. synth_ai/cli/modal_serve/errors.py +8 -0
  231. synth_ai/cli/modal_serve/validation.py +11 -0
  232. synth_ai/cli/recent.py +2 -1
  233. synth_ai/cli/serve/__init__.py +12 -0
  234. synth_ai/cli/serve/core.py +14 -0
  235. synth_ai/cli/serve/errors.py +8 -0
  236. synth_ai/cli/serve/validation.py +11 -0
  237. synth_ai/cli/setup.py +21 -0
  238. synth_ai/cli/status.py +7 -126
  239. synth_ai/cli/task_app_deploy.py +7 -0
  240. synth_ai/cli/task_app_list.py +25 -0
  241. synth_ai/cli/task_app_modal_serve.py +11 -0
  242. synth_ai/cli/task_app_serve.py +11 -0
  243. synth_ai/cli/task_apps.py +110 -1499
  244. synth_ai/cli/traces.py +1 -1
  245. synth_ai/cli/train/__init__.py +12 -0
  246. synth_ai/cli/train/core.py +21 -0
  247. synth_ai/cli/train/errors.py +8 -0
  248. synth_ai/cli/train/validation.py +24 -0
  249. synth_ai/cli/train.py +5 -0
  250. synth_ai/cli/turso.py +1 -1
  251. synth_ai/cli/watch.py +1 -1
  252. synth_ai/demos/__init__.py +10 -0
  253. synth_ai/demos/core/__init__.py +28 -1
  254. synth_ai/demos/crafter/__init__.py +1 -0
  255. synth_ai/demos/crafter/crafter_fft_4b.toml +55 -0
  256. synth_ai/demos/crafter/grpo_crafter_task_app.py +185 -0
  257. synth_ai/demos/crafter/rl_from_base_qwen4b.toml +74 -0
  258. synth_ai/demos/demo_registry.py +176 -0
  259. synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +1 -1
  260. synth_ai/demos/math/__init__.py +1 -0
  261. synth_ai/demos/math/_common.py +16 -0
  262. synth_ai/demos/math/app.py +38 -0
  263. synth_ai/demos/math/config.toml +76 -0
  264. synth_ai/demos/math/deploy_modal.py +54 -0
  265. synth_ai/demos/math/modal_task_app.py +702 -0
  266. synth_ai/demos/math/task_app_entry.py +51 -0
  267. synth_ai/environments/environment/core.py +7 -1
  268. synth_ai/environments/examples/bandit/engine.py +0 -1
  269. synth_ai/environments/examples/bandit/environment.py +0 -1
  270. synth_ai/environments/examples/red/engine.py +33 -12
  271. synth_ai/environments/examples/red/engine_helpers/reward_components.py +151 -179
  272. synth_ai/environments/examples/red/environment.py +26 -0
  273. synth_ai/environments/examples/red/trace_hooks_v3.py +168 -0
  274. synth_ai/environments/examples/wordle/environment.py +0 -1
  275. synth_ai/evals/base.py +16 -5
  276. synth_ai/evals/client.py +1 -1
  277. synth_ai/http.py +8 -22
  278. synth_ai/inference/client.py +1 -1
  279. synth_ai/judge_schemas.py +4 -5
  280. synth_ai/learning/client.py +1 -1
  281. synth_ai/learning/health.py +1 -1
  282. synth_ai/learning/jobs.py +1 -1
  283. synth_ai/learning/rl/client.py +4 -2
  284. synth_ai/learning/rl/env_keys.py +1 -1
  285. synth_ai/learning/rl/secrets.py +1 -1
  286. synth_ai/learning/sft/client.py +1 -1
  287. synth_ai/learning/sft/data.py +407 -4
  288. synth_ai/learning/validators.py +4 -1
  289. synth_ai/streaming/__init__.py +29 -0
  290. synth_ai/streaming/config.py +94 -0
  291. synth_ai/streaming/handlers.py +469 -0
  292. synth_ai/streaming/streamer.py +301 -0
  293. synth_ai/streaming/types.py +95 -0
  294. synth_ai/task/apps/__init__.py +4 -2
  295. synth_ai/task/config.py +6 -4
  296. synth_ai/task/rubrics/__init__.py +1 -2
  297. synth_ai/task/rubrics/loaders.py +14 -10
  298. synth_ai/task/rubrics.py +219 -0
  299. synth_ai/task/trace_correlation_helpers.py +24 -11
  300. synth_ai/task/tracing_utils.py +14 -3
  301. synth_ai/task/validators.py +0 -1
  302. synth_ai/tracing_v3/abstractions.py +3 -3
  303. synth_ai/tracing_v3/config.py +15 -13
  304. synth_ai/tracing_v3/constants.py +21 -0
  305. synth_ai/tracing_v3/db_config.py +3 -1
  306. synth_ai/tracing_v3/decorators.py +10 -7
  307. synth_ai/tracing_v3/llm_call_record_helpers.py +5 -5
  308. synth_ai/tracing_v3/migration_helper.py +1 -2
  309. synth_ai/tracing_v3/session_tracer.py +7 -7
  310. synth_ai/tracing_v3/storage/base.py +29 -29
  311. synth_ai/tracing_v3/storage/config.py +3 -3
  312. synth_ai/tracing_v3/turso/daemon.py +8 -9
  313. synth_ai/tracing_v3/turso/native_manager.py +80 -72
  314. synth_ai/tracing_v3/utils.py +2 -2
  315. synth_ai/utils/__init__.py +101 -0
  316. synth_ai/utils/base_url.py +94 -0
  317. synth_ai/utils/cli.py +131 -0
  318. synth_ai/utils/env.py +294 -0
  319. synth_ai/utils/http.py +172 -0
  320. synth_ai/utils/modal.py +308 -0
  321. synth_ai/utils/process.py +212 -0
  322. synth_ai/utils/prompts.py +39 -0
  323. synth_ai/utils/sqld.py +122 -0
  324. synth_ai/utils/task_app_discovery.py +882 -0
  325. synth_ai/utils/task_app_env.py +186 -0
  326. synth_ai/utils/task_app_state.py +318 -0
  327. synth_ai/utils/user_config.py +137 -0
  328. synth_ai/v0/config/__init__.py +1 -5
  329. synth_ai/v0/config/base_url.py +1 -7
  330. synth_ai/v0/tracing/config.py +1 -1
  331. synth_ai/v0/tracing/decorators.py +1 -1
  332. synth_ai/v0/tracing/upload.py +1 -1
  333. synth_ai/v0/tracing_v1/config.py +1 -1
  334. synth_ai/v0/tracing_v1/decorators.py +1 -1
  335. synth_ai/v0/tracing_v1/upload.py +1 -1
  336. {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/METADATA +91 -32
  337. {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/RECORD +341 -154
  338. synth_ai/cli/man.py +0 -106
  339. synth_ai/cli/tui.py +0 -57
  340. synth_ai/compound/cais.py +0 -0
  341. synth_ai/core/experiment.py +0 -13
  342. synth_ai/core/system.py +0 -15
  343. synth_ai/demo_registry.py +0 -295
  344. synth_ai/handshake.py +0 -109
  345. synth_ai/tui/__init__.py +0 -5
  346. synth_ai/tui/__main__.py +0 -13
  347. synth_ai/tui/cli/__init__.py +0 -1
  348. synth_ai/tui/cli/query_experiments.py +0 -164
  349. synth_ai/tui/cli/query_experiments_v3.py +0 -164
  350. synth_ai/tui/dashboard.py +0 -906
  351. {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/WHEEL +0 -0
  352. {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/entry_points.txt +0 -0
  353. {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/licenses/LICENSE +0 -0
  354. {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from collections.abc import MutableMapping
5
+ from typing import Any
6
+
7
+ __all__ = ["validate_filter_options"]
8
+
9
+
10
+ def validate_filter_options(options: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
11
+ """Validate parameters passed to the filter CLI command."""
12
+ # Coerce optional collections to the expected container types and strip blanks
13
+ result: dict[str, Any] = dict(options)
14
+
15
+ def _coerce_list(key: str) -> None:
16
+ value = result.get(key)
17
+ if value is None:
18
+ result[key] = []
19
+ elif isinstance(value, list | tuple | set):
20
+ result[key] = [str(item).strip() for item in value if str(item).strip()]
21
+ else:
22
+ result[key] = [str(value).strip()] if str(value).strip() else []
23
+
24
+ def _coerce_dict(key: str) -> None:
25
+ value = result.get(key)
26
+ if value is None:
27
+ result[key] = {}
28
+ elif isinstance(value, MutableMapping):
29
+ normalized: dict[str, float] = {}
30
+ for k, v in value.items():
31
+ if k is None:
32
+ continue
33
+ try:
34
+ number = float(v)
35
+ if math.isnan(number) or math.isinf(number):
36
+ continue
37
+ normalized[str(k).strip()] = number
38
+ except Exception:
39
+ continue
40
+ result[key] = normalized
41
+ else:
42
+ result[key] = {}
43
+
44
+ _coerce_list("splits")
45
+ _coerce_list("task_ids")
46
+ _coerce_list("models")
47
+ _coerce_dict("min_judge_scores")
48
+ _coerce_dict("max_judge_scores")
49
+
50
+ for duration_key in ("min_official_score", "max_official_score"):
51
+ value = result.get(duration_key)
52
+ if value is None or value == "":
53
+ result[duration_key] = None
54
+ else:
55
+ try:
56
+ result[duration_key] = float(value)
57
+ except Exception:
58
+ result[duration_key] = None
59
+
60
+ for int_key in ("limit", "offset", "shuffle_seed"):
61
+ value = result.get(int_key)
62
+ if value is None or value == "":
63
+ result[int_key] = None
64
+ else:
65
+ try:
66
+ result[int_key] = int(value)
67
+ except Exception:
68
+ result[int_key] = None
69
+
70
+ shuffle_value = result.get("shuffle")
71
+ if isinstance(shuffle_value, str):
72
+ result["shuffle"] = shuffle_value.strip().lower() in {"1", "true", "yes"}
73
+ else:
74
+ result["shuffle"] = bool(shuffle_value)
75
+
76
+ # Preserve extra keys (e.g., min_created_at) as-is for downstream handling
77
+ return result
@@ -0,0 +1,177 @@
1
+ """Help content for CLI commands."""
2
+
3
+ DEPLOY_HELP = """
4
+ Deploy a Synth AI task app locally or to Modal.
5
+
6
+ OVERVIEW
7
+ --------
8
+ The deploy command supports two runtimes:
9
+ • modal: Deploy to Modal's cloud platform (default)
10
+ • uvicorn: Run locally with FastAPI/Uvicorn
11
+
12
+ BASIC USAGE
13
+ -----------
14
+ # Deploy to Modal (production)
15
+ uvx synth-ai deploy
16
+
17
+ # Deploy specific task app
18
+ uvx synth-ai deploy my-math-app
19
+
20
+ # Run locally for development
21
+ uvx synth-ai deploy --runtime=uvicorn --port 8001
22
+
23
+ MODAL DEPLOYMENT
24
+ ----------------
25
+ Modal deployment requires:
26
+ 1. Modal authentication (run: modal token new)
27
+ 2. ENVIRONMENT_API_KEY (run: uvx synth-ai setup)
28
+
29
+ Options:
30
+ --modal-mode [deploy|serve] Use 'deploy' for production (default),
31
+ 'serve' for ephemeral development
32
+ --name TEXT Override Modal app name
33
+ --dry-run Preview the deploy command without executing
34
+ --env-file PATH Env file(s) to load (can be repeated)
35
+
36
+ Examples:
37
+ # Standard production deployment
38
+ uvx synth-ai deploy --runtime=modal
39
+
40
+ # Deploy with custom name
41
+ uvx synth-ai deploy --runtime=modal --name my-task-app-v2
42
+
43
+ # Preview deployment command
44
+ uvx synth-ai deploy --dry-run
45
+
46
+ # Deploy with custom env file
47
+ uvx synth-ai deploy --env-file .env.production
48
+
49
+ LOCAL DEVELOPMENT
50
+ -----------------
51
+ Run locally with auto-reload and tracing:
52
+
53
+ uvx synth-ai deploy --runtime=uvicorn --port 8001 --reload
54
+
55
+ Options:
56
+ --host TEXT Bind address (default: 0.0.0.0)
57
+ --port INTEGER Port number (prompted if not provided)
58
+ --reload/--no-reload Enable auto-reload on code changes
59
+ --force/--no-force Kill existing process on port
60
+ --trace PATH Enable tracing to directory (default: traces/v3)
61
+ --trace-db PATH SQLite DB for traces
62
+
63
+ Examples:
64
+ # Basic local server
65
+ uvx synth-ai deploy --runtime=uvicorn
66
+
67
+ # Development with auto-reload
68
+ uvx synth-ai deploy --runtime=uvicorn --reload --port 8001
69
+
70
+ # With custom trace directory
71
+ uvx synth-ai deploy --runtime=uvicorn --trace ./my-traces
72
+
73
+ TROUBLESHOOTING
74
+ ---------------
75
+ Common issues:
76
+
77
+ 1. "ENVIRONMENT_API_KEY is required"
78
+ → Run: uvx synth-ai setup
79
+
80
+ 2. "Modal CLI not found"
81
+ → Install: pip install modal
82
+ → Authenticate: modal token new
83
+
84
+ 3. "Task app not found"
85
+ → Check app_id matches your task_app.py configuration
86
+ → Run: uvx synth-ai task-app list (if available)
87
+
88
+ 4. "Port already in use" (uvicorn)
89
+ → Use --force to kill existing process
90
+ → Or specify different --port
91
+
92
+ 5. "No env file discovered"
93
+ → Create .env file with required keys
94
+ → Or pass --env-file explicitly
95
+
96
+ ENVIRONMENT VARIABLES
97
+ ---------------------
98
+ SYNTH_API_KEY Your Synth platform API key
99
+ ENVIRONMENT_API_KEY Task environment authentication
100
+ TASK_APP_BASE_URL Base URL for deployed task app
101
+ DEMO_DIR Demo directory path
102
+ SYNTH_DEMO_DIR Alternative demo directory
103
+
104
+ For more information: https://docs.usesynth.ai/deploy
105
+ """
106
+
107
+ SETUP_HELP = """
108
+ Configure Synth AI credentials and environment.
109
+
110
+ OVERVIEW
111
+ --------
112
+ The setup command initializes your Synth AI environment by:
113
+ 1. Authenticating with the Synth platform via browser
114
+ 2. Saving your API keys to ~/.synth/config
115
+ 3. Verifying Modal authentication (for deployments)
116
+ 4. Testing connectivity to backend services
117
+
118
+ USAGE
119
+ -----
120
+ uvx synth-ai setup
121
+
122
+ The command will:
123
+ • Open your browser for authentication (or prompt for manual entry)
124
+ • Save SYNTH_API_KEY and ENVIRONMENT_API_KEY
125
+ • Verify Modal is authenticated
126
+ • Test backend connectivity
127
+
128
+ WHAT YOU'LL NEED
129
+ ----------------
130
+ • Web browser for authentication
131
+ • Modal account (for deployments): https://modal.com
132
+ • Active internet connection
133
+
134
+ TROUBLESHOOTING
135
+ ---------------
136
+ 1. "Failed to fetch keys from frontend"
137
+ → You'll be prompted to enter keys manually
138
+ → Get keys from: https://www.usesynth.ai/dashboard/settings
139
+
140
+ 2. "Modal authentication status: not authenticated"
141
+ → Run: modal token new
142
+ → Then re-run: uvx synth-ai setup
143
+
144
+ 3. Browser doesn't open
145
+ → Check your default browser settings
146
+ → Or enter keys manually when prompted
147
+
148
+ WHERE ARE KEYS STORED?
149
+ ----------------------
150
+ Keys are saved to: ~/.synth/config
151
+
152
+ This file is read automatically by all Synth AI commands.
153
+ You can also use .env files in your project directory.
154
+
155
+ NEXT STEPS
156
+ ----------
157
+ After setup completes:
158
+ 1. Deploy your task app: uvx synth-ai deploy
159
+ 2. Start local development: uvx synth-ai deploy --runtime=uvicorn
160
+ 3. Run training: uvx synth-ai train
161
+
162
+ For more information: https://docs.usesynth.ai/setup
163
+ """
164
+
165
+ COMMAND_HELP = {
166
+ "deploy": DEPLOY_HELP,
167
+ "setup": SETUP_HELP,
168
+ }
169
+
170
+
171
+ def get_command_help(command: str) -> str | None:
172
+ """Get detailed help text for a command."""
173
+ return COMMAND_HELP.get(command)
174
+
175
+
176
+ __all__ = ["DEPLOY_HELP", "SETUP_HELP", "COMMAND_HELP", "get_command_help"]
177
+
@@ -0,0 +1,73 @@
1
+ """Help command implementation."""
2
+
3
+ import click
4
+ from click.exceptions import Exit
5
+
6
+ from . import COMMAND_HELP, get_command_help
7
+
8
+
9
+ @click.command("help")
10
+ @click.argument("command_name", type=str, required=False)
11
+ def help_command(command_name: str | None) -> None:
12
+ """Display detailed help for Synth AI commands.
13
+
14
+ USAGE
15
+ -----
16
+ uvx synth-ai help [COMMAND]
17
+
18
+ EXAMPLES
19
+ --------
20
+ # List available help topics
21
+ uvx synth-ai help
22
+
23
+ # Get detailed help for deploy
24
+ uvx synth-ai help deploy
25
+
26
+ # Get detailed help for setup
27
+ uvx synth-ai help setup
28
+ """
29
+ if not command_name:
30
+ # Show list of available help topics
31
+ click.echo("Synth AI - Detailed Help")
32
+ click.echo("=" * 50)
33
+ click.echo("\nAvailable help topics:")
34
+ click.echo("")
35
+
36
+ for cmd in sorted(COMMAND_HELP.keys()):
37
+ click.echo(f" • {cmd}")
38
+
39
+ click.echo("\nUsage:")
40
+ click.echo(" uvx synth-ai help [COMMAND]")
41
+ click.echo("")
42
+ click.echo("Examples:")
43
+ click.echo(" uvx synth-ai help deploy")
44
+ click.echo(" uvx synth-ai help setup")
45
+ click.echo("")
46
+ click.echo("You can also use standard --help flags:")
47
+ click.echo(" uvx synth-ai deploy --help")
48
+ click.echo(" uvx synth-ai setup --help")
49
+ return
50
+
51
+ # Show detailed help for specific command
52
+ help_text = get_command_help(command_name)
53
+ if not help_text:
54
+ click.echo(f"No detailed help available for '{command_name}'", err=True)
55
+ click.echo(f"\nTry: uvx synth-ai {command_name} --help", err=True)
56
+ click.echo("Or: uvx synth-ai help (to see available topics)", err=True)
57
+ raise Exit(1)
58
+
59
+ click.echo(help_text)
60
+
61
+
62
+ def get_command() -> click.Command:
63
+ """Get the help command for registration."""
64
+ return help_command
65
+
66
+
67
+ def register(group: click.Group) -> None:
68
+ """Register the help command with a Click group."""
69
+ group.add_command(help_command)
70
+
71
+
72
+ __all__ = ["help_command", "get_command", "register"]
73
+
@@ -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