synth-ai 0.2.9.dev4__py3-none-any.whl → 0.2.9.dev6__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 (353) hide show
  1. examples/__init__.py +16 -0
  2. examples/crafter_debug_render.py +23 -17
  3. examples/qwen_coder/README.md +102 -0
  4. examples/qwen_coder/_shared.py +113 -0
  5. examples/qwen_coder/configs/coder_lora_30b.toml +61 -0
  6. examples/qwen_coder/configs/coder_lora_4b.toml +57 -0
  7. examples/qwen_coder/configs/coder_lora_small.toml +58 -0
  8. examples/qwen_coder/generate_dataset.py +98 -0
  9. examples/qwen_coder/infer_ft_smoke.py +64 -0
  10. examples/qwen_coder/infer_prod_proxy.py +73 -0
  11. examples/qwen_coder/infer_via_synth.py +87 -0
  12. examples/qwen_coder/scripts/infer_coder.sh +18 -0
  13. examples/qwen_coder/scripts/train_coder_30b.sh +21 -0
  14. examples/qwen_coder/sft_full_17b.py +103 -0
  15. examples/qwen_coder/sft_lora_30b.py +110 -0
  16. examples/qwen_coder/subset_jsonl.py +38 -0
  17. examples/qwen_coder/validate_jsonl.py +59 -0
  18. examples/rl/configs/eval_base_qwen.toml +1 -1
  19. examples/rl/configs/rl_from_base_qwen17.toml +1 -1
  20. examples/rl/download_dataset.py +26 -10
  21. examples/rl/run_eval.py +53 -52
  22. examples/rl/run_rl_and_save.py +29 -12
  23. examples/rl/task_app/math_single_step.py +180 -41
  24. examples/rl/task_app/math_task_app.py +14 -6
  25. examples/sft/README.md +139 -0
  26. examples/sft/configs/crafter_fft_qwen0p6b.toml +44 -0
  27. examples/sft/configs/crafter_lora_qwen0p6b.toml +45 -0
  28. examples/sft/evaluate.py +117 -0
  29. examples/sft/export_dataset.py +117 -0
  30. examples/sft/generate_traces.py +162 -0
  31. examples/swe/__init__.py +12 -0
  32. examples/swe/task_app/README.md +105 -0
  33. examples/swe/task_app/__init__.py +2 -0
  34. examples/swe/task_app/grpo_swe_mini.py +571 -0
  35. examples/swe/task_app/grpo_swe_mini_task_app.py +136 -0
  36. examples/swe/task_app/hosted/README.md +173 -0
  37. examples/swe/task_app/hosted/__init__.py +5 -0
  38. examples/swe/task_app/hosted/branching.py +143 -0
  39. examples/swe/task_app/hosted/environment_routes.py +1289 -0
  40. examples/swe/task_app/hosted/envs/__init__.py +1 -0
  41. examples/swe/task_app/hosted/envs/crafter/__init__.py +6 -0
  42. examples/swe/task_app/hosted/envs/crafter/app.py +1 -0
  43. examples/swe/task_app/hosted/envs/crafter/environment.py +522 -0
  44. examples/swe/task_app/hosted/envs/crafter/policy.py +478 -0
  45. examples/swe/task_app/hosted/envs/crafter/react_agent.py +108 -0
  46. examples/swe/task_app/hosted/envs/crafter/shared.py +305 -0
  47. examples/swe/task_app/hosted/envs/crafter/tools.py +47 -0
  48. examples/swe/task_app/hosted/envs/mini_swe/__init__.py +8 -0
  49. examples/swe/task_app/hosted/envs/mini_swe/environment.py +1164 -0
  50. examples/swe/task_app/hosted/envs/mini_swe/policy.py +355 -0
  51. examples/swe/task_app/hosted/envs/mini_swe/shared.py +83 -0
  52. examples/swe/task_app/hosted/envs/mini_swe/tools.py +96 -0
  53. examples/swe/task_app/hosted/hosted_app.py +204 -0
  54. examples/swe/task_app/hosted/inference/__init__.py +5 -0
  55. examples/swe/task_app/hosted/inference/openai_client.py +618 -0
  56. examples/swe/task_app/hosted/main.py +100 -0
  57. examples/swe/task_app/hosted/policy_routes.py +1079 -0
  58. examples/swe/task_app/hosted/registry.py +195 -0
  59. examples/swe/task_app/hosted/rollout.py +1869 -0
  60. examples/swe/task_app/hosted/storage/__init__.py +5 -0
  61. examples/swe/task_app/hosted/storage/volume.py +211 -0
  62. examples/swe/task_app/hosted/test_agents.py +161 -0
  63. examples/swe/task_app/hosted/test_service.py +137 -0
  64. examples/swe/task_app/hosted/utils.py +62 -0
  65. examples/vlm/README.md +68 -0
  66. examples/vlm/configs/crafter_vlm_gpt4o.toml +44 -0
  67. examples/vlm/crafter_image_only_agent.py +207 -0
  68. examples/vlm/crafter_openai_vlm_agent.py +277 -0
  69. examples/vlm/filter_image_rows.py +63 -0
  70. examples/vlm/run_crafter_vlm_benchmark.py +316 -0
  71. examples/warming_up_to_rl/analyze_trace_db.py +12 -10
  72. examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +11 -1
  73. examples/warming_up_to_rl/export_trace_sft.py +218 -36
  74. examples/warming_up_to_rl/groq_test.py +15 -8
  75. examples/warming_up_to_rl/manage_secrets.py +29 -25
  76. examples/warming_up_to_rl/readme.md +9 -2
  77. examples/warming_up_to_rl/run_eval.py +137 -61
  78. examples/warming_up_to_rl/run_fft_and_save.py +131 -60
  79. examples/warming_up_to_rl/run_local_rollout.py +88 -39
  80. examples/warming_up_to_rl/run_local_rollout_modal.py +114 -28
  81. examples/warming_up_to_rl/run_local_rollout_parallel.py +81 -20
  82. examples/warming_up_to_rl/run_local_rollout_traced.py +126 -23
  83. examples/warming_up_to_rl/run_rl_and_save.py +35 -12
  84. examples/warming_up_to_rl/run_rollout_remote.py +44 -19
  85. examples/warming_up_to_rl/task_app/README.md +6 -2
  86. examples/warming_up_to_rl/task_app/grpo_crafter.py +319 -57
  87. examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +11 -30
  88. examples/warming_up_to_rl/task_app/synth_envs_hosted/__init__.py +1 -1
  89. examples/warming_up_to_rl/task_app/synth_envs_hosted/branching.py +9 -11
  90. examples/warming_up_to_rl/task_app/synth_envs_hosted/environment_routes.py +137 -182
  91. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/__init__.py +1 -1
  92. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/__init__.py +1 -1
  93. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/app.py +1 -1
  94. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/environment.py +150 -57
  95. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +105 -69
  96. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +19 -7
  97. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/shared.py +45 -42
  98. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/tools.py +1 -1
  99. examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +47 -45
  100. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/__init__.py +1 -1
  101. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +198 -92
  102. examples/warming_up_to_rl/task_app/synth_envs_hosted/main.py +0 -2
  103. examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +361 -263
  104. examples/warming_up_to_rl/task_app/synth_envs_hosted/registry.py +21 -23
  105. examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +394 -274
  106. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/__init__.py +1 -1
  107. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/volume.py +56 -62
  108. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_agents.py +1 -0
  109. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_service.py +6 -15
  110. examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +4 -3
  111. synth/__init__.py +14 -0
  112. synth_ai/__init__.py +20 -4
  113. synth_ai/api/models/supported.py +376 -0
  114. synth_ai/api/train/builders.py +157 -26
  115. synth_ai/api/train/cli.py +213 -57
  116. synth_ai/api/train/config_finder.py +65 -5
  117. synth_ai/api/train/env_resolver.py +33 -15
  118. synth_ai/api/train/pollers.py +13 -4
  119. synth_ai/api/train/supported_algos.py +139 -0
  120. synth_ai/api/train/task_app.py +5 -3
  121. synth_ai/api/train/utils.py +33 -48
  122. synth_ai/cli/__init__.py +19 -4
  123. synth_ai/cli/_modal_wrapper.py +28 -0
  124. synth_ai/cli/_typer_patch.py +49 -0
  125. synth_ai/cli/balance.py +2 -3
  126. synth_ai/cli/calc.py +1 -1
  127. synth_ai/cli/demo.py +21 -6
  128. synth_ai/cli/recent.py +2 -2
  129. synth_ai/cli/rl_demo.py +77 -17
  130. synth_ai/cli/root.py +116 -39
  131. synth_ai/cli/status.py +2 -2
  132. synth_ai/cli/task_apps.py +1709 -243
  133. synth_ai/cli/traces.py +7 -4
  134. synth_ai/cli/turso.py +73 -0
  135. synth_ai/cli/watch.py +12 -18
  136. synth_ai/core/experiment.py +0 -2
  137. synth_ai/demo_registry.py +68 -31
  138. synth_ai/demos/core/cli.py +516 -194
  139. synth_ai/demos/demo_task_apps/__init__.py +3 -3
  140. synth_ai/demos/demo_task_apps/core.py +64 -28
  141. synth_ai/demos/demo_task_apps/crafter/configs/crafter_fft_4b.toml +2 -3
  142. synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +37 -30
  143. synth_ai/demos/demo_task_apps/math/_common.py +1 -2
  144. synth_ai/demos/demo_task_apps/math/app.py +2 -1
  145. synth_ai/demos/demo_task_apps/math/deploy_modal.py +3 -6
  146. synth_ai/demos/demo_task_apps/math/modal_task_app.py +183 -82
  147. synth_ai/demos/demo_task_apps/math/task_app_entry.py +0 -2
  148. synth_ai/environments/examples/bandit/engine.py +12 -4
  149. synth_ai/environments/examples/bandit/taskset.py +4 -4
  150. synth_ai/environments/examples/crafter_classic/environment.py +76 -1
  151. synth_ai/environments/reproducibility/tree.py +5 -6
  152. synth_ai/environments/service/app.py +11 -12
  153. synth_ai/environments/service/core_routes.py +10 -9
  154. synth_ai/environments/stateful/engine.py +1 -1
  155. synth_ai/environments/tasks/core.py +1 -0
  156. synth_ai/environments/tasks/filters.py +5 -6
  157. synth_ai/environments/tasks/utils.py +4 -5
  158. synth_ai/evals/base.py +0 -2
  159. synth_ai/handshake.py +11 -9
  160. synth_ai/http.py +1 -1
  161. synth_ai/http_client.py +43 -11
  162. synth_ai/inference/__init__.py +0 -2
  163. synth_ai/inference/client.py +20 -6
  164. synth_ai/jobs/client.py +103 -78
  165. synth_ai/learning/__init__.py +41 -6
  166. synth_ai/learning/algorithms.py +14 -0
  167. synth_ai/learning/client.py +121 -29
  168. synth_ai/learning/config.py +2 -40
  169. synth_ai/learning/constants.py +0 -2
  170. synth_ai/learning/ft_client.py +4 -56
  171. synth_ai/learning/health.py +13 -7
  172. synth_ai/learning/jobs.py +43 -47
  173. synth_ai/{rl → learning/rl}/__init__.py +14 -5
  174. synth_ai/learning/rl/client.py +267 -0
  175. synth_ai/learning/rl/config.py +31 -0
  176. synth_ai/{rl → learning/rl}/contracts.py +5 -10
  177. synth_ai/{rl → learning/rl}/env_keys.py +45 -16
  178. synth_ai/learning/rl/secrets.py +13 -0
  179. synth_ai/learning/rl_client.py +2 -253
  180. synth_ai/learning/sft/__init__.py +29 -0
  181. synth_ai/learning/sft/client.py +68 -0
  182. synth_ai/learning/sft/config.py +270 -0
  183. synth_ai/learning/sft/data.py +295 -0
  184. synth_ai/learning/sse.py +25 -26
  185. synth_ai/learning/validators.py +25 -24
  186. synth_ai/lm/__init__.py +21 -47
  187. synth_ai/task/__init__.py +26 -27
  188. synth_ai/task/apps/__init__.py +18 -19
  189. synth_ai/task/auth.py +35 -23
  190. synth_ai/task/client.py +15 -13
  191. synth_ai/task/contracts.py +37 -35
  192. synth_ai/task/datasets.py +9 -6
  193. synth_ai/task/errors.py +11 -10
  194. synth_ai/task/health.py +17 -11
  195. synth_ai/task/json.py +58 -24
  196. synth_ai/task/proxy.py +15 -14
  197. synth_ai/task/rubrics.py +22 -15
  198. synth_ai/task/server.py +43 -17
  199. synth_ai/task/tracing_utils.py +12 -7
  200. synth_ai/task/validators.py +0 -1
  201. synth_ai/task/vendors.py +5 -7
  202. synth_ai/tracing_v3/__init__.py +2 -0
  203. synth_ai/tracing_v3/abstractions.py +21 -4
  204. synth_ai/tracing_v3/db_config.py +26 -1
  205. synth_ai/tracing_v3/decorators.py +18 -15
  206. synth_ai/tracing_v3/examples/basic_usage.py +3 -2
  207. synth_ai/tracing_v3/hooks.py +6 -4
  208. synth_ai/tracing_v3/llm_call_record_helpers.py +6 -6
  209. synth_ai/tracing_v3/replica_sync.py +1 -0
  210. synth_ai/tracing_v3/session_tracer.py +63 -16
  211. synth_ai/tracing_v3/storage/base.py +89 -1
  212. synth_ai/tracing_v3/storage/config.py +21 -8
  213. synth_ai/tracing_v3/storage/factory.py +10 -8
  214. synth_ai/tracing_v3/storage/utils.py +4 -2
  215. synth_ai/tracing_v3/turso/daemon.py +7 -2
  216. synth_ai/tracing_v3/turso/models.py +5 -2
  217. synth_ai/tracing_v3/turso/native_manager.py +1173 -0
  218. synth_ai/tracing_v3/utils.py +4 -3
  219. synth_ai/v0/api/__init__.py +8 -0
  220. synth_ai/v0/api/models/__init__.py +8 -0
  221. synth_ai/v0/api/models/supported.py +8 -0
  222. synth_ai/v0/config/__init__.py +15 -0
  223. synth_ai/v0/config/base_url.py +12 -0
  224. synth_ai/v0/lm/__init__.py +51 -0
  225. synth_ai/{lm → v0/lm}/caching/ephemeral.py +3 -5
  226. synth_ai/{lm → v0/lm}/caching/handler.py +4 -4
  227. synth_ai/{lm → v0/lm}/caching/initialize.py +1 -1
  228. synth_ai/{lm → v0/lm}/caching/persistent.py +1 -1
  229. synth_ai/{lm → v0/lm}/config.py +6 -1
  230. synth_ai/{lm → v0/lm}/core/all.py +9 -9
  231. synth_ai/{lm → v0/lm}/core/exceptions.py +0 -2
  232. synth_ai/{lm → v0/lm}/core/main.py +19 -7
  233. synth_ai/{lm → v0/lm}/core/main_v3.py +10 -10
  234. synth_ai/{lm → v0/lm}/core/synth_models.py +2 -15
  235. synth_ai/{lm → v0/lm}/core/vendor_clients.py +6 -4
  236. synth_ai/{lm → v0/lm}/overrides.py +4 -4
  237. synth_ai/{lm → v0/lm}/provider_support/anthropic.py +4 -4
  238. synth_ai/{lm → v0/lm}/provider_support/openai.py +5 -5
  239. synth_ai/{lm → v0/lm}/structured_outputs/handler.py +5 -5
  240. synth_ai/{lm → v0/lm}/structured_outputs/rehabilitate.py +1 -1
  241. synth_ai/{lm → v0/lm}/vendors/core/anthropic_api.py +16 -16
  242. synth_ai/{lm → v0/lm}/vendors/core/gemini_api.py +5 -5
  243. synth_ai/{lm → v0/lm}/vendors/core/mistral_api.py +5 -5
  244. synth_ai/{lm → v0/lm}/vendors/core/openai_api.py +12 -10
  245. synth_ai/{lm → v0/lm}/vendors/openai_standard.py +11 -9
  246. synth_ai/{lm → v0/lm}/vendors/openai_standard_responses.py +8 -5
  247. synth_ai/{lm → v0/lm}/vendors/supported/custom_endpoint.py +4 -6
  248. synth_ai/{lm → v0/lm}/vendors/supported/deepseek.py +2 -2
  249. synth_ai/{lm → v0/lm}/vendors/supported/grok.py +2 -2
  250. synth_ai/{lm → v0/lm}/vendors/supported/groq.py +1 -1
  251. synth_ai/{lm → v0/lm}/vendors/supported/ollama.py +1 -1
  252. synth_ai/{lm → v0/lm}/vendors/supported/openrouter.py +3 -3
  253. synth_ai/{lm → v0/lm}/vendors/supported/together.py +1 -1
  254. synth_ai/{lm → v0/lm}/vendors/synth_client.py +38 -11
  255. synth_ai/v0/tracing/upload.py +32 -135
  256. synth_ai/v0/tracing_v3/__init__.py +10 -0
  257. synth_ai/v0/tracing_v3/abstractions.py +3 -0
  258. synth_ai/v0/tracing_v3/decorators.py +3 -0
  259. synth_ai/v0/tracing_v3/llm_call_record_helpers.py +3 -0
  260. synth_ai/v0/tracing_v3/session_tracer.py +3 -0
  261. synth_ai-0.2.9.dev6.dist-info/METADATA +191 -0
  262. {synth_ai-0.2.9.dev4.dist-info → synth_ai-0.2.9.dev6.dist-info}/RECORD +291 -264
  263. {synth_ai-0.2.9.dev4.dist-info → synth_ai-0.2.9.dev6.dist-info}/top_level.txt +1 -0
  264. examples/common_old/backend.py +0 -21
  265. examples/evals_old/README.md +0 -98
  266. examples/evals_old/__init__.py +0 -6
  267. examples/evals_old/compare_models.py +0 -1037
  268. examples/evals_old/example_log.md +0 -145
  269. examples/evals_old/run_demo.sh +0 -126
  270. examples/evals_old/trace_analysis.py +0 -270
  271. examples/finetuning_old/_backup_synth_qwen/config.toml +0 -29
  272. examples/finetuning_old/_backup_synth_qwen/example_log.md +0 -324
  273. examples/finetuning_old/_backup_synth_qwen/filter_traces.py +0 -60
  274. examples/finetuning_old/_backup_synth_qwen/filter_traces_achievements.py +0 -239
  275. examples/finetuning_old/_backup_synth_qwen/purge_v3_traces.py +0 -109
  276. examples/finetuning_old/_backup_synth_qwen/react_agent_lm.py +0 -1924
  277. examples/finetuning_old/_backup_synth_qwen/readme.md +0 -49
  278. examples/finetuning_old/_backup_synth_qwen/run_crafter_qwen4b.py +0 -114
  279. examples/finetuning_old/_backup_synth_qwen/run_demo.sh +0 -195
  280. examples/finetuning_old/_backup_synth_qwen/sft_kickoff.py +0 -118
  281. examples/finetuning_old/synth_qwen_v1/README.md +0 -68
  282. examples/finetuning_old/synth_qwen_v1/filter_traces.py +0 -60
  283. examples/finetuning_old/synth_qwen_v1/filter_traces_achievements.py +0 -239
  284. examples/finetuning_old/synth_qwen_v1/finetune.py +0 -46
  285. examples/finetuning_old/synth_qwen_v1/hello_ft_model.py +0 -71
  286. examples/finetuning_old/synth_qwen_v1/infer.py +0 -37
  287. examples/finetuning_old/synth_qwen_v1/poll.py +0 -44
  288. examples/finetuning_old/synth_qwen_v1/prepare_data.py +0 -35
  289. examples/finetuning_old/synth_qwen_v1/purge_v3_traces.py +0 -109
  290. examples/finetuning_old/synth_qwen_v1/react_agent_lm.py +0 -1932
  291. examples/finetuning_old/synth_qwen_v1/run_crafter_sft_job.py +0 -207
  292. examples/finetuning_old/synth_qwen_v1/run_ft_job.py +0 -232
  293. examples/finetuning_old/synth_qwen_v1/upload_data.py +0 -34
  294. examples/finetuning_old/synth_qwen_v1/util.py +0 -147
  295. examples/rl_old/task_app.py +0 -962
  296. examples/warming_up_to_rl/old/event_rewards.md +0 -234
  297. examples/warming_up_to_rl/old/notes.md +0 -73
  298. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_stepwise_rewards.py +0 -58
  299. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/filter_traces_sft_turso.py +0 -738
  300. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/filter_traces_sft_turso.py +0 -580
  301. synth_ai/environments/examples/sokoban/units/astar_common.py +0 -95
  302. synth_ai/experimental/synth_oss.py +0 -446
  303. synth_ai/install_sqld.sh +0 -40
  304. synth_ai/learning/filtering.py +0 -0
  305. synth_ai/learning/offline/dpo.py +0 -0
  306. synth_ai/learning/offline/providers.py +0 -7
  307. synth_ai/learning/offline/sft.py +0 -0
  308. synth_ai/learning/offline/shared.py +0 -0
  309. synth_ai/learning/online/grpo.py +0 -0
  310. synth_ai/learning/online/irft.py +0 -0
  311. synth_ai/learning/prompts/banking77_injection_eval.py +0 -168
  312. synth_ai/learning/prompts/gepa.py +0 -0
  313. synth_ai/learning/prompts/hello_world_in_context_injection_ex.py +0 -213
  314. synth_ai/learning/prompts/mipro.py +0 -289
  315. synth_ai/learning/prompts/random_search.py +0 -246
  316. synth_ai/learning/prompts/run_mipro_banking77.py +0 -172
  317. synth_ai/learning/prompts/run_random_search_banking77.py +0 -324
  318. synth_ai/rl/secrets.py +0 -19
  319. synth_ai/scripts/verify_rewards.py +0 -100
  320. synth_ai/tracing/__init__.py +0 -30
  321. synth_ai/tracing_v1/__init__.py +0 -33
  322. synth_ai/tracing_v3/turso/__init__.py +0 -25
  323. synth_ai/tracing_v3/turso/manager.py +0 -774
  324. synth_ai/zyk/__init__.py +0 -30
  325. synth_ai-0.2.9.dev4.dist-info/METADATA +0 -131
  326. /synth_ai/{lm → v0/lm}/caching/__init__.py +0 -0
  327. /synth_ai/{lm → v0/lm}/caching/constants.py +0 -0
  328. /synth_ai/{lm → v0/lm}/caching/dbs.py +0 -0
  329. /synth_ai/{lm → v0/lm}/constants.py +0 -0
  330. /synth_ai/{lm → v0/lm}/core/__init__.py +0 -0
  331. /synth_ai/{lm → v0/lm}/cost/__init__.py +0 -0
  332. /synth_ai/{lm → v0/lm}/cost/monitor.py +0 -0
  333. /synth_ai/{lm → v0/lm}/cost/statefulness.py +0 -0
  334. /synth_ai/{lm → v0/lm}/injection.py +0 -0
  335. /synth_ai/{lm → v0/lm}/provider_support/__init__.py +0 -0
  336. /synth_ai/{lm → v0/lm}/provider_support/suppress_logging.py +0 -0
  337. /synth_ai/{lm → v0/lm}/structured_outputs/__init__.py +0 -0
  338. /synth_ai/{lm → v0/lm}/structured_outputs/inject.py +0 -0
  339. /synth_ai/{lm → v0/lm}/tools/__init__.py +0 -0
  340. /synth_ai/{lm → v0/lm}/tools/base.py +0 -0
  341. /synth_ai/{lm → v0/lm}/unified_interface.py +0 -0
  342. /synth_ai/{lm → v0/lm}/vendors/__init__.py +0 -0
  343. /synth_ai/{lm → v0/lm}/vendors/base.py +0 -0
  344. /synth_ai/{lm → v0/lm}/vendors/core/__init__.py +0 -0
  345. /synth_ai/{lm → v0/lm}/vendors/core/synth_dev_api.py +0 -0
  346. /synth_ai/{lm → v0/lm}/vendors/local/__init__.py +0 -0
  347. /synth_ai/{lm → v0/lm}/vendors/local/ollama.py +0 -0
  348. /synth_ai/{lm → v0/lm}/vendors/retries.py +0 -0
  349. /synth_ai/{lm → v0/lm}/vendors/supported/__init__.py +0 -0
  350. /synth_ai/{lm → v0/lm}/warmup.py +0 -0
  351. {synth_ai-0.2.9.dev4.dist-info → synth_ai-0.2.9.dev6.dist-info}/WHEEL +0 -0
  352. {synth_ai-0.2.9.dev4.dist-info → synth_ai-0.2.9.dev6.dist-info}/entry_points.txt +0 -0
  353. {synth_ai-0.2.9.dev4.dist-info → synth_ai-0.2.9.dev6.dist-info}/licenses/LICENSE +0 -0
