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.

Files changed (299) hide show
  1. examples/analyze_semantic_words.sh +2 -2
  2. examples/baseline/banking77_baseline.py +204 -0
  3. examples/baseline/crafter_baseline.py +407 -0
  4. examples/baseline/pokemon_red_baseline.py +326 -0
  5. examples/baseline/simple_baseline.py +56 -0
  6. examples/baseline/warming_up_to_rl_baseline.py +239 -0
  7. examples/blog_posts/gepa/README.md +355 -0
  8. examples/blog_posts/gepa/configs/banking77_gepa_local.toml +95 -0
  9. examples/blog_posts/gepa/configs/banking77_gepa_test.toml +82 -0
  10. examples/blog_posts/gepa/configs/banking77_mipro_local.toml +52 -0
  11. examples/blog_posts/gepa/configs/hotpotqa_gepa_local.toml +59 -0
  12. examples/blog_posts/gepa/configs/hotpotqa_gepa_qwen.toml +36 -0
  13. examples/blog_posts/gepa/configs/hotpotqa_mipro_local.toml +53 -0
  14. examples/blog_posts/gepa/configs/hover_gepa_local.toml +59 -0
  15. examples/blog_posts/gepa/configs/hover_gepa_qwen.toml +36 -0
  16. examples/blog_posts/gepa/configs/hover_mipro_local.toml +53 -0
  17. examples/blog_posts/gepa/configs/ifbench_gepa_local.toml +59 -0
  18. examples/blog_posts/gepa/configs/ifbench_gepa_qwen.toml +36 -0
  19. examples/blog_posts/gepa/configs/ifbench_mipro_local.toml +53 -0
  20. examples/blog_posts/gepa/configs/pupa_gepa_local.toml +60 -0
  21. examples/blog_posts/gepa/configs/pupa_mipro_local.toml +54 -0
  22. examples/blog_posts/gepa/deploy_banking77_task_app.sh +41 -0
  23. examples/blog_posts/gepa/gepa_baseline.py +204 -0
  24. examples/blog_posts/gepa/query_prompts_example.py +97 -0
  25. examples/blog_posts/gepa/run_gepa_banking77.sh +87 -0
  26. examples/blog_posts/gepa/task_apps.py +105 -0
  27. examples/blog_posts/gepa/test_gepa_local.sh +67 -0
  28. examples/blog_posts/gepa/verify_banking77_setup.sh +123 -0
  29. examples/blog_posts/pokemon_vl/README.md +98 -0
  30. examples/blog_posts/pokemon_vl/configs/eval_gpt5nano.toml +26 -0
  31. examples/blog_posts/pokemon_vl/configs/eval_qwen3_vl.toml +27 -0
  32. examples/blog_posts/pokemon_vl/configs/eval_rl_final.toml +24 -0
  33. examples/blog_posts/pokemon_vl/configs/filter_high_reward.toml +10 -0
  34. examples/blog_posts/pokemon_vl/configs/train_rl_from_sft.toml +43 -0
  35. examples/blog_posts/pokemon_vl/configs/train_sft_qwen4b_vl.toml +40 -0
  36. examples/blog_posts/pokemon_vl/extract_images.py +239 -0
  37. examples/blog_posts/pokemon_vl/pokemon_vl_baseline.py +326 -0
  38. examples/blog_posts/pokemon_vl/run_eval_extract_images.py +209 -0
  39. examples/blog_posts/pokemon_vl/run_qwen_eval_extract_images.py +212 -0
  40. examples/blog_posts/pokemon_vl/text_box_analysis.md +106 -0
  41. examples/blog_posts/warming_up_to_rl/ARCHITECTURE.md +195 -0
  42. examples/blog_posts/warming_up_to_rl/FINAL_TEST_RESULTS.md +127 -0
  43. examples/blog_posts/warming_up_to_rl/INFERENCE_SUCCESS.md +132 -0
  44. examples/blog_posts/warming_up_to_rl/README.md +158 -0
  45. examples/blog_posts/warming_up_to_rl/SMOKE_TESTING.md +164 -0
  46. examples/blog_posts/warming_up_to_rl/SMOKE_TEST_COMPLETE.md +253 -0
  47. examples/blog_posts/warming_up_to_rl/configs/eval_baseline_qwen32b_10x20.toml +25 -0
  48. examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b.toml +25 -0
  49. examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b_10x20.toml +26 -0
  50. examples/blog_posts/warming_up_to_rl/configs/eval_groq_qwen32b.toml +25 -0
  51. examples/blog_posts/warming_up_to_rl/configs/eval_openai_gpt_oss_120b.toml +29 -0
  52. examples/blog_posts/warming_up_to_rl/configs/filter_high_reward_dataset.toml +10 -0
  53. examples/blog_posts/warming_up_to_rl/configs/smoke_test.toml +75 -0
  54. examples/blog_posts/warming_up_to_rl/configs/train_rl_from_sft.toml +91 -0
  55. examples/blog_posts/warming_up_to_rl/configs/train_sft_qwen4b.toml +40 -0
  56. examples/blog_posts/warming_up_to_rl/warming_up_to_rl_baseline.py +187 -0
  57. examples/dev/qwen3_32b_qlora_4xh100.toml +5 -0
  58. examples/multi_step/configs/VERILOG_REWARDS.md +4 -0
  59. examples/multi_step/configs/VERILOG_RL_CHECKLIST.md +4 -0
  60. examples/multi_step/configs/crafter_rl_outcome.toml +2 -1
  61. examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +65 -107
  62. examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +2 -1
  63. examples/multi_step/configs/crafter_rl_stepwise_simple.toml +2 -1
  64. examples/multi_step/configs/crafter_rl_stepwise_simple_NEW_FORMAT.toml +105 -0
  65. examples/multi_step/configs/verilog_rl_lora.toml +80 -123
  66. examples/qwen_coder/configs/coder_lora_30b.toml +1 -3
  67. examples/qwen_coder/configs/coder_lora_4b.toml +4 -1
  68. examples/qwen_coder/configs/coder_lora_small.toml +1 -3
  69. examples/qwen_vl/README.md +10 -12
  70. examples/qwen_vl/SETUP_COMPLETE.md +7 -8
  71. examples/qwen_vl/VISION_TESTS_COMPLETE.md +2 -3
  72. examples/qwen_vl/collect_data_via_cli.md +76 -84
  73. examples/qwen_vl/collect_vision_traces.py +4 -4
  74. examples/qwen_vl/configs/crafter_rl_vision_qwen3vl4b.toml +40 -57
  75. examples/qwen_vl/configs/crafter_vlm_sft_example.toml +1 -2
  76. examples/qwen_vl/configs/eval_gpt4o_mini_vision.toml +20 -37
  77. examples/qwen_vl/configs/eval_gpt5nano_vision.toml +21 -40
  78. examples/qwen_vl/configs/eval_qwen3vl_vision.toml +26 -0
  79. examples/qwen_vl/configs/{filter_qwen2vl_sft.toml → filter_qwen3vl_sft.toml} +4 -5
  80. examples/qwen_vl/configs/filter_vision_sft.toml +2 -3
  81. examples/qwen_vl/crafter_qwen_vl_agent.py +5 -5
  82. examples/qwen_vl/run_vision_comparison.sh +6 -7
  83. examples/rl/README.md +5 -5
  84. examples/rl/configs/rl_from_base_qwen.toml +26 -1
  85. examples/rl/configs/rl_from_base_qwen17.toml +6 -2
  86. examples/rl/task_app/README.md +1 -2
  87. examples/rl/task_app/math_single_step.py +2 -2
  88. examples/run_crafter_demo.sh +2 -2
  89. examples/sft/README.md +1 -1
  90. examples/sft/configs/crafter_fft_qwen0p6b.toml +4 -1
  91. examples/sft/configs/crafter_lora_qwen0p6b.toml +4 -1
  92. examples/swe/task_app/README.md +32 -2
  93. examples/swe/task_app/grpo_swe_mini.py +4 -0
  94. examples/swe/task_app/hosted/envs/crafter/react_agent.py +1 -1
  95. examples/swe/task_app/hosted/envs/mini_swe/environment.py +37 -10
  96. examples/swe/task_app/hosted/inference/openai_client.py +4 -38
  97. examples/swe/task_app/hosted/policy_routes.py +17 -0
  98. examples/swe/task_app/hosted/rollout.py +4 -2
  99. examples/swe/task_app/morph_backend.py +178 -0
  100. examples/task_apps/banking77/__init__.py +6 -0
  101. examples/task_apps/banking77/banking77_task_app.py +841 -0
  102. examples/task_apps/banking77/deploy_wrapper.py +46 -0
  103. examples/task_apps/crafter/CREATE_SFT_DATASET.md +4 -0
  104. examples/task_apps/crafter/FILTER_COMMAND_STATUS.md +4 -0
  105. examples/task_apps/crafter/FILTER_COMMAND_SUCCESS.md +4 -0
  106. examples/task_apps/crafter/task_app/README.md +1 -1
  107. examples/task_apps/crafter/task_app/grpo_crafter.py +90 -5
  108. examples/task_apps/crafter/task_app/grpo_crafter_task_app.py +1 -1
  109. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/policy.py +4 -26
  110. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/react_agent.py +1 -2
  111. examples/task_apps/crafter/task_app/synth_envs_hosted/hosted_app.py +49 -0
  112. examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +372 -107
  113. examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +81 -12
  114. examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +82 -11
  115. examples/task_apps/crafter/task_app/synth_envs_hosted/utils.py +194 -1
  116. examples/task_apps/enron/task_app/grpo_enron_task_app.py +1 -1
  117. examples/task_apps/gepa_benchmarks/__init__.py +7 -0
  118. examples/task_apps/gepa_benchmarks/common.py +260 -0
  119. examples/task_apps/gepa_benchmarks/hotpotqa_task_app.py +507 -0
  120. examples/task_apps/gepa_benchmarks/hover_task_app.py +436 -0
  121. examples/task_apps/gepa_benchmarks/ifbench_task_app.py +563 -0
  122. examples/task_apps/gepa_benchmarks/pupa_task_app.py +460 -0
  123. examples/task_apps/math/README.md +1 -2
  124. examples/task_apps/pokemon_red/README.md +3 -4
  125. examples/task_apps/pokemon_red/README_IMAGE_ONLY_EVAL.md +4 -0
  126. examples/task_apps/pokemon_red/eval_image_only_gpt4o.toml +6 -5
  127. examples/task_apps/pokemon_red/eval_pokemon_red_policy.py +1 -2
  128. examples/task_apps/pokemon_red/task_app.py +288 -39
  129. examples/task_apps/sokoban/README.md +2 -3
  130. examples/task_apps/verilog/eval_groq_qwen32b.toml +12 -14
  131. examples/task_apps/verilog/task_app/grpo_verilog_task_app.py +1 -1
  132. examples/vlm/configs/crafter_vlm_gpt4o.toml +4 -1
  133. examples/warming_up_to_rl/configs/crafter_fft.toml +4 -1
  134. examples/warming_up_to_rl/configs/crafter_fft_4b.toml +0 -2
  135. examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +3 -2
  136. examples/warming_up_to_rl/run_local_rollout_traced.py +1 -1
  137. examples/warming_up_to_rl/task_app/README.md +1 -1
  138. examples/warming_up_to_rl/task_app/grpo_crafter.py +185 -5
  139. examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +1 -1
  140. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +3 -27
  141. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +1 -1
  142. examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +49 -0
  143. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +156 -45
  144. examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +37 -4
  145. examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +33 -3
  146. examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +67 -0
  147. examples/workflows/math_rl/configs/rl_from_base_qwen.toml +27 -0
  148. examples/workflows/math_rl/configs/rl_from_base_qwen17.toml +6 -0
  149. synth_ai/api/train/builders.py +99 -4
  150. synth_ai/api/train/cli.py +516 -26
  151. synth_ai/api/train/config_finder.py +13 -2
  152. synth_ai/api/train/configs/__init__.py +23 -2
  153. synth_ai/api/train/configs/prompt_learning.py +442 -0
  154. synth_ai/api/train/configs/rl.py +61 -7
  155. synth_ai/api/train/configs/sft.py +6 -2
  156. synth_ai/api/train/configs/shared.py +59 -2
  157. synth_ai/api/train/task_app.py +1 -1
  158. synth_ai/api/train/validators.py +277 -0
  159. synth_ai/auth/credentials.py +119 -0
  160. synth_ai/baseline/__init__.py +25 -0
  161. synth_ai/baseline/config.py +209 -0
  162. synth_ai/baseline/discovery.py +214 -0
  163. synth_ai/baseline/execution.py +146 -0
  164. synth_ai/cli/__init__.py +94 -18
  165. synth_ai/cli/__main__.py +0 -0
  166. synth_ai/cli/claude.py +70 -0
  167. synth_ai/cli/codex.py +84 -0
  168. synth_ai/cli/commands/__init__.py +18 -0
  169. synth_ai/cli/commands/baseline/__init__.py +12 -0
  170. synth_ai/cli/commands/baseline/core.py +637 -0
  171. synth_ai/cli/commands/baseline/list.py +93 -0
  172. synth_ai/cli/commands/demo/__init__.py +6 -0
  173. synth_ai/cli/commands/demo/core.py +163 -0
  174. synth_ai/cli/commands/eval/__init__.py +19 -0
  175. synth_ai/cli/commands/eval/core.py +1112 -0
  176. synth_ai/cli/commands/eval/errors.py +81 -0
  177. synth_ai/cli/commands/eval/validation.py +133 -0
  178. synth_ai/cli/commands/filter/__init__.py +12 -0
  179. synth_ai/cli/commands/filter/core.py +424 -0
  180. synth_ai/cli/commands/filter/errors.py +55 -0
  181. synth_ai/cli/commands/filter/validation.py +77 -0
  182. synth_ai/cli/commands/help/__init__.py +177 -0
  183. synth_ai/cli/commands/help/core.py +72 -0
  184. synth_ai/cli/commands/smoke/__init__.py +7 -0
  185. synth_ai/cli/commands/smoke/core.py +1436 -0
  186. synth_ai/cli/commands/status/__init__.py +64 -0
  187. synth_ai/cli/commands/status/client.py +192 -0
  188. synth_ai/cli/commands/status/config.py +92 -0
  189. synth_ai/cli/commands/status/errors.py +20 -0
  190. synth_ai/cli/commands/status/formatters.py +164 -0
  191. synth_ai/cli/commands/status/subcommands/__init__.py +9 -0
  192. synth_ai/cli/commands/status/subcommands/files.py +79 -0
  193. synth_ai/cli/commands/status/subcommands/jobs.py +334 -0
  194. synth_ai/cli/commands/status/subcommands/models.py +79 -0
  195. synth_ai/cli/commands/status/subcommands/pricing.py +22 -0
  196. synth_ai/cli/commands/status/subcommands/runs.py +81 -0
  197. synth_ai/cli/commands/status/subcommands/summary.py +47 -0
  198. synth_ai/cli/commands/status/subcommands/usage.py +203 -0
  199. synth_ai/cli/commands/status/utils.py +114 -0
  200. synth_ai/cli/commands/train/__init__.py +53 -0
  201. synth_ai/cli/commands/train/core.py +21 -0
  202. synth_ai/cli/commands/train/errors.py +117 -0
  203. synth_ai/cli/commands/train/judge_schemas.py +200 -0
  204. synth_ai/cli/commands/train/judge_validation.py +305 -0
  205. synth_ai/cli/commands/train/validation.py +386 -0
  206. synth_ai/cli/demo.py +30 -158
  207. synth_ai/cli/deploy/__init__.py +43 -0
  208. synth_ai/cli/deploy.py +162 -0
  209. synth_ai/cli/eval/__init__.py +36 -0
  210. synth_ai/cli/eval/core.py +5 -0
  211. synth_ai/cli/eval/errors.py +31 -0
  212. synth_ai/cli/eval/validation.py +5 -0
  213. synth_ai/cli/filter/__init__.py +28 -0
  214. synth_ai/cli/filter/core.py +5 -0
  215. synth_ai/cli/filter/errors.py +23 -0
  216. synth_ai/cli/filter/validation.py +5 -0
  217. synth_ai/cli/legacy_root_backup.py +14 -8
  218. synth_ai/cli/modal_serve/__init__.py +12 -0
  219. synth_ai/cli/modal_serve/core.py +14 -0
  220. synth_ai/cli/modal_serve/errors.py +8 -0
  221. synth_ai/cli/modal_serve/validation.py +11 -0
  222. synth_ai/cli/opencode.py +107 -0
  223. synth_ai/cli/root.py +9 -5
  224. synth_ai/cli/serve/__init__.py +12 -0
  225. synth_ai/cli/serve/core.py +14 -0
  226. synth_ai/cli/serve/errors.py +8 -0
  227. synth_ai/cli/serve/validation.py +11 -0
  228. synth_ai/cli/setup.py +20 -265
  229. synth_ai/cli/status.py +7 -126
  230. synth_ai/cli/task_app_deploy.py +1 -10
  231. synth_ai/cli/task_app_modal_serve.py +4 -9
  232. synth_ai/cli/task_app_serve.py +4 -11
  233. synth_ai/cli/task_apps.py +51 -1480
  234. synth_ai/cli/train/__init__.py +12 -0
  235. synth_ai/cli/train/core.py +21 -0
  236. synth_ai/cli/train/errors.py +8 -0
  237. synth_ai/cli/train/validation.py +24 -0
  238. synth_ai/cli/train.py +1 -14
  239. synth_ai/demos/crafter/grpo_crafter_task_app.py +1 -1
  240. synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +1 -1
  241. synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +7 -4
  242. synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +9 -5
  243. synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +4 -3
  244. synth_ai/environments/examples/red/engine.py +33 -12
  245. synth_ai/environments/examples/red/engine_helpers/reward_components.py +151 -179
  246. synth_ai/environments/examples/red/environment.py +26 -0
  247. synth_ai/environments/examples/red/trace_hooks_v3.py +168 -0
  248. synth_ai/http.py +12 -0
  249. synth_ai/judge_schemas.py +10 -10
  250. synth_ai/learning/__init__.py +10 -0
  251. synth_ai/learning/prompt_learning_client.py +276 -0
  252. synth_ai/learning/prompt_learning_types.py +184 -0
  253. synth_ai/learning/rl/client.py +3 -1
  254. synth_ai/pricing/__init__.py +2 -0
  255. synth_ai/pricing/model_pricing.py +57 -0
  256. synth_ai/streaming/__init__.py +29 -0
  257. synth_ai/streaming/config.py +94 -0
  258. synth_ai/streaming/handlers.py +518 -0
  259. synth_ai/streaming/streamer.py +320 -0
  260. synth_ai/streaming/types.py +95 -0
  261. synth_ai/task/apps/__init__.py +1 -0
  262. synth_ai/task/config.py +2 -0
  263. synth_ai/task/tracing_utils.py +25 -25
  264. synth_ai/task/validators.py +45 -9
  265. synth_ai/task_app_cfgs.py +21 -0
  266. synth_ai/tracing_v3/config.py +162 -19
  267. synth_ai/tracing_v3/constants.py +1 -1
  268. synth_ai/tracing_v3/db_config.py +24 -38
  269. synth_ai/tracing_v3/migration_helper.py +1 -2
  270. synth_ai/tracing_v3/storage/config.py +47 -13
  271. synth_ai/tracing_v3/storage/factory.py +3 -3
  272. synth_ai/tracing_v3/turso/daemon.py +113 -11
  273. synth_ai/tracing_v3/turso/native_manager.py +92 -16
  274. synth_ai/types.py +8 -0
  275. synth_ai/urls.py +11 -0
  276. synth_ai/utils/__init__.py +30 -1
  277. synth_ai/utils/agents.py +74 -0
  278. synth_ai/utils/bin.py +39 -0
  279. synth_ai/utils/cli.py +149 -5
  280. synth_ai/utils/env.py +40 -33
  281. synth_ai/utils/http.py +4 -1
  282. synth_ai/utils/json.py +72 -0
  283. synth_ai/utils/modal.py +285 -3
  284. synth_ai/utils/paths.py +48 -0
  285. synth_ai/utils/uvicorn.py +113 -0
  286. {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/METADATA +109 -6
  287. {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/RECORD +291 -142
  288. examples/qwen_vl/configs/eval_qwen2vl_vision.toml +0 -44
  289. synth_ai/cli/tui.py +0 -62
  290. synth_ai/tui/__init__.py +0 -5
  291. synth_ai/tui/__main__.py +0 -13
  292. synth_ai/tui/cli/__init__.py +0 -1
  293. synth_ai/tui/cli/query_experiments.py +0 -164
  294. synth_ai/tui/cli/query_experiments_v3.py +0 -164
  295. synth_ai/tui/dashboard.py +0 -911
  296. {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/WHEEL +0 -0
  297. {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/entry_points.txt +0 -0
  298. {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/licenses/LICENSE +0 -0
  299. {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/top_level.txt +0 -0
synth_ai/cli/deploy.py ADDED
@@ -0,0 +1,162 @@
1
+ from pathlib import Path
2
+ from types import SimpleNamespace
3
+ from typing import Literal, TypeAlias, get_args
4
+
5
+ import click
6
+ from synth_ai.task_app_cfgs import LocalTaskAppConfig, ModalTaskAppConfig
7
+ from synth_ai.utils.cli import PromptedChoiceOption, PromptedChoiceType, PromptedPathOption
8
+ from synth_ai.utils.modal import deploy_modal_app, get_default_modal_bin_path
9
+ from synth_ai.utils.uvicorn import deploy_uvicorn_app
10
+
11
+ RuntimeType: TypeAlias = Literal[
12
+ "local",
13
+ "modal"
14
+ ]
15
+ RUNTIMES = get_args(RuntimeType)
16
+
17
+ MODAL_RUNTIME_OPTIONS = [
18
+ "task_app_name",
19
+ "cmd_arg",
20
+ "modal_bin_path",
21
+ "dry_run",
22
+ "modal_app_path",
23
+ ]
24
+ LOCAL_RUNTIME_OPTIONS = [
25
+ "trace",
26
+ "host",
27
+ "port"
28
+ ]
29
+
30
+ RUNTIME_MSG = SimpleNamespace(
31
+ init="[deploy]",
32
+ local="[deploy --runtime local]",
33
+ modal="[deploy --runtime modal]",
34
+ )
35
+
36
+
37
+ @click.command("deploy")
38
+ # --- Required options ---
39
+ @click.option(
40
+ "--task-app",
41
+ "task_app_path",
42
+ cls=PromptedPathOption,
43
+ type=click.Path(
44
+ exists=True,
45
+ dir_okay=False,
46
+ file_okay=True,
47
+ path_type=Path
48
+ ),
49
+ file_type=".py",
50
+ help=f"{RUNTIME_MSG.init} Enter the path to your task app",
51
+ )
52
+ @click.option(
53
+ "--runtime",
54
+ "runtime",
55
+ cls=PromptedChoiceOption,
56
+ type=PromptedChoiceType(RUNTIMES),
57
+ required=True
58
+ )
59
+ # --- Local-only options ---
60
+ @click.option(
61
+ "--trace/--no-trace",
62
+ "trace",
63
+ default=True,
64
+ help=f"{RUNTIME_MSG.local} Enable or disable trace output"
65
+ )
66
+ @click.option(
67
+ "--host",
68
+ "host",
69
+ default="127.0.0.1",
70
+ help=f"{RUNTIME_MSG.local} Host to bind to"
71
+ )
72
+ @click.option(
73
+ "--port",
74
+ "port",
75
+ default=8000,
76
+ type=int,
77
+ help=f"{RUNTIME_MSG.local} Port to bind to"
78
+ )
79
+ # --- Modal-only options ---
80
+ @click.option(
81
+ "--modal-app",
82
+ "modal_app_path",
83
+ cls=PromptedPathOption,
84
+ type=click.Path(
85
+ exists=True,
86
+ dir_okay=False,
87
+ file_okay=True,
88
+ path_type=Path
89
+ ),
90
+ file_type=".py",
91
+ prompt_guard=lambda ctx: (ctx.params.get("runtime") != "local"),
92
+ help=f"{RUNTIME_MSG.modal} Enter the path to your Modal app",
93
+ )
94
+ @click.option(
95
+ "--name",
96
+ "task_app_name",
97
+ default=None,
98
+ help=f"{RUNTIME_MSG.modal} Override Modal app name"
99
+ )
100
+ @click.option(
101
+ "--modal-mode",
102
+ "cmd_arg",
103
+ default="deploy",
104
+ help=f"{RUNTIME_MSG.modal} Mode: deploy or serve"
105
+ )
106
+ @click.option(
107
+ "--modal-cli",
108
+ "modal_bin_path",
109
+ type=click.Path(
110
+ dir_okay=False,
111
+ file_okay=True,
112
+ exists=True,
113
+ path_type=Path
114
+ ),
115
+ default=None,
116
+ help=f"{RUNTIME_MSG.modal} Path to Modal CLI",
117
+ )
118
+ @click.option(
119
+ "--dry-run",
120
+ "dry_run",
121
+ is_flag=True,
122
+ help=f"{RUNTIME_MSG.modal} Print Modal command without executing"
123
+ )
124
+ @click.option(
125
+ "--env-file",
126
+ "env_file",
127
+ multiple=True,
128
+ type=click.Path(exists=True),
129
+ help="Path to .env file(s) to load"
130
+ )
131
+ def deploy_cmd(
132
+ task_app_path: Path,
133
+ runtime: RuntimeType,
134
+ env_file: tuple[str, ...],
135
+ **kwargs
136
+ ) -> None:
137
+ """Deploy a task app to local or Modal runtime."""
138
+ match runtime:
139
+ case "local":
140
+ opts = {k: v for k, v in kwargs.items() if k in LOCAL_RUNTIME_OPTIONS}
141
+ deploy_uvicorn_app(LocalTaskAppConfig(**opts, task_app_path=task_app_path))
142
+
143
+ case "modal":
144
+ opts = {k: v for k, v in kwargs.items() if k in MODAL_RUNTIME_OPTIONS}
145
+
146
+ if "modal_app_path" not in opts or opts["modal_app_path"] is None:
147
+ raise click.ClickException("Modal app path required")
148
+
149
+ if opts["cmd_arg"] == "serve" and opts["dry_run"] is True:
150
+ raise click.ClickException("--modal-mode=serve cannot be combined with --dry-run")
151
+
152
+ modal_bin_path = opts.get("modal_bin_path") or get_default_modal_bin_path()
153
+ if not modal_bin_path:
154
+ raise click.ClickException(
155
+ "Modal CLI not found. Install the `modal` package or pass --modal-cli with its path."
156
+ )
157
+ if isinstance(modal_bin_path, str):
158
+ modal_bin_path = Path(modal_bin_path)
159
+ opts["modal_bin_path"] = modal_bin_path
160
+ deploy_modal_app(ModalTaskAppConfig(**opts, task_app_path=task_app_path))
161
+
162
+ __all__ = ["deploy_cmd"]
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from .core import command, get_command
4
+ from .errors import (
5
+ EvalCliError,
6
+ EvalConfigNotFoundError,
7
+ EvalConfigParseError,
8
+ InvalidEvalConfigError,
9
+ MetadataFilterFormatError,
10
+ MetadataSQLExecutionError,
11
+ MetadataSQLResultError,
12
+ MissingEvalTableError,
13
+ NoSeedsMatchedError,
14
+ SeedParseError,
15
+ TaskInfoUnavailableError,
16
+ TomlUnavailableError,
17
+ )
18
+ from .validation import validate_eval_options
19
+
20
+ __all__ = [
21
+ "command",
22
+ "get_command",
23
+ "EvalCliError",
24
+ "TomlUnavailableError",
25
+ "EvalConfigNotFoundError",
26
+ "EvalConfigParseError",
27
+ "MissingEvalTableError",
28
+ "InvalidEvalConfigError",
29
+ "SeedParseError",
30
+ "MetadataFilterFormatError",
31
+ "TaskInfoUnavailableError",
32
+ "NoSeedsMatchedError",
33
+ "MetadataSQLExecutionError",
34
+ "MetadataSQLResultError",
35
+ "validate_eval_options",
36
+ ]
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from synth_ai.cli.commands.eval.core import command, get_command
4
+
5
+ __all__ = ["command", "get_command"]
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from synth_ai.cli.commands.eval.errors import (
4
+ EvalCliError,
5
+ EvalConfigNotFoundError,
6
+ EvalConfigParseError,
7
+ InvalidEvalConfigError,
8
+ MetadataFilterFormatError,
9
+ MetadataSQLExecutionError,
10
+ MetadataSQLResultError,
11
+ MissingEvalTableError,
12
+ NoSeedsMatchedError,
13
+ SeedParseError,
14
+ TaskInfoUnavailableError,
15
+ TomlUnavailableError,
16
+ )
17
+
18
+ __all__ = [
19
+ "EvalCliError",
20
+ "TomlUnavailableError",
21
+ "EvalConfigNotFoundError",
22
+ "EvalConfigParseError",
23
+ "MissingEvalTableError",
24
+ "InvalidEvalConfigError",
25
+ "SeedParseError",
26
+ "MetadataFilterFormatError",
27
+ "TaskInfoUnavailableError",
28
+ "NoSeedsMatchedError",
29
+ "MetadataSQLExecutionError",
30
+ "MetadataSQLResultError",
31
+ ]
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from synth_ai.cli.commands.eval.validation import validate_eval_options
4
+
5
+ __all__ = ["validate_eval_options"]
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from .core import command, get_command
4
+ from .errors import (
5
+ FilterCliError,
6
+ FilterConfigNotFoundError,
7
+ FilterConfigParseError,
8
+ InvalidFilterConfigError,
9
+ MissingFilterTableError,
10
+ NoSessionsMatchedError,
11
+ NoTracesFoundError,
12
+ TomlUnavailableError,
13
+ )
14
+ from .validation import validate_filter_options
15
+
16
+ __all__ = [
17
+ "command",
18
+ "get_command",
19
+ "FilterCliError",
20
+ "TomlUnavailableError",
21
+ "FilterConfigNotFoundError",
22
+ "FilterConfigParseError",
23
+ "MissingFilterTableError",
24
+ "InvalidFilterConfigError",
25
+ "NoTracesFoundError",
26
+ "NoSessionsMatchedError",
27
+ "validate_filter_options",
28
+ ]
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from synth_ai.cli.commands.filter.core import command, get_command
4
+
5
+ __all__ = ["command", "get_command"]
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from synth_ai.cli.commands.filter.errors import (
4
+ FilterCliError,
5
+ FilterConfigNotFoundError,
6
+ FilterConfigParseError,
7
+ InvalidFilterConfigError,
8
+ MissingFilterTableError,
9
+ NoSessionsMatchedError,
10
+ NoTracesFoundError,
11
+ TomlUnavailableError,
12
+ )
13
+
14
+ __all__ = [
15
+ "FilterCliError",
16
+ "TomlUnavailableError",
17
+ "FilterConfigNotFoundError",
18
+ "FilterConfigParseError",
19
+ "MissingFilterTableError",
20
+ "InvalidFilterConfigError",
21
+ "NoTracesFoundError",
22
+ "NoSessionsMatchedError",
23
+ ]
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from synth_ai.cli.commands.filter.validation import validate_filter_options
4
+
5
+ __all__ = ["validate_filter_options"]
@@ -253,7 +253,7 @@ def view(url: str):
253
253
 
254
254
  @cli.command()
255
255
  @click.option("--db-file", default="synth_ai.db", help="Database file path")
256
- @click.option("--sqld-port", default=8080, type=int, help="Port for sqld HTTP interface")
256
+ @click.option("--sqld-port", default=8080, type=int, help="Port for sqld Hrana WebSocket interface (HTTP API will be port+1)")
257
257
  @click.option("--env-port", default=8901, type=int, help="Port for environment service")
258
258
  @click.option("--no-sqld", is_flag=True, help="Skip starting sqld daemon")
259
259
  @click.option("--no-env", is_flag=True, help="Skip starting environment service")
@@ -298,32 +298,37 @@ def serve(
298
298
 
299
299
  # Start sqld if requested
300
300
  if not no_sqld:
301
+ hrana_port = sqld_port
302
+ http_port = sqld_port + 1
301
303
  # Check if sqld is already running
302
304
  try:
303
305
  result = subprocess.run(
304
- ["pgrep", "-f", f"sqld.*--http-listen-addr.*:{sqld_port}"],
306
+ ["pgrep", "-f", f"sqld.*(--hrana-listen-addr.*:{hrana_port}|--http-listen-addr.*:{http_port})"],
305
307
  capture_output=True,
306
308
  text=True,
307
309
  )
308
310
  if result.returncode == 0:
309
- click.echo(f"✅ sqld already running on port {sqld_port}")
311
+ click.echo(f"✅ sqld already running on hrana port {hrana_port}, HTTP API port {http_port}")
310
312
  click.echo(f" Database: {db_file}")
311
- click.echo(f" HTTP API: http://127.0.0.1:{sqld_port}")
313
+ click.echo(f" libsql: libsql://127.0.0.1:{hrana_port}")
314
+ click.echo(f" HTTP API: http://127.0.0.1:{http_port}")
312
315
  else:
313
316
  # Find or install sqld
314
317
  sqld_bin = find_sqld_binary()
315
318
  if not sqld_bin:
316
319
  sqld_bin = install_sqld()
317
320
 
318
- click.echo(f"🗄️ Starting sqld (local only) on port {sqld_port}")
321
+ click.echo(f"🗄️ Starting sqld (local only) on hrana port {hrana_port}, HTTP API port {http_port}")
319
322
 
320
323
  # Start sqld
321
324
  sqld_cmd = [
322
325
  sqld_bin,
323
326
  "--db-path",
324
327
  db_file,
328
+ "--hrana-listen-addr",
329
+ f"127.0.0.1:{hrana_port}",
325
330
  "--http-listen-addr",
326
- f"127.0.0.1:{sqld_port}",
331
+ f"127.0.0.1:{http_port}",
327
332
  ]
328
333
 
329
334
  # Create log file
@@ -346,7 +351,8 @@ def serve(
346
351
 
347
352
  click.echo("✅ sqld started successfully!")
348
353
  click.echo(f" Database: {db_file}")
349
- click.echo(f" HTTP API: http://127.0.0.1:{sqld_port}")
354
+ click.echo(f" libsql: libsql://127.0.0.1:{hrana_port}")
355
+ click.echo(f" HTTP API: http://127.0.0.1:{http_port}")
350
356
  click.echo(f" Log file: {os.path.abspath('sqld.log')}")
351
357
 
352
358
  except FileNotFoundError:
@@ -417,7 +423,7 @@ def serve(
417
423
  click.echo(f" Working directory: {os.getcwd()}")
418
424
  click.echo("")
419
425
  click.echo("🔄 Starting services...")
420
- click.echo(f" - sqld daemon: http://127.0.0.1:{sqld_port}")
426
+ click.echo(f" - sqld daemon: libsql://127.0.0.1:{sqld_port} (HTTP API: http://127.0.0.1:{sqld_port + 1})")
421
427
  click.echo(f" - Environment service: http://127.0.0.1:{env_port}")
422
428
  click.echo("")
423
429
  click.echo("💡 Tips:")
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from .core import command, get_command
4
+ from .errors import ModalServeCliError
5
+ from .validation import validate_modal_serve_options
6
+
7
+ __all__ = [
8
+ "command",
9
+ "get_command",
10
+ "ModalServeCliError",
11
+ "validate_modal_serve_options",
12
+ ]
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ import click
4
+ from synth_ai.cli.task_apps import task_app_group
5
+
6
+ __all__ = ["command", "get_command"]
7
+
8
+ command = task_app_group.commands.get("modal-serve")
9
+
10
+
11
+ def get_command() -> click.Command:
12
+ if command is None:
13
+ raise RuntimeError("modal-serve command is not registered on task_app_group")
14
+ return command
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class ModalServeCliError(RuntimeError):
5
+ """Base exception for modal-serve CLI failures."""
6
+
7
+
8
+ __all__ = ["ModalServeCliError"]
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import MutableMapping
4
+ from typing import Any
5
+
6
+ __all__ = ["validate_modal_serve_options"]
7
+
8
+
9
+ def validate_modal_serve_options(options: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
10
+ """Validate parameters passed to the modal-serve CLI command."""
11
+ return options
@@ -0,0 +1,107 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+
4
+ import click
5
+ from synth_ai.types import MODEL_NAMES, ModelName
6
+ from synth_ai.urls import BACKEND_URL_SYNTH_RESEARCH_BASE
7
+ from synth_ai.utils import (
8
+ create_and_write_json,
9
+ find_bin_path,
10
+ install_bin,
11
+ load_json_to_dict,
12
+ resolve_env_var,
13
+ verify_bin,
14
+ write_agents_md,
15
+ )
16
+
17
+ CONFIG_PATH = Path.home() / ".config" / "opencode" / "opencode.json"
18
+ AUTH_PATH = Path.home() / ".local" / "share" / "opencode" / "auth.json"
19
+ SYNTH_PROVIDER_ID = "synth"
20
+
21
+
22
+ @click.command("opencode")
23
+ @click.option(
24
+ "--model",
25
+ "model_name",
26
+ type=str,
27
+ default=None
28
+ )
29
+ @click.option(
30
+ "--force",
31
+ is_flag=True,
32
+ help="Prompt for API keys even if cached values exist."
33
+ )
34
+ @click.option(
35
+ "--url",
36
+ "override_url",
37
+ type=str,
38
+ default=None,
39
+ )
40
+ def opencode_cmd(
41
+ model_name: ModelName | None = None,
42
+ force: bool = False,
43
+ override_url: str | None = None
44
+ ) -> None:
45
+
46
+ while True:
47
+ bin_path = find_bin_path("opencode")
48
+ if bin_path:
49
+ break
50
+ if not install_bin(
51
+ "OpenCode",
52
+ [
53
+ "brew install opencode",
54
+ "bun add -g opencode-ai",
55
+ "curl -fsSL https://opencode.ai/install | bash",
56
+ "npm i -g opencode-ai",
57
+ "paru -S opencode"
58
+ ]
59
+ ):
60
+ print("Failed to find your installed OpenCode")
61
+ print("Please install from: https://opencode.ai")
62
+ return
63
+ print(f"Using OpenCode at {bin_path}")
64
+
65
+ if not verify_bin(bin_path):
66
+ print("Failed to verify OpenCode is runnable")
67
+ return
68
+
69
+ write_agents_md()
70
+
71
+ if model_name is not None:
72
+ if model_name not in MODEL_NAMES:
73
+ raise ValueError(
74
+ f"model_name={model_name} is invalid. Valid values for model_name: {MODEL_NAMES}"
75
+ )
76
+ synth_api_key = resolve_env_var("SYNTH_API_KEY", override_process_env=force)
77
+ data = load_json_to_dict(AUTH_PATH)
78
+ good_entry = {
79
+ "type": "api",
80
+ "key": synth_api_key,
81
+ }
82
+ if data.get(SYNTH_PROVIDER_ID) != good_entry:
83
+ data[SYNTH_PROVIDER_ID] = good_entry
84
+ create_and_write_json(AUTH_PATH, data)
85
+ config = load_json_to_dict(CONFIG_PATH)
86
+ config.setdefault("$schema", "https://opencode.ai/config.json")
87
+ if override_url:
88
+ url = override_url
89
+ print("Using override URL:", url)
90
+ else:
91
+ url = BACKEND_URL_SYNTH_RESEARCH_BASE
92
+ provider_section = config.setdefault("provider", {})
93
+ synth_provider = provider_section.setdefault(SYNTH_PROVIDER_ID, {})
94
+ synth_provider["npm"] = "@ai-sdk/openai-compatible"
95
+ synth_provider.setdefault("name", "Synth")
96
+ models = synth_provider.setdefault("models", {})
97
+ models.setdefault(model_name, {})
98
+ options = synth_provider.setdefault("options", {})
99
+ options["baseURL"] = url
100
+ full_model_name = f"{SYNTH_PROVIDER_ID}/{model_name}"
101
+ config["model"] = full_model_name
102
+ create_and_write_json(CONFIG_PATH, config)
103
+
104
+ try:
105
+ subprocess.run([str(bin_path)], check=True)
106
+ except subprocess.CalledProcessError:
107
+ print("Failed to launch OpenCode")
synth_ai/cli/root.py CHANGED
@@ -158,7 +158,7 @@ def cli():
158
158
 
159
159
  @cli.command()
160
160
  @click.option("--db-file", default="traces/v3/synth_ai.db", help="Database file path")
161
- @click.option("--sqld-port", default=8080, type=int, help="Port for sqld HTTP interface")
161
+ @click.option("--sqld-port", default=8080, type=int, help="Port for sqld Hrana WebSocket interface (HTTP API will be port+1)")
162
162
  @click.option("--env-port", default=8901, type=int, help="Port for environment service")
163
163
  @click.option("--no-sqld", is_flag=True, help="Skip starting sqld daemon")
164
164
  @click.option("--no-env", is_flag=True, help="Skip starting environment service")
@@ -204,21 +204,25 @@ def serve_deprecated(
204
204
 
205
205
  if not no_sqld:
206
206
  try:
207
+ hrana_port = sqld_port
208
+ http_port = sqld_port + 1
207
209
  result = subprocess.run(
208
- ["pgrep", "-f", f"sqld.*--http-listen-addr.*:{sqld_port}"],
210
+ ["pgrep", "-f", f"sqld.*(--hrana-listen-addr.*:{hrana_port}|--http-listen-addr.*:{http_port})"],
209
211
  capture_output=True,
210
212
  text=True,
211
213
  )
212
214
  if result.returncode != 0:
213
215
  sqld_bin = find_sqld_binary() or install_sqld()
214
- click.echo(f"🗄️ Starting sqld (local only) on port {sqld_port}")
216
+ click.echo(f"🗄️ Starting sqld (local only) on hrana port {hrana_port}, HTTP API port {http_port}")
215
217
  proc = subprocess.Popen(
216
218
  [
217
219
  sqld_bin,
218
220
  "--db-path",
219
221
  db_file,
222
+ "--hrana-listen-addr",
223
+ f"127.0.0.1:{hrana_port}",
220
224
  "--http-listen-addr",
221
- f"127.0.0.1:{sqld_port}",
225
+ f"127.0.0.1:{http_port}",
222
226
  ],
223
227
  stdout=open("sqld.log", "w"), # noqa: SIM115
224
228
  stderr=subprocess.STDOUT,
@@ -274,7 +278,7 @@ def serve_deprecated(
274
278
  click.echo(f" Working directory: {os.getcwd()}")
275
279
  click.echo("")
276
280
  click.echo("🔄 Starting services...")
277
- click.echo(f" - sqld daemon: http://127.0.0.1:{sqld_port}")
281
+ click.echo(f" - sqld daemon: libsql://127.0.0.1:{sqld_port} (HTTP API: http://127.0.0.1:{sqld_port + 1})")
278
282
  click.echo(f" - Environment service: http://127.0.0.1:{env_port}")
279
283
  click.echo("")
280
284
  click.echo("💡 Tips:")
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from .core import command, get_command
4
+ from .errors import ServeCliError
5
+ from .validation import validate_serve_options
6
+
7
+ __all__ = [
8
+ "command",
9
+ "get_command",
10
+ "ServeCliError",
11
+ "validate_serve_options",
12
+ ]
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ import click
4
+ from synth_ai.cli.task_apps import task_app_group
5
+
6
+ __all__ = ["command", "get_command"]
7
+
8
+ command = task_app_group.commands.get("serve")
9
+
10
+
11
+ def get_command() -> click.Command:
12
+ if command is None:
13
+ raise RuntimeError("Serve command is not registered on task_app_group")
14
+ return command
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class ServeCliError(RuntimeError):
5
+ """Base exception for serve CLI failures."""
6
+
7
+
8
+ __all__ = ["ServeCliError"]
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import MutableMapping
4
+ from typing import Any
5
+
6
+ __all__ = ["validate_serve_options"]
7
+
8
+
9
+ def validate_serve_options(options: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
10
+ """Validate parameters passed to the serve CLI command."""
11
+ return options