@@ -1,25 +1,26 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
+ import contextlib
4
5
  import json
5
6
  import os
6
- import sys
7
- import time
8
- from pathlib import Path
9
- from typing import Any, Dict, Callable
10
7
  import shutil
11
8
  import stat
9
+ import sys
12
10
  import textwrap
11
+ import time
12
+ from collections.abc import Callable
13
+ from pathlib import Path
14
+ from typing import Any
13
15
 
14
- from synth_ai.demos.demo_task_apps import core as demo_core
15
- from synth_ai.demos.demo_task_apps.core import DemoEnv, DEFAULT_TASK_APP_SECRET_NAME
16
16
  from synth_ai.demo_registry import (
17
- CopySpec,
18
17
  DemoTemplate,
19
18
  get_demo_template,
20
19
  list_demo_templates,
21
20
  )
22
- from synth_ai.handshake import run_handshake, HandshakeError
21
+ from synth_ai.demos.demo_task_apps import core as demo_core
22
+ from synth_ai.demos.demo_task_apps.core import DEFAULT_TASK_APP_SECRET_NAME, DemoEnv
23
+ from synth_ai.handshake import HandshakeError, run_handshake
23
24
 
24
25
 
25
26
  def _key_preview(value: str, label: str) -> str:
@@ -45,34 +46,70 @@ def _is_modal_public_url(u: str) -> bool:
45
46
 
46
47
 
47
48
  def cmd_setup(_args: argparse.Namespace) -> int:
48
- # 1) Always perform SDK handshake and overwrite .env with returned keys
49
+ # Change to demo directory if stored
50
+ demo_dir = demo_core.load_demo_dir()
51
+ if demo_dir and os.path.isdir(demo_dir):
52
+ os.chdir(demo_dir)
53
+ print(f"Using demo directory: {demo_dir}")
54
+
55
+ # 1) Try to fetch keys from frontend; fall back to manual input if fetch fails
56
+ synth_key = ""
57
+ rl_env_key = ""
58
+ org_name = "this organization"
59
+
49
60
  try:
50
61
  print("\n⏳ Connecting SDK to your browser session…")
51
62
  res = run_handshake()
52
- user = res.get("user") or {}
53
63
  org = res.get("org") or {}
54
64
  keys = res.get("keys") or {}
55
65
  synth_key = str(keys.get("synth") or "").strip()
56
66
  rl_env_key = str(keys.get("rl_env") or "").strip()
57
- if not synth_key or not rl_env_key:
58
- raise HandshakeError("handshake returned missing keys")
59
- # Overwrite .env with the latest values from the account/org
60
- demo_core.persist_dotenv_values({
67
+ org_name = org.get("name") or "this organization"
68
+ print(f" Connected to {org_name}!")
69
+ except (HandshakeError, Exception) as e:
70
+ print(f"⚠️ Failed to fetch keys from frontend: {e}")
71
+ print("Falling back to manual entry...")
72
+
73
+ # Prompt for manual input if any key is missing
74
+ if not synth_key:
75
+ try:
76
+ synth_key = input(
77
+ "Failed to fetch your Synth API key. Please enter your Synth API key here:\n> "
78
+ ).strip()
79
+ except (EOFError, KeyboardInterrupt):
80
+ print("\nSetup cancelled.")
81
+ return 1
82
+ if not synth_key:
83
+ print("Synth API key is required.")
84
+ return 1
85
+
86
+ if not rl_env_key:
87
+ try:
88
+ rl_env_key = input(
89
+ "Failed to fetch your RL Environment API key. Please enter your RL Environment API key here:\n> "
90
+ ).strip()
91
+ except (EOFError, KeyboardInterrupt):
92
+ print("\nSetup cancelled.")
93
+ return 1
94
+ if not rl_env_key:
95
+ print("RL Environment API key is required.")
96
+ return 1
97
+
98
+ # Persist both keys to .env
99
+ dotenv_path = demo_core.persist_dotenv_values(
100
+ {
61
101
  "SYNTH_API_KEY": synth_key,
62
102
  "ENVIRONMENT_API_KEY": rl_env_key,
63
- })
64
- org_name = (org.get("name") or "this organization")
65
- print(f"✅ Connected to {org_name}!")
66
- except HandshakeError as e:
67
- print(f"Handshake failed: {e}")
68
- return 1
69
- except Exception as e:
70
- print(f"Unexpected handshake error: {e}")
71
- return 1
103
+ }
104
+ )
105
+
106
+ # Store .env path for subsequent commands
107
+ demo_core.persist_env_file_path(dotenv_path)
72
108
 
73
109
  # 2) Reload env after handshake to pick up values from .env (suppress env prints)
74
- import io
75
110
  import contextlib
111
+ import io
112
+
76
113
  _buf = io.StringIO()
77
114
  with contextlib.redirect_stdout(_buf):
78
115
  env = demo_core.load_env()
@@ -89,22 +126,22 @@ def cmd_setup(_args: argparse.Namespace) -> int:
89
126
  return
90
127
  current = env.task_app_base_url
91
128
  needs_lookup = False
92
- if not current:
93
- needs_lookup = True
94
- elif not _is_modal_public_url(current):
129
+ if not current or not _is_modal_public_url(current):
95
130
  needs_lookup = True
96
131
  if not needs_lookup:
97
132
  return
98
- code, out = _popen_capture([
99
- "uv",
100
- "run",
101
- "python",
102
- "-m",
103
- "modal",
104
- "app",
105
- "url",
106
- env.task_app_name,
107
- ])
133
+ code, out = _popen_capture(
134
+ [
135
+ "uv",
136
+ "run",
137
+ "python",
138
+ "-m",
139
+ "modal",
140
+ "app",
141
+ "url",
142
+ env.task_app_name,
143
+ ]
144
+ )
108
145
  if code != 0 or not out:
109
146
  return
110
147
  new_url = ""
@@ -134,15 +171,16 @@ def cmd_setup(_args: argparse.Namespace) -> int:
134
171
 
135
172
  _maybe_fix_task_url()
136
173
 
137
- ok_backend = False
138
- ok_task = False
139
174
  if env.dev_backend_url:
140
- api = env.dev_backend_url.rstrip("/") + ("" if env.dev_backend_url.endswith("/api") else "/api")
141
- ok_backend = demo_core.assert_http_ok(api + "/health", method="GET")
175
+ api = env.dev_backend_url.rstrip("/") + (
176
+ "" if env.dev_backend_url.endswith("/api") else "/api"
177
+ )
178
+ demo_core.assert_http_ok(api + "/health", method="GET")
142
179
  # Intentionally suppress backend health print for concise output
143
180
  if env.task_app_base_url:
144
- ok_task = demo_core.assert_http_ok(env.task_app_base_url.rstrip("/") + "/health", method="GET") or \
145
- demo_core.assert_http_ok(env.task_app_base_url.rstrip("/"), method="GET")
181
+ demo_core.assert_http_ok(
182
+ env.task_app_base_url.rstrip("/") + "/health", method="GET"
183
+ ) or demo_core.assert_http_ok(env.task_app_base_url.rstrip("/"), method="GET")
146
184
  # Intentionally suppress task app health print
147
185
  else:
148
186
  print("\nSet your task app URL by running:\nuvx synth-ai rl_demo deploy\n")
@@ -150,13 +188,19 @@ def cmd_setup(_args: argparse.Namespace) -> int:
150
188
  # Omit uv version print to keep output concise
151
189
 
152
190
  # Keep exit code neutral; not all checks are critical for pairing
191
+ print(f"\nKeys saved to: {dotenv_path}")
153
192
  return 0
154
193
 
155
194
 
156
- def _popen_capture(cmd: list[str], cwd: str | None = None, env: dict | None = None) -> tuple[int, str]:
195
+ def _popen_capture(
196
+ cmd: list[str], cwd: str | None = None, env: dict | None = None
197
+ ) -> tuple[int, str]:
157
198
  import subprocess
199
+
158
200
  try:
159
- proc = subprocess.Popen(cmd, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
201
+ proc = subprocess.Popen(
202
+ cmd, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
203
+ )
160
204
  out, _ = proc.communicate()
161
205
  return int(proc.returncode or 0), out or ""
162
206
  except Exception as e:
@@ -200,7 +244,9 @@ def _popen_stream(cmd: list[str], cwd: str | None = None, env: dict | None = Non
200
244
  return int(proc.returncode or 0)
201
245
 
202
246
 
203
- def _popen_stream_capture(cmd: list[str], cwd: str | None = None, env: dict | None = None) -> tuple[int, str]:
247
+ def _popen_stream_capture(
248
+ cmd: list[str], cwd: str | None = None, env: dict | None = None
249
+ ) -> tuple[int, str]:
204
250
  """Stream subprocess output to stdout and also capture it into a buffer."""
205
251
  import subprocess
206
252
  import threading
@@ -251,7 +297,19 @@ def _find_asgi_apps(root: Path) -> list[Path]:
251
297
  - "@modal.asgi_app()"
252
298
  """
253
299
  results: list[Path] = []
254
- skip_dirs = {".git", ".hg", ".svn", "node_modules", "dist", "build", "__pycache__", ".ruff_cache", ".mypy_cache", "venv", ".venv"}
300
+ skip_dirs = {
301
+ ".git",
302
+ ".hg",
303
+ ".svn",
304
+ "node_modules",
305
+ "dist",
306
+ "build",
307
+ "__pycache__",
308
+ ".ruff_cache",
309
+ ".mypy_cache",
310
+ "venv",
311
+ ".venv",
312
+ }
255
313
  for dirpath, dirnames, filenames in os.walk(root):
256
314
  dirnames[:] = [d for d in dirnames if d not in skip_dirs]
257
315
  for name in filenames:
@@ -265,16 +323,20 @@ def _find_asgi_apps(root: Path) -> list[Path]:
265
323
  results.append(path)
266
324
  except Exception:
267
325
  continue
326
+
268
327
  # Stable order: prioritize files under synth_demo/ first, then alphabetical
269
328
  def _priority(p: Path) -> tuple[int, str]:
270
329
  rel = str(p.resolve())
271
330
  in_demo = "/synth_demo/" in rel or rel.endswith("/synth_demo/task_app.py")
272
331
  return (0 if in_demo else 1, rel)
332
+
273
333
  results.sort(key=_priority)
274
334
  return results
275
335
 
276
336
 
277
- def _prompt_value(label: str, default: str | int | float, cast: Callable[[str], Any] | None = None) -> Any:
337
+ def _prompt_value(
338
+ label: str, default: str | int | float, cast: Callable[[str], Any] | None = None
339
+ ) -> Any:
278
340
  prompt = f"{label} [{default}]: "
279
341
  try:
280
342
  raw = input(prompt).strip()
@@ -293,7 +355,19 @@ def _prompt_value(label: str, default: str | int | float, cast: Callable[[str],
293
355
 
294
356
  def _find_vllm_tomls(root: Path) -> list[Path]:
295
357
  results: list[Path] = []
296
- skip_dirs = {".git", ".hg", ".svn", "node_modules", "dist", "build", "__pycache__", ".ruff_cache", ".mypy_cache", "venv", ".venv"}
358
+ skip_dirs = {
359
+ ".git",
360
+ ".hg",
361
+ ".svn",
362
+ "node_modules",
363
+ "dist",
364
+ "build",
365
+ "__pycache__",
366
+ ".ruff_cache",
367
+ ".mypy_cache",
368
+ "venv",
369
+ ".venv",
370
+ }
297
371
  for dirpath, dirnames, filenames in os.walk(root):
298
372
  dirnames[:] = [d for d in dirnames if d not in skip_dirs]
299
373
  for name in filenames:
@@ -313,7 +387,9 @@ def _create_new_config(env: DemoEnv) -> str:
313
387
  default_path = os.path.join(os.getcwd(), "demo_config.toml")
314
388
  while True:
315
389
  try:
316
- destination = input(f"Path to save new config [{default_path}]: ").strip() or default_path
390
+ destination = (
391
+ input(f"Path to save new config [{default_path}]: ").strip() or default_path
392
+ )
317
393
  except Exception:
318
394
  destination = default_path
319
395
  destination = os.path.abspath(destination)
@@ -322,7 +398,9 @@ def _create_new_config(env: DemoEnv) -> str:
322
398
  continue
323
399
  if os.path.exists(destination):
324
400
  try:
325
- overwrite = input(f"{destination} exists. Overwrite? [y/N]: ").strip().lower() or "n"
401
+ overwrite = (
402
+ input(f"{destination} exists. Overwrite? [y/N]: ").strip().lower() or "n"
403
+ )
326
404
  except Exception:
327
405
  overwrite = "n"
328
406
  if not overwrite.startswith("y"):
@@ -334,7 +412,9 @@ def _create_new_config(env: DemoEnv) -> str:
334
412
  model_name = _prompt_value("Model name", "Qwen/Qwen3-0.6B")
335
413
  compute_gpu_type = _prompt_value("Compute GPU type", "H100")
336
414
  compute_gpu_count = _prompt_value("Compute GPU count", 4, int)
337
- topology_gpu_type = _prompt_value("Topology GPU type", f"{compute_gpu_type}:{compute_gpu_count}")
415
+ topology_gpu_type = _prompt_value(
416
+ "Topology GPU type", f"{compute_gpu_type}:{compute_gpu_count}"
417
+ )
338
418
  gpus_for_vllm = _prompt_value("Topology gpus_for_vllm", 2, int)
339
419
  gpus_for_training = _prompt_value("Topology gpus_for_training", 1, int)
340
420
  tensor_parallel = _prompt_value("Topology tensor_parallel", 2, int)
@@ -352,8 +432,9 @@ def _create_new_config(env: DemoEnv) -> str:
352
432
  task_url_default = env.task_app_base_url or ""
353
433
  services_task_url = _prompt_value("services.task_url", task_url_default)
354
434
 
355
- template = textwrap.dedent(
356
- f"""\
435
+ template = (
436
+ textwrap.dedent(
437
+ f"""\
357
438
  # Crafter online RL training configuration (research local copy)
358
439
 
359
440
  [model]
@@ -495,7 +576,9 @@ def _create_new_config(env: DemoEnv) -> str:
495
576
  [services]
496
577
  task_url = \"{services_task_url}\"
497
578
  """
498
- ).strip() + "\n"
579
+ ).strip()
580
+ + "\n"
581
+ )
499
582
 
500
583
  with open(destination, "w", encoding="utf-8") as fh:
501
584
  fh.write(template)
@@ -514,7 +597,11 @@ def _select_or_create_config(explicit: str | None, env: DemoEnv) -> str:
514
597
  discovered = _find_vllm_tomls(search_root)
515
598
 
516
599
  extras: list[Path] = []
517
- packaged = Path(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "demo_task_apps", "math", "config.toml")))
600
+ packaged = Path(
601
+ os.path.abspath(
602
+ os.path.join(os.path.dirname(__file__), "..", "demo_task_apps", "math", "config.toml")
603
+ )
604
+ )
518
605
  extras.append(packaged)
519
606
  home_cfg = Path(os.path.expanduser("~/.synth-ai/demo_config.toml"))
520
607
  extras.append(home_cfg)
@@ -560,29 +647,36 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
560
647
 
561
648
  env_key = (env.env_api_key or "").strip()
562
649
  if not env_key:
563
- raise RuntimeError(f"[{label}] ENVIRONMENT_API_KEY missing. Run `uvx synth-ai rl_demo deploy` first.")
650
+ raise RuntimeError(
651
+ f"[{label}] ENVIRONMENT_API_KEY missing. Run `uvx synth-ai rl_demo deploy` first."
652
+ )
564
653
 
565
654
  task_url = env.task_app_base_url
566
655
  if not task_url or not _is_modal_public_url(task_url):
567
656
  resolved = ""
568
657
  if env.task_app_name:
569
658
  try:
570
- choice = input(
571
- f"Resolve URL from Modal for app '{env.task_app_name}'? [Y/n]: "
572
- ).strip().lower() or "y"
659
+ choice = (
660
+ input(f"Resolve URL from Modal for app '{env.task_app_name}'? [Y/n]: ")
661
+ .strip()
662
+ .lower()
663
+ or "y"
664
+ )
573
665
  except Exception:
574
666
  choice = "y"
575
667
  if choice.startswith("y"):
576
- code, out = _popen_capture([
577
- "uv",
578
- "run",
579
- "python",
580
- "-m",
581
- "modal",
582
- "app",
583
- "url",
584
- env.task_app_name,
585
- ])
668
+ code, out = _popen_capture(
669
+ [
670
+ "uv",
671
+ "run",
672
+ "python",
673
+ "-m",
674
+ "modal",
675
+ "app",
676
+ "url",
677
+ env.task_app_name,
678
+ ]
679
+ )
586
680
  if code == 0 and out:
587
681
  for tok in out.split():
588
682
  if _is_modal_public_url(tok):
@@ -591,7 +685,9 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
591
685
  if not resolved:
592
686
  print(f"[{label}] Task app URL not configured or not a valid Modal public URL.")
593
687
  print("Examples: https://<app-name>-fastapi-app.modal.run")
594
- entered = input("Enter Task App base URL (must contain '.modal.run'), or press Enter to abort: ").strip()
688
+ entered = input(
689
+ "Enter Task App base URL (must contain '.modal.run'), or press Enter to abort: "
690
+ ).strip()
595
691
  if not entered or not _is_modal_public_url(entered):
596
692
  raise RuntimeError(f"[{label}] Valid Task App URL is required.")
597
693
  task_url = entered.rstrip("/")
@@ -608,11 +704,13 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
608
704
  demo_core.persist_task_url(task_url, name=app_name)
609
705
 
610
706
  demo_core.persist_task_url(task_url, name=app_name)
611
- demo_core.persist_dotenv_values({
612
- "TASK_APP_BASE_URL": task_url,
613
- "TASK_APP_NAME": app_name,
614
- "TASK_APP_SECRET_NAME": DEFAULT_TASK_APP_SECRET_NAME,
615
- })
707
+ demo_core.persist_dotenv_values(
708
+ {
709
+ "TASK_APP_BASE_URL": task_url,
710
+ "TASK_APP_NAME": app_name,
711
+ "TASK_APP_SECRET_NAME": DEFAULT_TASK_APP_SECRET_NAME,
712
+ }
713
+ )
616
714
 
617
715
  if synth_key:
618
716
  os.environ["SYNTH_API_KEY"] = synth_key
@@ -621,7 +719,6 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
621
719
  if openai_key:
622
720
  os.environ["OPENAI_API_KEY"] = openai_key
623
721
 
624
- rollout_url = task_url.rstrip("/") + "/health/rollout"
625
722
  print(f"[{label}] Verifying rollout health:")
626
723
  try:
627
724
  ek = (env_key or "").strip()
@@ -636,7 +733,6 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
636
733
  print(f"[{label}] GET", h)
637
734
  rc, body = _http("GET", h, headers={"X-API-Key": env_key})
638
735
  if rc == 200:
639
- rollout_url = h
640
736
  break
641
737
  print(f"[{label}] status: {rc}")
642
738
  try:
@@ -648,10 +744,8 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
648
744
  print(f"[{label}] body:", preview)
649
745
  if rc != 200:
650
746
  print(f"[{label}] Warning: rollout health check failed ({rc}). Response: {body}")
651
- try:
747
+ with contextlib.suppress(Exception):
652
748
  print(f"[{label}] Sent header X-API-Key → {_key_preview(env_key, 'X-API-Key')}")
653
- except Exception:
654
- pass
655
749
  else:
656
750
  print(f"[{label}] Task app rollout health check OK.")
657
751
 
@@ -667,6 +761,12 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
667
761
 
668
762
 
669
763
  def cmd_deploy(args: argparse.Namespace) -> int:
764
+ # Change to demo directory if stored
765
+ demo_dir = demo_core.load_demo_dir()
766
+ if demo_dir and os.path.isdir(demo_dir):
767
+ os.chdir(demo_dir)
768
+ print(f"Using demo directory: {demo_dir}")
769
+
670
770
  env = demo_core.load_env()
671
771
  os.environ["TASK_APP_SECRET_NAME"] = DEFAULT_TASK_APP_SECRET_NAME
672
772
  cwd_env_path = os.path.join(os.getcwd(), ".env")
@@ -677,12 +777,22 @@ def cmd_deploy(args: argparse.Namespace) -> int:
677
777
  if args.local:
678
778
  print("Starting local Task App…")
679
779
  import subprocess
680
- subprocess.Popen([sys.executable, "-c", "from synth_ai.demos.demo_task_apps.math.app import run; run()"],
681
- stdout=sys.stdout, stderr=sys.stderr)
780
+
781
+ subprocess.Popen(
782
+ [
783
+ sys.executable,
784
+ "-c",
785
+ "from synth_ai.demos.demo_task_apps.math.app import run; run()",
786
+ ],
787
+ stdout=sys.stdout,
788
+ stderr=sys.stderr,
789
+ )
682
790
  target = "http://127.0.0.1:8080"
683
791
  app_name = ""
684
792
  for _ in range(30):
685
- if demo_core.assert_http_ok(target + "/health", method="GET") or demo_core.assert_http_ok(target, method="GET"):
793
+ if demo_core.assert_http_ok(
794
+ target + "/health", method="GET"
795
+ ) or demo_core.assert_http_ok(target, method="GET"):
686
796
  url = target
687
797
  break
688
798
  time.sleep(1)
@@ -707,7 +817,9 @@ def cmd_deploy(args: argparse.Namespace) -> int:
707
817
  rel = os.path.relpath(str(pth), os.getcwd())
708
818
  print(f" [{idx}] {rel}")
709
819
  try:
710
- sel = input(f"Enter choice [1-{len(found)}] (default 1): ").strip() or "1"
820
+ sel = (
821
+ input(f"Enter choice [1-{len(found)}] (default 1): ").strip() or "1"
822
+ )
711
823
  except Exception:
712
824
  sel = "1"
713
825
  try:
@@ -719,6 +831,7 @@ def cmd_deploy(args: argparse.Namespace) -> int:
719
831
  if not app_path and args.script:
720
832
  # Legacy script fallback if user supplied --script explicitly
721
833
  from synth_ai.demos.demo_task_apps.math.deploy_modal import deploy as modal_deploy
834
+
722
835
  url = modal_deploy(script_path=args.script, env_api_key=env.env_api_key)
723
836
  if args.name:
724
837
  app_name = args.name
@@ -750,16 +863,19 @@ def cmd_deploy(args: argparse.Namespace) -> int:
750
863
  env_key: str | None = existing_env_key or None
751
864
  if existing_env_key:
752
865
  try:
753
- reuse_choice = input(
754
- "Use existing ENVIRONMENT_API_KEY from state/.env? [Y/n]: "
755
- ).strip().lower() or "y"
866
+ reuse_choice = (
867
+ input("Use existing ENVIRONMENT_API_KEY from state/.env? [Y/n]: ")
868
+ .strip()
869
+ .lower()
870
+ or "y"
871
+ )
756
872
  except Exception:
757
873
  reuse_choice = "y"
758
874
  if not reuse_choice.startswith("y"):
759
875
  env_key = None
760
876
 
761
877
  if env_key is None:
762
- from synth_ai.rl.secrets import mint_environment_api_key
878
+ from synth_ai.learning.rl.secrets import mint_environment_api_key
763
879
 
764
880
  env_key = mint_environment_api_key()
765
881
  demo_core.persist_env_api_key(env_key)
@@ -770,35 +886,50 @@ def cmd_deploy(args: argparse.Namespace) -> int:
770
886
  print("[deploy] Minted new ENVIRONMENT_API_KEY")
771
887
  elif env_key:
772
888
  os.environ["ENVIRONMENT_API_KEY"] = env_key
773
-
889
+
774
890
  # Optionally upload the new key to the backend using sealed box helper
775
891
  backend_base = (env.dev_backend_url or "").rstrip("/")
776
- synth_key = (env.synth_api_key or os.environ.get("SYNTH_API_KEY") or local_env.get("SYNTH_API_KEY") or "").strip()
892
+ synth_key = (
893
+ env.synth_api_key
894
+ or os.environ.get("SYNTH_API_KEY")
895
+ or local_env.get("SYNTH_API_KEY")
896
+ or ""
897
+ ).strip()
777
898
  if backend_base and synth_key:
778
- # Pass a base WITHOUT trailing /api to setup_environment_api_key,
779
- # since it appends /api/v1/... internally.
780
- non_api_base = backend_base[:-4] if backend_base.endswith("/api") else backend_base
899
+ # Pass a base WITHOUT trailing /api to setup_environment_api_key,
900
+ # since it appends /api/v1/... internally.
901
+ non_api_base = (
902
+ backend_base[:-4] if backend_base.endswith("/api") else backend_base
903
+ )
904
+ try:
905
+ choice = (
906
+ input(f"Upload ENVIRONMENT_API_KEY to backend {non_api_base}? [Y/n]: ")
907
+ .strip()
908
+ .lower()
909
+ or "y"
910
+ )
911
+ except Exception:
912
+ choice = "y"
913
+ if choice.startswith("y"):
781
914
  try:
782
- choice = input(
783
- f"Upload ENVIRONMENT_API_KEY to backend {non_api_base}? [Y/n]: "
784
- ).strip().lower() or "y"
785
- except Exception:
786
- choice = "y"
787
- if choice.startswith("y"):
788
- try:
789
- print(f"[deploy] Uploading ENVIRONMENT_API_KEY to {non_api_base} …")
790
- from synth_ai.rl.env_keys import setup_environment_api_key
791
-
792
- setup_environment_api_key(non_api_base, synth_key, token=env_key)
793
- print("[deploy] Backend sealed-box upload complete.")
794
- except Exception as upload_err:
795
- print(f"[deploy] Failed to upload ENVIRONMENT_API_KEY: {upload_err}")
796
- print(
797
- "Hint: run `uvx python -c \"from synth_ai.rl.env_keys import setup_environment_api_key as s;"
798
- " s('<backend>', '<synth_api_key>')\"` once the backend is reachable."
799
- )
800
-
801
- synth_key = (env.synth_api_key or os.environ.get("SYNTH_API_KEY") or local_env.get("SYNTH_API_KEY") or "").strip()
915
+ print(f"[deploy] Uploading ENVIRONMENT_API_KEY to {non_api_base} …")
916
+ from synth_ai.learning.rl.env_keys import setup_environment_api_key
917
+
918
+ setup_environment_api_key(non_api_base, synth_key, token=env_key)
919
+ print("[deploy] Backend sealed-box upload complete.")
920
+ except Exception as upload_err:
921
+ print(f"[deploy] Failed to upload ENVIRONMENT_API_KEY: {upload_err}")
922
+ print(
923
+ 'Hint: run `uvx python -c "from synth_ai.learning.rl.env_keys import setup_environment_api_key as s;'
924
+ " s('<backend>', '<synth_api_key>')\"` once the backend is reachable."
925
+ )
926
+
927
+ synth_key = (
928
+ env.synth_api_key
929
+ or os.environ.get("SYNTH_API_KEY")
930
+ or local_env.get("SYNTH_API_KEY")
931
+ or ""
932
+ ).strip()
802
933
  if not synth_key:
803
934
  synth_key = input("Enter SYNTH_API_KEY for deployment (required): ").strip()
804
935
  if not synth_key:
@@ -809,7 +940,9 @@ def cmd_deploy(args: argparse.Namespace) -> int:
809
940
  env.synth_api_key = synth_key
810
941
  os.environ["SYNTH_API_KEY"] = synth_key
811
942
 
812
- openai_key = (os.environ.get("OPENAI_API_KEY") or local_env.get("OPENAI_API_KEY") or "").strip()
943
+ openai_key = (
944
+ os.environ.get("OPENAI_API_KEY") or local_env.get("OPENAI_API_KEY") or ""
945
+ ).strip()
813
946
  if not openai_key:
814
947
  openai_key = input(
815
948
  "Enter your OpenAI API key, found at https://platform.openai.com/api-keys\n> "
@@ -821,8 +954,20 @@ def cmd_deploy(args: argparse.Namespace) -> int:
821
954
  local_env["OPENAI_API_KEY"] = openai_key
822
955
  os.environ["OPENAI_API_KEY"] = openai_key
823
956
 
824
- deploy_cmd = ["uv", "run", "python", "-m", "modal", "deploy", "--name", name_in, app_path]
825
- print("\nStreaming Modal build/deploy logs (this can take several minutes on first run)…\n")
957
+ deploy_cmd = [
958
+ "uv",
959
+ "run",
960
+ "python",
961
+ "-m",
962
+ "modal",
963
+ "deploy",
964
+ "--name",
965
+ name_in,
966
+ app_path,
967
+ ]
968
+ print(
969
+ "\nStreaming Modal build/deploy logs (this can take several minutes on first run)…\n"
970
+ )
826
971
  code, deploy_logs = _popen_stream_capture(deploy_cmd)
827
972
  if code != 0:
828
973
  raise RuntimeError(f"modal deploy failed (exit {code})")
@@ -830,6 +975,7 @@ def cmd_deploy(args: argparse.Namespace) -> int:
830
975
  if not url:
831
976
  try:
832
977
  import re as _re
978
+
833
979
  m_all = _re.findall(r"https?://[^\s]+\.modal\.run", deploy_logs or "")
834
980
  if m_all:
835
981
  url = m_all[-1].strip().rstrip("/")
@@ -844,7 +990,9 @@ def cmd_deploy(args: argparse.Namespace) -> int:
844
990
  break
845
991
  # Fallback: try reading recent Modal logs for the app to find a URL line
846
992
  if not url:
847
- code3, out3 = _popen_capture(["uv", "run", "python", "-m", "modal", "app", "list"])
993
+ code3, out3 = _popen_capture(
994
+ ["uv", "run", "python", "-m", "modal", "app", "list"]
995
+ )
848
996
  if code3 == 0 and out3:
849
997
  for line in out3.splitlines():
850
998
  if name_in in line:
@@ -857,7 +1005,9 @@ def cmd_deploy(args: argparse.Namespace) -> int:
857
1005
  # Prompt user if still no valid URL
858
1006
  if not url:
859
1007
  print("\nCould not auto-detect a public Modal URL for the app.")
860
- entered = input("Enter the Modal public URL (must contain '.modal.run'), or press Enter to abort: ").strip()
1008
+ entered = input(
1009
+ "Enter the Modal public URL (must contain '.modal.run'), or press Enter to abort: "
1010
+ ).strip()
861
1011
  if entered and _is_modal_public_url(entered):
862
1012
  url = entered.rstrip("/")
863
1013
  if not url:
@@ -885,8 +1035,9 @@ def cmd_deploy(args: argparse.Namespace) -> int:
885
1035
  print(f"Deploy error: {e}")
886
1036
  return 2
887
1037
 
888
-
889
- print("`rl_demo configure` prepares environment and secrets; `synth-ai run` now handles launches.")
1038
+ print(
1039
+ "`rl_demo configure` prepares environment and secrets; `synth-ai run` now handles launches."
1040
+ )
890
1041
  env = demo_core.load_env()
891
1042
  synth_key = (env.synth_api_key or "").strip()
892
1043
  if not synth_key:
@@ -919,40 +1070,62 @@ def cmd_deploy(args: argparse.Namespace) -> int:
919
1070
 
920
1071
 
921
1072
  def _ensure_modal_installed() -> None:
922
- """Install the modal package if it is not already available."""
1073
+ """Install the modal package if it is not already available and check authentication."""
923
1074
 
1075
+ # Check if modal is installed
1076
+ modal_installed = False
924
1077
  try:
925
1078
  import importlib.util as _iu
926
1079
 
927
1080
  if _iu.find_spec("modal") is not None:
928
- print("modal package found")
929
- return
1081
+ modal_installed = True
930
1082
  except Exception:
931
1083
  pass
932
1084
 
933
- print("modal not found; installing…")
934
- try:
935
- if shutil.which("uv"):
936
- code, out = _popen_capture(["uv", "pip", "install", "modal>=1.1.4"])
937
- else:
938
- code, out = _popen_capture([sys.executable, "-m", "pip", "install", "modal>=1.1.4"])
939
- if code != 0:
940
- print(out)
941
- print("Failed to install modal; continuing may fail.")
942
- else:
943
- print("modal installed successfully.")
944
- except Exception as exc:
945
- print(f"modal install error: {exc}")
1085
+ # Install modal if needed
1086
+ if not modal_installed:
1087
+ print("modal not found; installing…")
1088
+ try:
1089
+ if shutil.which("uv"):
1090
+ code, out = _popen_capture(["uv", "pip", "install", "modal>=1.1.4"])
1091
+ else:
1092
+ code, out = _popen_capture([sys.executable, "-m", "pip", "install", "modal>=1.1.4"])
1093
+ if code != 0:
1094
+ print(out)
1095
+ print("Failed to install modal; continuing may fail.")
1096
+ return
1097
+ else:
1098
+ print("✓ modal installed successfully")
1099
+ modal_installed = True
1100
+ except Exception as exc:
1101
+ print(f"modal install error: {exc}")
1102
+ return
946
1103
 
947
- try:
948
- import importlib.util as _iu
1104
+ # Verify modal is importable
1105
+ if modal_installed:
1106
+ try:
1107
+ import importlib.util as _iu
949
1108
 
950
- if _iu.find_spec("modal") is None:
951
- print("Warning: modal is still not importable after install attempt.")
952
- else:
953
- print("modal package ready")
954
- except Exception:
955
- print("Warning: unable to verify modal installation.")
1109
+ if _iu.find_spec("modal") is None:
1110
+ print("Warning: modal is still not importable after install attempt.")
1111
+ return
1112
+ except Exception:
1113
+ print("Warning: unable to verify modal installation.")
1114
+ return
1115
+
1116
+ # Check modal authentication status
1117
+ auth_ok, auth_msg = demo_core.modal_auth_status()
1118
+ if auth_ok:
1119
+ print(f"✓ Modal authenticated: {auth_msg}")
1120
+ else:
1121
+ print("\n⚠️ Modal authentication required")
1122
+ print(f" Status: {auth_msg}")
1123
+ print("\n To authenticate Modal, run:")
1124
+ print(" modal setup")
1125
+ print("\n Or set environment variables:")
1126
+ print(" export MODAL_TOKEN_ID=your-token-id")
1127
+ print(" export MODAL_TOKEN_SECRET=your-token-secret")
1128
+ print("\n You can deploy later after authenticating.\n")
956
1129
 
957
1130
 
958
1131
  def cmd_init(args: argparse.Namespace) -> int:
@@ -991,20 +1164,61 @@ def cmd_init(args: argparse.Namespace) -> int:
991
1164
  assert selected is not None
992
1165
 
993
1166
  default_subdir = selected.default_subdir or selected.template_id
994
- default_dest = Path(args.dest).expanduser().resolve() if args.dest else (Path.cwd() / default_subdir).resolve()
1167
+
1168
+ # Check if default destination is already occupied and switch to local_demos/ if needed
1169
+ if args.dest:
1170
+ default_dest = Path(args.dest).expanduser().resolve()
1171
+ else:
1172
+ primary_dest = Path.cwd() / default_subdir
1173
+ if primary_dest.exists() and any(primary_dest.iterdir()):
1174
+ # Switch to local_demos/ automatically if primary location is occupied
1175
+ default_dest = (Path.cwd() / "local_demos" / default_subdir).resolve()
1176
+ else:
1177
+ default_dest = primary_dest.resolve()
1178
+
995
1179
  try:
996
1180
  dest_input = input(f"Destination directory [{default_dest}]: ").strip()
997
1181
  except Exception:
998
1182
  dest_input = ""
999
1183
  destination = Path(dest_input).expanduser().resolve() if dest_input else default_dest
1000
1184
 
1185
+ # Track whether we should skip individual file prompts (if we already cleared the directory)
1186
+ directory_cleared = False
1187
+
1001
1188
  if destination.exists():
1002
1189
  if destination.is_file():
1003
1190
  print(f"Destination {destination} is a file. Provide a directory path.")
1004
1191
  return 1
1005
- if not args.force and any(destination.iterdir()):
1006
- print(f"Destination {destination} is not empty. Use --force or choose another directory.")
1007
- return 1
1192
+ if any(destination.iterdir()):
1193
+ try:
1194
+ response = (
1195
+ input(f"Destination {destination} is not empty. Overwrite? [y/N]: ")
1196
+ .strip()
1197
+ .lower()
1198
+ )
1199
+ except (EOFError, KeyboardInterrupt):
1200
+ print("\nCancelled.")
1201
+ return 1
1202
+ if response not in ("y", "yes"):
1203
+ print("Cancelled. Choose another directory or delete the existing one.")
1204
+ return 1
1205
+ # User agreed to overwrite - clear the entire directory including hidden files
1206
+ print(f"Clearing {destination}...")
1207
+ try:
1208
+ # Remove all contents including hidden files (.env, .git, etc.)
1209
+ shutil.rmtree(destination)
1210
+ except Exception as e:
1211
+ print(f"Error clearing directory: {e}")
1212
+ print("Please manually remove the directory and try again.")
1213
+ return 1
1214
+ # Recreate empty directory
1215
+ destination.mkdir(parents=True, exist_ok=True)
1216
+ # Verify it's actually empty
1217
+ if any(destination.iterdir()):
1218
+ print(f"Warning: Directory {destination} still contains files after clearing.")
1219
+ print("Some files may not have been removed. Please check manually.")
1220
+ return 1
1221
+ directory_cleared = True
1008
1222
  else:
1009
1223
  destination.mkdir(parents=True, exist_ok=True)
1010
1224
 
@@ -1018,29 +1232,83 @@ def cmd_init(args: argparse.Namespace) -> int:
1018
1232
  print(f"Template source missing: {src_path}")
1019
1233
  return 1
1020
1234
  dest_path = (destination / spec.destination).resolve()
1021
- dest_path.parent.mkdir(parents=True, exist_ok=True)
1022
- if dest_path.exists() and not args.force:
1023
- print(f"Refusing to overwrite existing file: {dest_path} (use --force)")
1024
- return 1
1025
- shutil.copy2(src_path, dest_path)
1026
- if spec.make_executable:
1027
- try:
1028
- st = os.stat(dest_path)
1029
- os.chmod(dest_path, st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
1030
- except Exception:
1031
- pass
1235
+
1236
+ # Handle directory copying
1237
+ if src_path.is_dir():
1238
+ if dest_path.exists() and not directory_cleared:
1239
+ try:
1240
+ response = (
1241
+ input(f"Directory {dest_path.name} exists. Overwrite? [y/N]: ")
1242
+ .strip()
1243
+ .lower()
1244
+ )
1245
+ except (EOFError, KeyboardInterrupt):
1246
+ print("\nCancelled.")
1247
+ return 1
1248
+ if response not in ("y", "yes"):
1249
+ print(f"Skipping {dest_path.name}")
1250
+ continue
1251
+ shutil.rmtree(dest_path)
1252
+ elif dest_path.exists() and directory_cleared:
1253
+ shutil.rmtree(dest_path)
1254
+ shutil.copytree(src_path, dest_path)
1255
+ else:
1256
+ # Handle file copying
1257
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
1258
+ if dest_path.exists() and not directory_cleared:
1259
+ try:
1260
+ response = (
1261
+ input(f"File {dest_path.name} exists. Overwrite? [y/N]: ")
1262
+ .strip()
1263
+ .lower()
1264
+ )
1265
+ except (EOFError, KeyboardInterrupt):
1266
+ print("\nCancelled.")
1267
+ return 1
1268
+ if response not in ("y", "yes"):
1269
+ print(f"Skipping {dest_path.name}")
1270
+ continue
1271
+ shutil.copy2(src_path, dest_path)
1272
+ if spec.make_executable:
1273
+ try:
1274
+ st = os.stat(dest_path)
1275
+ os.chmod(dest_path, st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
1276
+ except Exception:
1277
+ pass
1032
1278
 
1033
1279
  if selected.env_lines:
1034
1280
  env_path = destination / ".env"
1035
- if not env_path.exists() or args.force:
1281
+ should_write = True
1282
+ if env_path.exists() and not directory_cleared:
1283
+ try:
1284
+ response = input("File .env exists. Overwrite? [y/N]: ").strip().lower()
1285
+ except (EOFError, KeyboardInterrupt):
1286
+ print("\nCancelled.")
1287
+ return 1
1288
+ should_write = response in ("y", "yes")
1289
+ if should_write:
1036
1290
  _write_text(env_path, "\n".join(selected.env_lines) + "\n")
1291
+ elif not directory_cleared:
1292
+ print("Skipping .env")
1037
1293
 
1038
1294
  config_src = selected.config_source_path()
1039
1295
  if config_src and config_src.exists():
1040
1296
  cfg_dst = (destination / selected.config_destination).resolve()
1041
- if not cfg_dst.exists() or args.force:
1297
+ should_copy = True
1298
+ if cfg_dst.exists() and not directory_cleared:
1299
+ try:
1300
+ response = (
1301
+ input(f"File {cfg_dst.name} exists. Overwrite? [y/N]: ").strip().lower()
1302
+ )
1303
+ except (EOFError, KeyboardInterrupt):
1304
+ print("\nCancelled.")
1305
+ return 1
1306
+ should_copy = response in ("y", "yes")
1307
+ if should_copy:
1042
1308
  cfg_dst.parent.mkdir(parents=True, exist_ok=True)
1043
1309
  shutil.copy2(config_src, cfg_dst)
1310
+ elif not directory_cleared:
1311
+ print(f"Skipping {cfg_dst.name}")
1044
1312
 
1045
1313
  if selected.post_copy is not None:
1046
1314
  try:
@@ -1049,6 +1317,14 @@ def cmd_init(args: argparse.Namespace) -> int:
1049
1317
  print(f"Post-processing failed: {post_exc}")
1050
1318
  return 1
1051
1319
 
1320
+ # Store demo directory for subsequent commands
1321
+ demo_core.persist_demo_dir(str(destination))
1322
+
1323
+ # Store .env path if it was created
1324
+ env_file = destination / ".env"
1325
+ if env_file.exists():
1326
+ demo_core.persist_env_file_path(str(env_file))
1327
+
1052
1328
  print(f"Demo template '{selected.name}' materialised at {destination}.")
1053
1329
  print("Files created:")
1054
1330
  for spec in selected.iter_copy_specs():
@@ -1057,6 +1333,7 @@ def cmd_init(args: argparse.Namespace) -> int:
1057
1333
  print(" - .env")
1058
1334
  if selected.config_source_path():
1059
1335
  print(f" - {selected.config_destination}")
1336
+ print("\nDemo directory stored. Subsequent commands will use this directory automatically.")
1060
1337
  print("Review the files, edit .env, and run any provided deploy scripts when ready.")
1061
1338
  return 0
1062
1339
  except KeyboardInterrupt:
@@ -1067,8 +1344,14 @@ def cmd_init(args: argparse.Namespace) -> int:
1067
1344
  return 1
1068
1345
 
1069
1346
 
1070
- def _http(method: str, url: str, headers: Dict[str, str] | None = None, body: Dict[str, Any] | None = None) -> tuple[int, Dict[str, Any] | str]:
1071
- import urllib.request, urllib.error, json as _json, ssl
1347
+ def _http(
1348
+ method: str, url: str, headers: dict[str, str] | None = None, body: dict[str, Any] | None = None
1349
+ ) -> tuple[int, dict[str, Any] | str]:
1350
+ import json as _json
1351
+ import ssl
1352
+ import urllib.error
1353
+ import urllib.request
1354
+
1072
1355
  data = None
1073
1356
  if body is not None:
1074
1357
  data = _json.dumps(body).encode("utf-8")
@@ -1106,9 +1389,15 @@ def _write_text(path: str, content: str) -> None:
1106
1389
 
1107
1390
 
1108
1391
  def cmd_run(args: argparse.Namespace) -> int:
1392
+ # Change to demo directory if stored
1393
+ demo_dir = demo_core.load_demo_dir()
1394
+ if demo_dir and os.path.isdir(demo_dir):
1395
+ os.chdir(demo_dir)
1396
+ print(f"Using demo directory: {demo_dir}")
1397
+
1109
1398
  env = demo_core.load_env()
1110
1399
  cwd_env_path = os.path.join(os.getcwd(), ".env")
1111
- local_env = demo_core.load_dotenv_file(cwd_env_path)
1400
+ demo_core.load_dotenv_file(cwd_env_path)
1112
1401
 
1113
1402
  synth_key = (env.synth_api_key or "").strip()
1114
1403
  if not synth_key:
@@ -1148,7 +1437,11 @@ def cmd_run(args: argparse.Namespace) -> int:
1148
1437
  # Detect monorepo launcher and delegate if available (aligns with run_clustered.sh which works)
1149
1438
  launcher = "/Users/joshpurtell/Documents/GitHub/monorepo/tests/applications/math/rl/start_math_clustered.py"
1150
1439
  if os.path.isfile(launcher):
1151
- backend_base = env.dev_backend_url[:-4] if env.dev_backend_url.endswith("/api") else env.dev_backend_url
1440
+ backend_base = (
1441
+ env.dev_backend_url[:-4]
1442
+ if env.dev_backend_url.endswith("/api")
1443
+ else env.dev_backend_url
1444
+ )
1152
1445
  run_env = os.environ.copy()
1153
1446
  run_env["BACKEND_URL"] = backend_base
1154
1447
  run_env["SYNTH_API_KEY"] = env.synth_api_key
@@ -1181,13 +1474,15 @@ def cmd_run(args: argparse.Namespace) -> int:
1181
1474
  print(f" {_key_preview(sk, 'SYNTH_API_KEY')}")
1182
1475
  if ek:
1183
1476
  print(f" {_key_preview(ek, 'ENVIRONMENT_API_KEY')}")
1184
- print("Ensure the ENVIRONMENT_API_KEY you deployed with matches the task app and remains exported.")
1477
+ print(
1478
+ "Ensure the ENVIRONMENT_API_KEY you deployed with matches the task app and remains exported."
1479
+ )
1185
1480
  return code
1186
1481
 
1187
1482
  # Fallback: legacy jobs API flow
1188
1483
  with open(cfg_path, "rb") as fh:
1189
1484
  inline_cfg = tomllib.load(fh)
1190
- with open(cfg_path, "r") as fh2:
1485
+ with open(cfg_path) as fh2:
1191
1486
  toml_text = fh2.read()
1192
1487
  if args.batch_size is not None:
1193
1488
  inline_cfg.setdefault("training", {})["batch_size"] = int(args.batch_size)
@@ -1198,13 +1493,11 @@ def cmd_run(args: argparse.Namespace) -> int:
1198
1493
  # Print backend and key preview before request for clearer diagnostics
1199
1494
  try:
1200
1495
  sk = (env.synth_api_key or "").strip()
1201
- sk_len = len(sk)
1202
- sk_tail = sk[-5:] if sk_len >= 5 else sk
1203
1496
  print(f"[run] Backend API: {api}")
1204
1497
  print(f"[run] {_key_preview(sk, 'SYNTH_API_KEY')}")
1205
1498
  except Exception:
1206
1499
  pass
1207
- data_fragment: Dict[str, Any] = {
1500
+ data_fragment: dict[str, Any] = {
1208
1501
  "model": model_name,
1209
1502
  "endpoint_base_url": env.task_app_base_url,
1210
1503
  "config": inline_cfg,
@@ -1222,23 +1515,28 @@ def cmd_run(args: argparse.Namespace) -> int:
1222
1515
  if inline_cfg["compute"].get("gpu_type"):
1223
1516
  compute["gpu_type"] = str(inline_cfg["compute"]["gpu_type"]).upper()
1224
1517
  if inline_cfg["compute"].get("gpu_count"):
1225
- compute["gpu_count"] = int(inline_cfg["compute"]["gpu_count"])
1518
+ compute["gpu_count"] = int(inline_cfg["compute"]["gpu_count"])
1226
1519
  if not compute:
1227
1520
  topo = inline_cfg.get("topology") or {}
1228
1521
  gshape = str(topo.get("gpu_type") or "")
1229
1522
  if ":" in gshape:
1230
1523
  t, c = gshape.split(":", 1)
1231
1524
  compute = {"gpu_type": t.upper(), "gpu_count": int(c)}
1232
- body: Dict[str, Any] = {
1525
+ body: dict[str, Any] = {
1233
1526
  "job_type": "rl",
1234
1527
  "data": data_fragment,
1235
1528
  }
1236
1529
  if compute:
1237
1530
  body["compute"] = compute
1238
- code, js = _http("POST", api + "/rl/jobs", headers={
1239
- "Content-Type": "application/json",
1240
- "Authorization": f"Bearer {env.synth_api_key}",
1241
- }, body=body)
1531
+ code, js = _http(
1532
+ "POST",
1533
+ api + "/rl/jobs",
1534
+ headers={
1535
+ "Content-Type": "application/json",
1536
+ "Authorization": f"Bearer {env.synth_api_key}",
1537
+ },
1538
+ body=body,
1539
+ )
1242
1540
  if code not in (200, 201) or not isinstance(js, dict):
1243
1541
  print("Job create failed:", code)
1244
1542
  print(f"Backend: {api}")
@@ -1276,12 +1574,14 @@ def cmd_run(args: argparse.Namespace) -> int:
1276
1574
  try:
1277
1575
  sent_key = detail.get("sent_key")
1278
1576
  if isinstance(sent_key, str):
1279
- print(f"[run] Backend detail.sent_key {_key_preview(sent_key, 'detail.sent_key')}")
1577
+ print(
1578
+ f"[run] Backend detail.sent_key {_key_preview(sent_key, 'detail.sent_key')}"
1579
+ )
1280
1580
  except Exception:
1281
1581
  pass
1282
1582
  try:
1283
1583
  sent_keys = detail.get("sent_keys")
1284
- if isinstance(sent_keys, (list, tuple)):
1584
+ if isinstance(sent_keys, list | tuple):
1285
1585
  previews = []
1286
1586
  for idx, val in enumerate(sent_keys):
1287
1587
  if isinstance(val, str):
@@ -1306,12 +1606,19 @@ def cmd_run(args: argparse.Namespace) -> int:
1306
1606
  # Extra hints for auth failures
1307
1607
  try:
1308
1608
  sk = (env.synth_api_key or "").strip()
1309
- if int(code) == 401 or (isinstance(js, dict) and any(isinstance(v, str) and "Invalid API key" in v for v in js.values())):
1609
+ if int(code) == 401 or (
1610
+ isinstance(js, dict)
1611
+ and any(isinstance(v, str) and "Invalid API key" in v for v in js.values())
1612
+ ):
1310
1613
  base_url = env.dev_backend_url
1311
- print("Hint: HTTP 401 Unauthorized from backend. Verify SYNTH_API_KEY for:", base_url)
1614
+ print(
1615
+ "Hint: HTTP 401 Unauthorized from backend. Verify SYNTH_API_KEY for:", base_url
1616
+ )
1312
1617
  if sk:
1313
1618
  print(f" {_key_preview(sk, 'SYNTH_API_KEY')}")
1314
- print("Ensure the ENVIRONMENT_API_KEY and OPENAI_API_KEY used for deployment remain valid.")
1619
+ print(
1620
+ "Ensure the ENVIRONMENT_API_KEY and OPENAI_API_KEY used for deployment remain valid."
1621
+ )
1315
1622
  except Exception:
1316
1623
  pass
1317
1624
  return 2
@@ -1363,9 +1670,7 @@ def cmd_run(args: argparse.Namespace) -> int:
1363
1670
  "rl.performance.metrics",
1364
1671
  ):
1365
1672
  print(f"[{seq}] {typ}: {msg}")
1366
- mc, mj = _http(
1367
- "GET", api + f"/learning/jobs/{job_id}/metrics?after_step=-1&limit=50"
1368
- )
1673
+ mc, mj = _http("GET", api + f"/learning/jobs/{job_id}/metrics?after_step=-1&limit=50")
1369
1674
  if mc == 200 and isinstance(mj, dict):
1370
1675
  pts = mj.get("points") or []
1371
1676
  for p in pts:
@@ -1384,17 +1689,23 @@ def main(argv: list[str] | None = None) -> int:
1384
1689
  p = argparse.ArgumentParser(prog="synth-ai")
1385
1690
  sub = p.add_subparsers(dest="cmd")
1386
1691
 
1387
- def _add_parser(names: list[str], *, configure: Callable[[argparse.ArgumentParser], None]) -> None:
1692
+ def _add_parser(
1693
+ names: list[str], *, configure: Callable[[argparse.ArgumentParser], None]
1694
+ ) -> None:
1388
1695
  for name in names:
1389
1696
  parser = sub.add_parser(name)
1390
1697
  configure(parser)
1391
1698
 
1392
- _add_parser(["rl_demo.setup", "demo.setup"], configure=lambda parser: parser.set_defaults(func=cmd_setup))
1699
+ _add_parser(
1700
+ ["rl_demo.setup", "demo.setup"],
1701
+ configure=lambda parser: parser.set_defaults(func=cmd_setup),
1702
+ )
1393
1703
 
1394
1704
  def _init_opts(parser):
1395
1705
  parser.add_argument("--template", type=str, default=None, help="Template id to instantiate")
1396
- parser.add_argument("--dest", type=str, default=None, help="Destination directory for files")
1397
- parser.add_argument("--force", action="store_true", help="Overwrite existing files in destination")
1706
+ parser.add_argument(
1707
+ "--dest", type=str, default=None, help="Destination directory for files"
1708
+ )
1398
1709
  parser.set_defaults(func=cmd_init)
1399
1710
 
1400
1711
  _add_parser(["rl_demo.init", "demo.init"], configure=_init_opts)
@@ -1402,18 +1713,29 @@ def main(argv: list[str] | None = None) -> int:
1402
1713
  # (prepare command removed)
1403
1714
 
1404
1715
  def _deploy_opts(parser):
1405
- parser.add_argument("--local", action="store_true", help="Run local FastAPI instead of Modal deploy")
1406
- parser.add_argument("--app", type=str, default=None, help="Path to Modal app.py for uv run modal deploy")
1716
+ parser.add_argument(
1717
+ "--local", action="store_true", help="Run local FastAPI instead of Modal deploy"
1718
+ )
1719
+ parser.add_argument(
1720
+ "--app", type=str, default=None, help="Path to Modal app.py for uv run modal deploy"
1721
+ )
1407
1722
  parser.add_argument("--name", type=str, default=None, help="Modal app name")
1408
- parser.add_argument("--script", type=str, default=None, help="Path to deploy_task_app.sh (optional legacy)")
1723
+ parser.add_argument(
1724
+ "--script", type=str, default=None, help="Path to deploy_task_app.sh (optional legacy)"
1725
+ )
1409
1726
  parser.set_defaults(func=cmd_deploy)
1410
1727
 
1411
1728
  _add_parser(["rl_demo.deploy", "demo.deploy"], configure=_deploy_opts)
1412
1729
 
1413
- _add_parser(["rl_demo.configure", "demo.configure"], configure=lambda parser: parser.set_defaults(func=cmd_run))
1730
+ _add_parser(
1731
+ ["rl_demo.configure", "demo.configure"],
1732
+ configure=lambda parser: parser.set_defaults(func=cmd_run),
1733
+ )
1414
1734
 
1415
1735
  def _run_opts(parser):
1416
- parser.add_argument("--config", type=str, default=None, help="Path to TOML config (skip prompt)")
1736
+ parser.add_argument(
1737
+ "--config", type=str, default=None, help="Path to TOML config (skip prompt)"
1738
+ )
1417
1739
  parser.add_argument("--batch-size", type=int, default=None)
1418
1740
  parser.add_argument("--group-size", type=int, default=None)
1419
1741
  parser.add_argument("--model", type=str, default=None)