synth-ai 0.2.9.dev7__py3-none-any.whl → 0.2.9.dev9__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 (327) hide show
  1. examples/__init__.py +16 -0
  2. examples/crafter_debug_render.py +8 -11
  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/run_eval.py +36 -37
  19. examples/rl/run_rl_and_save.py +5 -5
  20. examples/rl/task_app/math_single_step.py +65 -43
  21. examples/rl/task_app/math_task_app.py +3 -3
  22. examples/sft/README.md +139 -0
  23. examples/sft/configs/crafter_fft_qwen0p6b.toml +44 -0
  24. examples/sft/configs/crafter_lora_qwen0p6b.toml +45 -0
  25. examples/sft/evaluate.py +117 -0
  26. examples/sft/export_dataset.py +117 -0
  27. examples/sft/generate_traces.py +162 -0
  28. examples/swe/__init__.py +12 -0
  29. examples/swe/task_app/README.md +105 -0
  30. examples/swe/task_app/__init__.py +2 -0
  31. examples/swe/task_app/grpo_swe_mini.py +571 -0
  32. examples/swe/task_app/grpo_swe_mini_task_app.py +136 -0
  33. examples/swe/task_app/hosted/README.md +173 -0
  34. examples/swe/task_app/hosted/__init__.py +5 -0
  35. examples/swe/task_app/hosted/branching.py +143 -0
  36. examples/swe/task_app/hosted/environment_routes.py +1289 -0
  37. examples/swe/task_app/hosted/envs/__init__.py +1 -0
  38. examples/swe/task_app/hosted/envs/crafter/__init__.py +6 -0
  39. examples/swe/task_app/hosted/envs/crafter/app.py +1 -0
  40. examples/swe/task_app/hosted/envs/crafter/environment.py +522 -0
  41. examples/swe/task_app/hosted/envs/crafter/policy.py +478 -0
  42. examples/swe/task_app/hosted/envs/crafter/react_agent.py +108 -0
  43. examples/swe/task_app/hosted/envs/crafter/shared.py +305 -0
  44. examples/swe/task_app/hosted/envs/crafter/tools.py +47 -0
  45. examples/swe/task_app/hosted/envs/mini_swe/__init__.py +8 -0
  46. examples/swe/task_app/hosted/envs/mini_swe/environment.py +1164 -0
  47. examples/swe/task_app/hosted/envs/mini_swe/policy.py +355 -0
  48. examples/swe/task_app/hosted/envs/mini_swe/shared.py +83 -0
  49. examples/swe/task_app/hosted/envs/mini_swe/tools.py +96 -0
  50. examples/swe/task_app/hosted/hosted_app.py +204 -0
  51. examples/swe/task_app/hosted/inference/__init__.py +5 -0
  52. examples/swe/task_app/hosted/inference/openai_client.py +618 -0
  53. examples/swe/task_app/hosted/main.py +100 -0
  54. examples/swe/task_app/hosted/policy_routes.py +1079 -0
  55. examples/swe/task_app/hosted/registry.py +195 -0
  56. examples/swe/task_app/hosted/rollout.py +1869 -0
  57. examples/swe/task_app/hosted/storage/__init__.py +5 -0
  58. examples/swe/task_app/hosted/storage/volume.py +211 -0
  59. examples/swe/task_app/hosted/test_agents.py +161 -0
  60. examples/swe/task_app/hosted/test_service.py +137 -0
  61. examples/swe/task_app/hosted/utils.py +62 -0
  62. examples/vlm/README.md +68 -0
  63. examples/vlm/configs/crafter_vlm_gpt4o.toml +44 -0
  64. examples/vlm/crafter_image_only_agent.py +207 -0
  65. examples/vlm/crafter_openai_vlm_agent.py +277 -0
  66. examples/vlm/filter_image_rows.py +63 -0
  67. examples/vlm/run_crafter_vlm_benchmark.py +316 -0
  68. examples/warming_up_to_rl/analyze_trace_db.py +5 -5
  69. examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +11 -1
  70. examples/warming_up_to_rl/export_trace_sft.py +78 -21
  71. examples/warming_up_to_rl/groq_test.py +4 -4
  72. examples/warming_up_to_rl/manage_secrets.py +13 -18
  73. examples/warming_up_to_rl/run_eval.py +42 -44
  74. examples/warming_up_to_rl/run_fft_and_save.py +11 -16
  75. examples/warming_up_to_rl/run_local_rollout.py +1 -3
  76. examples/warming_up_to_rl/run_local_rollout_modal.py +2 -4
  77. examples/warming_up_to_rl/run_local_rollout_parallel.py +1 -4
  78. examples/warming_up_to_rl/run_local_rollout_traced.py +3 -5
  79. examples/warming_up_to_rl/run_rl_and_save.py +5 -6
  80. examples/warming_up_to_rl/run_rollout_remote.py +8 -10
  81. examples/warming_up_to_rl/task_app/README.md +6 -2
  82. examples/warming_up_to_rl/task_app/grpo_crafter.py +234 -35
  83. examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +2 -3
  84. examples/warming_up_to_rl/task_app/synth_envs_hosted/__init__.py +1 -1
  85. examples/warming_up_to_rl/task_app/synth_envs_hosted/branching.py +9 -11
  86. examples/warming_up_to_rl/task_app/synth_envs_hosted/environment_routes.py +131 -114
  87. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/environment.py +101 -41
  88. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +73 -51
  89. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +14 -6
  90. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/shared.py +16 -16
  91. examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +32 -34
  92. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +94 -31
  93. examples/warming_up_to_rl/task_app/synth_envs_hosted/main.py +0 -2
  94. examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +303 -203
  95. examples/warming_up_to_rl/task_app/synth_envs_hosted/registry.py +21 -23
  96. examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +328 -225
  97. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/volume.py +13 -13
  98. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_agents.py +1 -0
  99. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_service.py +1 -0
  100. examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +4 -3
  101. synth/__init__.py +14 -0
  102. synth_ai/__init__.py +26 -4
  103. synth_ai/api/models/supported.py +376 -0
  104. synth_ai/api/train/builders.py +128 -21
  105. synth_ai/api/train/cli.py +80 -64
  106. synth_ai/api/train/config_finder.py +7 -2
  107. synth_ai/api/train/env_resolver.py +1 -1
  108. synth_ai/api/train/pollers.py +2 -1
  109. synth_ai/api/train/supported_algos.py +139 -0
  110. synth_ai/api/train/task_app.py +1 -2
  111. synth_ai/api/train/utils.py +13 -44
  112. synth_ai/cli/__init__.py +8 -0
  113. synth_ai/cli/_modal_wrapper.py +28 -0
  114. synth_ai/cli/_typer_patch.py +49 -0
  115. synth_ai/cli/balance.py +1 -2
  116. synth_ai/cli/calc.py +1 -1
  117. synth_ai/cli/demo.py +2 -1
  118. synth_ai/cli/recent.py +2 -2
  119. synth_ai/cli/rl_demo.py +2 -1
  120. synth_ai/cli/root.py +11 -13
  121. synth_ai/cli/status.py +2 -2
  122. synth_ai/cli/task_apps.py +529 -179
  123. synth_ai/cli/traces.py +6 -4
  124. synth_ai/cli/watch.py +12 -18
  125. synth_ai/demo_registry.py +1 -1
  126. synth_ai/demos/core/cli.py +36 -43
  127. synth_ai/demos/demo_task_apps/__init__.py +3 -3
  128. synth_ai/demos/demo_task_apps/core.py +17 -25
  129. synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +3 -4
  130. synth_ai/demos/demo_task_apps/math/app.py +2 -1
  131. synth_ai/demos/demo_task_apps/math/deploy_modal.py +3 -4
  132. synth_ai/demos/demo_task_apps/math/modal_task_app.py +16 -18
  133. synth_ai/demos/demo_task_apps/math/task_app_entry.py +0 -1
  134. synth_ai/environments/examples/crafter_classic/environment.py +76 -1
  135. synth_ai/environments/reproducibility/tree.py +2 -5
  136. synth_ai/environments/service/app.py +11 -12
  137. synth_ai/environments/service/core_routes.py +4 -7
  138. synth_ai/environments/stateful/engine.py +1 -1
  139. synth_ai/environments/tasks/core.py +1 -0
  140. synth_ai/environments/tasks/filters.py +5 -6
  141. synth_ai/environments/tasks/utils.py +4 -5
  142. synth_ai/handshake.py +9 -9
  143. synth_ai/http.py +1 -1
  144. synth_ai/http_client.py +18 -10
  145. synth_ai/inference/client.py +15 -5
  146. synth_ai/jobs/client.py +78 -83
  147. synth_ai/learning/__init__.py +41 -6
  148. synth_ai/learning/algorithms.py +14 -0
  149. synth_ai/learning/client.py +91 -24
  150. synth_ai/learning/config.py +2 -38
  151. synth_ai/learning/ft_client.py +4 -59
  152. synth_ai/learning/health.py +5 -6
  153. synth_ai/learning/jobs.py +31 -47
  154. synth_ai/{rl → learning/rl}/__init__.py +14 -4
  155. synth_ai/learning/rl/client.py +267 -0
  156. synth_ai/learning/rl/config.py +31 -0
  157. synth_ai/{rl → learning/rl}/contracts.py +5 -8
  158. synth_ai/{rl → learning/rl}/env_keys.py +39 -15
  159. synth_ai/learning/rl/secrets.py +13 -0
  160. synth_ai/learning/rl_client.py +2 -281
  161. synth_ai/learning/sft/__init__.py +29 -0
  162. synth_ai/learning/sft/client.py +68 -0
  163. synth_ai/learning/sft/config.py +270 -0
  164. synth_ai/learning/sft/data.py +295 -0
  165. synth_ai/learning/sse.py +25 -24
  166. synth_ai/learning/validators.py +25 -28
  167. synth_ai/lm/__init__.py +21 -47
  168. synth_ai/main.py +6 -0
  169. synth_ai/task/__init__.py +25 -27
  170. synth_ai/task/apps/__init__.py +7 -8
  171. synth_ai/task/auth.py +8 -8
  172. synth_ai/task/client.py +14 -14
  173. synth_ai/task/contracts.py +36 -35
  174. synth_ai/task/datasets.py +6 -5
  175. synth_ai/task/errors.py +10 -10
  176. synth_ai/task/health.py +17 -9
  177. synth_ai/task/json.py +58 -23
  178. synth_ai/task/proxy.py +13 -9
  179. synth_ai/task/rubrics.py +16 -15
  180. synth_ai/task/server.py +12 -12
  181. synth_ai/task/tracing_utils.py +4 -4
  182. synth_ai/task/vendors.py +5 -6
  183. synth_ai/tracing_v3/__init__.py +2 -0
  184. synth_ai/tracing_v3/abstractions.py +21 -4
  185. synth_ai/tracing_v3/decorators.py +18 -16
  186. synth_ai/tracing_v3/hooks.py +5 -5
  187. synth_ai/tracing_v3/llm_call_record_helpers.py +6 -6
  188. synth_ai/tracing_v3/session_tracer.py +40 -14
  189. synth_ai/tracing_v3/storage/base.py +85 -0
  190. synth_ai/tracing_v3/storage/config.py +21 -8
  191. synth_ai/tracing_v3/storage/factory.py +10 -7
  192. synth_ai/tracing_v3/storage/utils.py +4 -2
  193. synth_ai/tracing_v3/turso/daemon.py +7 -2
  194. synth_ai/tracing_v3/turso/models.py +2 -2
  195. synth_ai/tracing_v3/turso/native_manager.py +1173 -0
  196. synth_ai/tracing_v3/utils.py +4 -4
  197. synth_ai/v0/api/__init__.py +8 -0
  198. synth_ai/v0/api/models/__init__.py +8 -0
  199. synth_ai/v0/api/models/supported.py +8 -0
  200. synth_ai/v0/config/__init__.py +15 -0
  201. synth_ai/v0/config/base_url.py +12 -0
  202. synth_ai/v0/lm/__init__.py +51 -0
  203. synth_ai/{lm → v0/lm}/caching/ephemeral.py +2 -2
  204. synth_ai/{lm → v0/lm}/caching/handler.py +4 -4
  205. synth_ai/{lm → v0/lm}/caching/initialize.py +1 -1
  206. synth_ai/{lm → v0/lm}/caching/persistent.py +1 -1
  207. synth_ai/{lm → v0/lm}/config.py +6 -1
  208. synth_ai/{lm → v0/lm}/core/all.py +9 -9
  209. synth_ai/{lm → v0/lm}/core/main.py +6 -6
  210. synth_ai/{lm → v0/lm}/core/main_v3.py +10 -10
  211. synth_ai/{lm → v0/lm}/core/synth_models.py +2 -14
  212. synth_ai/{lm → v0/lm}/core/vendor_clients.py +2 -2
  213. synth_ai/{lm → v0/lm}/overrides.py +2 -2
  214. synth_ai/{lm → v0/lm}/provider_support/anthropic.py +4 -4
  215. synth_ai/{lm → v0/lm}/provider_support/openai.py +5 -5
  216. synth_ai/{lm → v0/lm}/structured_outputs/handler.py +5 -5
  217. synth_ai/{lm → v0/lm}/structured_outputs/rehabilitate.py +1 -1
  218. synth_ai/{lm → v0/lm}/vendors/core/anthropic_api.py +9 -9
  219. synth_ai/{lm → v0/lm}/vendors/core/gemini_api.py +5 -5
  220. synth_ai/{lm → v0/lm}/vendors/core/mistral_api.py +5 -5
  221. synth_ai/{lm → v0/lm}/vendors/core/openai_api.py +10 -10
  222. synth_ai/{lm → v0/lm}/vendors/openai_standard.py +8 -8
  223. synth_ai/{lm → v0/lm}/vendors/openai_standard_responses.py +2 -2
  224. synth_ai/{lm → v0/lm}/vendors/supported/custom_endpoint.py +3 -3
  225. synth_ai/{lm → v0/lm}/vendors/supported/deepseek.py +2 -2
  226. synth_ai/{lm → v0/lm}/vendors/supported/grok.py +2 -2
  227. synth_ai/{lm → v0/lm}/vendors/supported/groq.py +1 -1
  228. synth_ai/{lm → v0/lm}/vendors/supported/ollama.py +1 -1
  229. synth_ai/{lm → v0/lm}/vendors/supported/openrouter.py +3 -3
  230. synth_ai/{lm → v0/lm}/vendors/supported/together.py +1 -1
  231. synth_ai/{lm → v0/lm}/vendors/synth_client.py +1 -1
  232. synth_ai/v0/tracing_v3/__init__.py +10 -0
  233. synth_ai/v0/tracing_v3/abstractions.py +3 -0
  234. synth_ai/v0/tracing_v3/decorators.py +3 -0
  235. synth_ai/v0/tracing_v3/llm_call_record_helpers.py +3 -0
  236. synth_ai/v0/tracing_v3/session_tracer.py +3 -0
  237. synth_ai-0.2.9.dev9.dist-info/METADATA +191 -0
  238. {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.9.dev9.dist-info}/RECORD +268 -238
  239. {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.9.dev9.dist-info}/top_level.txt +1 -0
  240. examples/common_old/backend.py +0 -20
  241. examples/evals_old/README.md +0 -98
  242. examples/evals_old/__init__.py +0 -6
  243. examples/evals_old/compare_models.py +0 -1038
  244. examples/evals_old/example_log.md +0 -145
  245. examples/evals_old/run_demo.sh +0 -126
  246. examples/evals_old/trace_analysis.py +0 -270
  247. examples/finetuning_old/_backup_synth_qwen/config.toml +0 -29
  248. examples/finetuning_old/_backup_synth_qwen/example_log.md +0 -324
  249. examples/finetuning_old/_backup_synth_qwen/filter_traces.py +0 -60
  250. examples/finetuning_old/_backup_synth_qwen/filter_traces_achievements.py +0 -243
  251. examples/finetuning_old/_backup_synth_qwen/purge_v3_traces.py +0 -109
  252. examples/finetuning_old/_backup_synth_qwen/react_agent_lm.py +0 -1924
  253. examples/finetuning_old/_backup_synth_qwen/readme.md +0 -49
  254. examples/finetuning_old/_backup_synth_qwen/run_crafter_qwen4b.py +0 -114
  255. examples/finetuning_old/_backup_synth_qwen/run_demo.sh +0 -195
  256. examples/finetuning_old/_backup_synth_qwen/sft_kickoff.py +0 -119
  257. examples/finetuning_old/synth_qwen_v1/README.md +0 -68
  258. examples/finetuning_old/synth_qwen_v1/filter_traces.py +0 -60
  259. examples/finetuning_old/synth_qwen_v1/filter_traces_achievements.py +0 -243
  260. examples/finetuning_old/synth_qwen_v1/finetune.py +0 -46
  261. examples/finetuning_old/synth_qwen_v1/hello_ft_model.py +0 -71
  262. examples/finetuning_old/synth_qwen_v1/infer.py +0 -36
  263. examples/finetuning_old/synth_qwen_v1/poll.py +0 -46
  264. examples/finetuning_old/synth_qwen_v1/prepare_data.py +0 -35
  265. examples/finetuning_old/synth_qwen_v1/purge_v3_traces.py +0 -109
  266. examples/finetuning_old/synth_qwen_v1/react_agent_lm.py +0 -1933
  267. examples/finetuning_old/synth_qwen_v1/run_crafter_sft_job.py +0 -210
  268. examples/finetuning_old/synth_qwen_v1/run_ft_job.py +0 -237
  269. examples/finetuning_old/synth_qwen_v1/upload_data.py +0 -34
  270. examples/finetuning_old/synth_qwen_v1/util.py +0 -152
  271. examples/rl_old/task_app.py +0 -1131
  272. examples/warming_up_to_rl/old/event_rewards.md +0 -234
  273. examples/warming_up_to_rl/old/notes.md +0 -73
  274. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/filter_traces_sft_turso.py +0 -738
  275. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/filter_traces_sft_turso.py +0 -580
  276. synth_ai/experimental/synth_oss.py +0 -445
  277. synth_ai/learning/filtering.py +0 -0
  278. synth_ai/learning/offline/dpo.py +0 -0
  279. synth_ai/learning/offline/providers.py +0 -7
  280. synth_ai/learning/offline/sft.py +0 -0
  281. synth_ai/learning/offline/shared.py +0 -0
  282. synth_ai/learning/online/grpo.py +0 -0
  283. synth_ai/learning/online/irft.py +0 -0
  284. synth_ai/learning/prompts/banking77_injection_eval.py +0 -168
  285. synth_ai/learning/prompts/gepa.py +0 -0
  286. synth_ai/learning/prompts/hello_world_in_context_injection_ex.py +0 -211
  287. synth_ai/learning/prompts/mipro.py +0 -289
  288. synth_ai/learning/prompts/random_search.py +0 -249
  289. synth_ai/learning/prompts/run_mipro_banking77.py +0 -172
  290. synth_ai/learning/prompts/run_random_search_banking77.py +0 -329
  291. synth_ai/rl/secrets.py +0 -19
  292. synth_ai/scripts/verify_rewards.py +0 -100
  293. synth_ai/tracing/__init__.py +0 -30
  294. synth_ai/tracing_v1/__init__.py +0 -33
  295. synth_ai/tracing_v3/turso/__init__.py +0 -25
  296. synth_ai/tracing_v3/turso/manager.py +0 -838
  297. synth_ai/zyk/__init__.py +0 -30
  298. synth_ai-0.2.9.dev7.dist-info/METADATA +0 -131
  299. /synth_ai/{lm → v0/lm}/caching/__init__.py +0 -0
  300. /synth_ai/{lm → v0/lm}/caching/constants.py +0 -0
  301. /synth_ai/{lm → v0/lm}/caching/dbs.py +0 -0
  302. /synth_ai/{lm → v0/lm}/constants.py +0 -0
  303. /synth_ai/{lm → v0/lm}/core/__init__.py +0 -0
  304. /synth_ai/{lm → v0/lm}/core/exceptions.py +0 -0
  305. /synth_ai/{lm → v0/lm}/cost/__init__.py +0 -0
  306. /synth_ai/{lm → v0/lm}/cost/monitor.py +0 -0
  307. /synth_ai/{lm → v0/lm}/cost/statefulness.py +0 -0
  308. /synth_ai/{lm → v0/lm}/injection.py +0 -0
  309. /synth_ai/{lm → v0/lm}/provider_support/__init__.py +0 -0
  310. /synth_ai/{lm → v0/lm}/provider_support/suppress_logging.py +0 -0
  311. /synth_ai/{lm → v0/lm}/structured_outputs/__init__.py +0 -0
  312. /synth_ai/{lm → v0/lm}/structured_outputs/inject.py +0 -0
  313. /synth_ai/{lm → v0/lm}/tools/__init__.py +0 -0
  314. /synth_ai/{lm → v0/lm}/tools/base.py +0 -0
  315. /synth_ai/{lm → v0/lm}/unified_interface.py +0 -0
  316. /synth_ai/{lm → v0/lm}/vendors/__init__.py +0 -0
  317. /synth_ai/{lm → v0/lm}/vendors/base.py +0 -0
  318. /synth_ai/{lm → v0/lm}/vendors/core/__init__.py +0 -0
  319. /synth_ai/{lm → v0/lm}/vendors/core/synth_dev_api.py +0 -0
  320. /synth_ai/{lm → v0/lm}/vendors/local/__init__.py +0 -0
  321. /synth_ai/{lm → v0/lm}/vendors/local/ollama.py +0 -0
  322. /synth_ai/{lm → v0/lm}/vendors/retries.py +0 -0
  323. /synth_ai/{lm → v0/lm}/vendors/supported/__init__.py +0 -0
  324. /synth_ai/{lm → v0/lm}/warmup.py +0 -0
  325. {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.9.dev9.dist-info}/WHEEL +0 -0
  326. {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.9.dev9.dist-info}/entry_points.txt +0 -0
  327. {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.9.dev9.dist-info}/licenses/LICENSE +0 -0
@@ -1,210 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Crafter → SFT end-to-end runner (single script).
4
-
5
- Pipeline:
6
- 1) Read v3 traces DB (sqld/Turso) and filter sessions (achievements >= min)
7
- 2) Export OpenAI-format JSONL
8
- 3) Upload file, create/start SFT job, poll to terminal
9
- 4) (Optional) quick inference with the resulting model
10
-
11
- Usage:
12
- uv run python examples/finetuning/synth_qwen_v1/run_crafter_sft_job.py --mode dev \
13
- --db /Users/joshpurtell/Documents/GitHub/synth-ai/traces/v3/synth_ai.db/dbs/default/data \
14
- --min-achievements 2 --output examples/finetuning/synth_qwen_v1/data/training_crafter.jsonl
15
- """
16
-
17
- from __future__ import annotations
18
-
19
- import argparse
20
- import asyncio
21
- import json
22
- import os
23
- import sys
24
- from pathlib import Path
25
- from typing import Any
26
-
27
- # Repo root on sys.path for local runs
28
- sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))))
29
-
30
- from synth_ai.learning import FtClient, JobHandle, validate_training_jsonl # type: ignore
31
- from synth_ai.inference import InferenceClient # type: ignore
32
- from examples.finetuning.synth_qwen_v1.util import load_env, save_state # type: ignore
33
-
34
-
35
- def parse_args() -> argparse.Namespace:
36
- p = argparse.ArgumentParser(description="Crafter traces → SFT JSONL → FT job runner")
37
- p.add_argument("--mode", choices=["local", "dev", "prod"], default=None)
38
- p.add_argument(
39
- "--db",
40
- default=str(Path(__file__).resolve().parents[3] / "traces/v3/synth_ai.db/dbs/default/data"),
41
- help="Path to sqld internal data file or sqlite+aiosqlite URL",
42
- )
43
- p.add_argument(
44
- "--output", default=str(Path(__file__).parent / "data" / "training_crafter.jsonl")
45
- )
46
- p.add_argument("--min-achievements", type=int, default=2)
47
- p.add_argument("--max-cost", type=float, default=10.0)
48
- p.add_argument("--max-tokens", type=int, default=100000)
49
- p.add_argument("--model", default="Qwen/Qwen3-0.6B")
50
- p.add_argument("--epochs", type=int, default=1)
51
- p.add_argument("--batch-size", type=int, default=4)
52
- p.add_argument("--no-infer", action="store_true")
53
- p.add_argument("--models", nargs="*", help="Optional model name filter (any match)")
54
- return p.parse_args()
55
-
56
-
57
- def _normalize_db_url(raw: str) -> str:
58
- if raw.endswith(".db") and not raw.startswith("sqlite"):
59
- return f"sqlite+aiosqlite:///{raw}"
60
- if raw.startswith("sqlite+aiosqlite:///"):
61
- return raw
62
- if raw.startswith("sqlite:///") and raw.endswith(".db"):
63
- return raw.replace("sqlite:///", "sqlite+aiosqlite:///")
64
- return raw
65
-
66
-
67
- async def extract_jsonl_from_traces(db_url: str, output_path: str, cfg: dict[str, Any]) -> int:
68
- # Import extractor with robust fallbacks across dist variants
69
- Extractor = None
70
- try:
71
- from synth_ai.environments.examples.crafter_classic.agent_demos.crafter_modal_ft.filter_traces_sft_turso import ( # type: ignore
72
- FinetuningDataExtractorV3 as _Ex,
73
- )
74
-
75
- Extractor = _Ex
76
- except Exception:
77
- try:
78
- from synth_ai.environments.examples.crafter_classic.agent_demos.crafter_openai_ft.filter_traces_sft_turso import ( # type: ignore
79
- FinetuningDataExtractorV3 as _Ex,
80
- )
81
-
82
- Extractor = _Ex
83
- except Exception as e:
84
- raise ImportError("FinetuningDataExtractorV3 not available in current build") from e
85
-
86
- filters: dict[str, Any] = cfg.get("filters", {})
87
- min_ach = int(filters.get("min_achievements", 2))
88
- max_cost = float(filters.get("max_cost", 10.0))
89
- max_tokens = int(filters.get("max_tokens", 100000))
90
- models: list[str] = list(filters.get("models", []) or [])
91
-
92
- kept: list[str] = []
93
- async with Extractor(db_url) as ex:
94
- sessions = await ex.get_all_sessions()
95
- for _, row in sessions.iterrows():
96
- sid = row["session_id"]
97
- metrics = await ex.get_session_metrics(sid)
98
- if float(metrics.get("total_cost", 0.0)) > max_cost:
99
- continue
100
- if int(metrics.get("total_tokens", 0) or 0) > max_tokens:
101
- continue
102
- # Optional model filter
103
- if models:
104
- model_df = await ex.db_manager.query_traces(
105
- """
106
- SELECT DISTINCT model_name
107
- FROM events
108
- WHERE session_id = :session_id
109
- AND event_type = 'cais'
110
- AND model_name IS NOT NULL
111
- """,
112
- {"session_id": sid},
113
- )
114
- session_models = (
115
- model_df["model_name"].tolist()
116
- if model_df is not None and not model_df.empty
117
- else []
118
- )
119
- if not any(m in session_models for m in models):
120
- continue
121
- ach = await ex.get_session_achievements(sid) or []
122
- if len([a for a in ach if a]) >= min_ach:
123
- kept.append(sid)
124
-
125
- data = await ex.extract_openai_format(kept)
126
- Path(output_path).parent.mkdir(parents=True, exist_ok=True)
127
- with open(output_path, "w") as f:
128
- for exm in data:
129
- f.write(json.dumps(exm) + "\n")
130
- return len(data)
131
-
132
-
133
- async def run(args: argparse.Namespace) -> None:
134
- base_url, api_key = load_env(args.mode)
135
-
136
- # 1) Filter and export JSONL from v3 traces
137
- db_url = _normalize_db_url(args.db)
138
- cfg = {
139
- "mode": "trajectory",
140
- "filters": {
141
- "min_achievements": int(args.min_achievements),
142
- "max_cost": float(args.max_cost),
143
- "max_tokens": int(args.max_tokens),
144
- "models": args.models or [],
145
- },
146
- }
147
- out_path = str(Path(args.output))
148
- print("Extracting SFT data from traces…")
149
- n = await extract_jsonl_from_traces(db_url, out_path, cfg)
150
- print(f"✅ Wrote {n} examples → {out_path}")
151
-
152
- # 2) Validate JSONL
153
- validate_training_jsonl(out_path)
154
-
155
- # 3) Upload and create FT job
156
- client = FtClient(base_url=base_url, api_key=api_key)
157
- file_id = await client.upload_training_file(Path(out_path), purpose="fine-tune")
158
- print(f"file_id={file_id}")
159
- save_state({"file_id": file_id})
160
-
161
- create = await client.create_sft_job(
162
- model=str(args.model),
163
- training_file_id=file_id,
164
- hyperparameters={"n_epochs": int(args.epochs), "batch_size": int(args.batch_size)},
165
- metadata={"upload_to_wasabi": True},
166
- )
167
- job_id = (create or {}).get("job_id")
168
- if not job_id:
169
- raise RuntimeError(f"create_sft_job missing job_id: {create}")
170
- print(f"job_id={job_id}")
171
- save_state({"job_id": job_id})
172
-
173
- start = await client.start_job(job_id)
174
- print(f"start={start}")
175
-
176
- # 4) Poll to terminal
177
- handle = JobHandle(base_url, api_key, job_id, strict=True)
178
- final = await handle.poll_until_terminal(interval_seconds=2.0, max_seconds=1800)
179
- status = (final or {}).get("status")
180
- print(f"final_status={status}")
181
- ft_model = (final or {}).get("fine_tuned_model")
182
- if ft_model:
183
- save_state({"fine_tuned_model": ft_model})
184
- print(f"fine_tuned_model={ft_model}")
185
-
186
- # 5) Optional inference check
187
- if not args.no_infer:
188
- try:
189
- ic = InferenceClient(base_url=base_url, api_key=api_key)
190
- model_for_infer = ft_model or str(args.model)
191
- print(f"\nInference sanity check (model={model_for_infer})…")
192
- resp = await ic.create_chat_completion(
193
- model=model_for_infer,
194
- messages=[{"role": "user", "content": "Give me a cheerful two-line greeting."}],
195
- max_tokens=128,
196
- temperature=0.7,
197
- stream=False,
198
- )
199
- print(resp)
200
- except Exception as e:
201
- print(f"(inference skipped due to error: {e})")
202
-
203
-
204
- def main() -> None:
205
- args = parse_args()
206
- asyncio.run(run(args))
207
-
208
-
209
- if __name__ == "__main__":
210
- main()
@@ -1,237 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- End-to-end SFT workflow for Qwen (single script).
4
-
5
- Steps performed:
6
- 1) Ensure/validate training JSONL (creates a minimal one if missing)
7
- 2) Upload training file → save file_id to state.json
8
- 3) Create SFT job (Qwen/Qwen3-0.6B by default) and start → save job_id
9
- 4) Poll until terminal → save fine_tuned_model when available
10
- 5) (Optional) Quick inference with the fine-tuned model (or base if absent)
11
-
12
- Usage:
13
- uv run python examples/finetuning/synth_qwen_v1/run_ft_job.py --mode dev
14
-
15
- Options:
16
- --mode {local,dev,prod} Backend mode/environment (default: env override or prod)
17
- --data PATH Path to training JSONL (default: ./data/training.jsonl)
18
- --model NAME Base model for SFT (default: Qwen/Qwen3-0.6B)
19
- --epochs N Epochs (default: 1)
20
- --batch-size N Batch size (default: 4)
21
- --no-infer Skip the post-training inference check
22
- """
23
-
24
- from __future__ import annotations
25
-
26
- import argparse
27
- import asyncio
28
- import json
29
- import os
30
- import sys
31
- from pathlib import Path
32
- from typing import Any
33
-
34
- # Make repo root importable when running directly
35
- sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))))
36
-
37
- from synth_ai.config.base_url import get_backend_from_env
38
- from synth_ai.learning import FtClient, JobHandle, validate_training_jsonl # type: ignore
39
- from synth_ai.inference import InferenceClient # type: ignore
40
- from examples.finetuning.synth_qwen_v1.util import load_env, load_state, save_state # type: ignore
41
-
42
- try:
43
- from examples.common.backend import resolve_backend_url as _resolve_backend_default # type: ignore
44
- except Exception: # pragma: no cover - fallback for direct execution
45
-
46
- def _resolve_backend_default() -> str:
47
- base, _ = get_backend_from_env()
48
- base = base.rstrip("/")
49
- return base if base.endswith("/api") else f"{base}/api"
50
-
51
-
52
- def parse_args() -> argparse.Namespace:
53
- p = argparse.ArgumentParser(description="Run Qwen SFT end-to-end")
54
- p.add_argument("--mode", choices=["prod", "dev", "local"], default=None)
55
- p.add_argument("--data", default=str(Path(__file__).parent / "data" / "training_crafter.jsonl"))
56
- p.add_argument("--model", default="Qwen/Qwen3-0.6B")
57
- p.add_argument("--epochs", type=int, default=1)
58
- p.add_argument("--batch-size", type=int, default=4, dest="batch_size")
59
- p.add_argument("--no-infer", action="store_true")
60
- return p.parse_args()
61
-
62
-
63
- def ensure_training_jsonl(path: Path) -> Path:
64
- path.parent.mkdir(parents=True, exist_ok=True)
65
- if not path.exists():
66
- # Minimal JSONL with a single example
67
- lines: list[str] = [
68
- json.dumps(
69
- {
70
- "messages": [
71
- {"role": "user", "content": "Write a short greeting."},
72
- {"role": "assistant", "content": "Hello there!"},
73
- ]
74
- }
75
- )
76
- ]
77
- path.write_text("\n".join(lines) + "\n")
78
- # Validate using shared SDK validator
79
- validate_training_jsonl(path)
80
- return path
81
-
82
-
83
- async def run(args: argparse.Namespace) -> None:
84
- # Resolve backend and key
85
- base_url, api_key = load_env(args.mode)
86
- # Force canonical prod base when prod mode (or override) is selected
87
- try:
88
- if (args.mode == "prod") or (
89
- os.getenv("SYNTH_BACKEND_URL_OVERRIDE", "").strip().lower() == "prod"
90
- ):
91
- base_url = _resolve_backend_default()
92
- # Also export for any downstream helpers that read env
93
- os.environ["PROD_BACKEND_URL"] = base_url
94
- except Exception:
95
- pass
96
-
97
- # Ensure/validate training JSONL
98
- data_path = ensure_training_jsonl(Path(args.data))
99
- print(f"Training JSONL: {data_path}")
100
-
101
- # Upload file
102
- ft = FtClient(base_url=base_url, api_key=api_key)
103
- file_id = await ft.upload_training_file(data_path, purpose="fine-tune")
104
- if not file_id:
105
- raise RuntimeError("upload_training_file returned empty file_id")
106
- print(f"file_id={file_id}")
107
- save_state({"file_id": file_id})
108
-
109
- # Create job
110
- hyperparameters: dict[str, Any] = {
111
- "n_epochs": int(args.epochs),
112
- "batch_size": int(args.batch_size),
113
- }
114
- # Include explicit compute topology for billing/inference resolution.
115
- # Default: 1x A10G (can be surfaced via CLI later if needed).
116
- metadata = {
117
- "upload_to_wasabi": True,
118
- # Normalized effective config consumed by the backend SFT workflow
119
- "effective_config": {
120
- "compute": {
121
- "gpu_type": "A10G",
122
- "gpu_count": 1,
123
- "nodes": 1,
124
- },
125
- "data": {
126
- "topology": {
127
- "gpu_type": "A10G",
128
- "container_count": 1,
129
- }
130
- },
131
- },
132
- }
133
-
134
- create_resp = await ft.create_sft_job(
135
- model=str(args.model),
136
- training_file_id=file_id,
137
- hyperparameters=hyperparameters,
138
- metadata=metadata,
139
- )
140
- job_id = (create_resp or {}).get("job_id")
141
- if not job_id:
142
- raise RuntimeError(f"create_sft_job missing job_id: {create_resp}")
143
- print(f"job_id={job_id}")
144
- save_state({"job_id": job_id})
145
-
146
- # Start job
147
- start_resp = await ft.start_job(job_id)
148
- print(f"start={start_resp}")
149
-
150
- # Poll until terminal with streaming event/metric logs
151
- def _on_event(e: dict[str, Any]) -> None:
152
- try:
153
- seq = e.get("seq")
154
- etype = e.get("type") or e.get("event_type")
155
- msg = e.get("message")
156
- print(f"event seq={seq} type={etype} msg={msg}")
157
- except Exception:
158
- pass
159
-
160
- def _on_metric(p: dict[str, Any]) -> None:
161
- try:
162
- name = str(p.get("name") or "")
163
- step = p.get("step")
164
- epoch = p.get("epoch")
165
- val = p.get("value")
166
- print(f"metric {name} step={step} epoch={epoch} value={val}")
167
- except Exception:
168
- pass
169
-
170
- handle = JobHandle(base_url, api_key, job_id, strict=True)
171
- final = await handle.poll_until_terminal(
172
- interval_seconds=2.0,
173
- max_seconds=1800,
174
- on_event=_on_event,
175
- on_metric=_on_metric,
176
- )
177
- status = (final or {}).get("status")
178
- print(f"final_status={status}")
179
- ft_model = (final or {}).get("fine_tuned_model")
180
- if ft_model:
181
- print(f"fine_tuned_model={ft_model}")
182
- save_state({"fine_tuned_model": ft_model})
183
-
184
- # Optional: quick inference check
185
- if not args.no_infer:
186
- model_for_infer = ft_model or str(args.model)
187
- try:
188
- ic = InferenceClient(base_url=base_url, api_key=api_key, timeout=600.0)
189
- print(f"\nInference sanity check (model={model_for_infer})…")
190
- resp = await ic.create_chat_completion(
191
- model=model_for_infer,
192
- messages=[{"role": "user", "content": "Give me a cheerful two-line greeting."}],
193
- max_tokens=128,
194
- temperature=0.7,
195
- stream=False,
196
- )
197
- print(resp)
198
- except Exception as e:
199
- # Always print full error details and traceback
200
- import traceback
201
-
202
- try:
203
- from synth_ai.http import HTTPError # type: ignore
204
- except Exception: # pragma: no cover - fallback if import shape changes
205
- HTTPError = tuple() # type: ignore
206
- print("\n===== Inference Error =====")
207
- print(f"Type: {type(e).__name__}")
208
- print(f"Repr: {repr(e)}")
209
- tb = traceback.format_exc()
210
- if tb:
211
- print("Traceback:")
212
- print(tb)
213
- # If HTTP error from backend, surface structured fields
214
- if "HTTPError" in str(type(e)) or (isinstance((), tuple) and False):
215
- pass
216
- try:
217
- if HTTPError and isinstance(e, HTTPError): # type: ignore[arg-type]
218
- print("HTTPError details:")
219
- print(f" status={e.status}")
220
- print(f" url={e.url}")
221
- print(f" message={e.message}")
222
- if getattr(e, "detail", None) is not None:
223
- print(f" detail={e.detail}")
224
- if getattr(e, "body_snippet", None):
225
- print(f" body_snippet={e.body_snippet}")
226
- except Exception:
227
- pass
228
- print("===== End Inference Error =====\n")
229
-
230
-
231
- def main() -> None:
232
- args = parse_args()
233
- asyncio.run(run(args))
234
-
235
-
236
- if __name__ == "__main__":
237
- main()
@@ -1,34 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from pathlib import Path
4
-
5
- import asyncio
6
-
7
- import sys
8
- import os
9
-
10
- sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))))
11
-
12
- from synth_ai.learning import FtClient, validate_training_jsonl
13
- from examples.finetuning.synth_qwen_v1.util import load_env, save_state, parse_args
14
-
15
-
16
- async def _run(mode: str | None) -> None:
17
- base, key = load_env(mode)
18
- client = FtClient(base_url=base, api_key=key)
19
-
20
- p = Path(__file__).parent / "data" / "training.jsonl"
21
- # Use shared validator from synth_ai.learning.validators
22
- validate_training_jsonl(p)
23
- file_id = await client.upload_training_file(p, purpose="fine-tune")
24
- print(f"file_id={file_id}")
25
- save_state({"file_id": file_id})
26
-
27
-
28
- def main() -> None:
29
- args = parse_args()
30
- asyncio.run(_run(args.mode))
31
-
32
-
33
- if __name__ == "__main__":
34
- main()
@@ -1,152 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import os
4
- import json
5
- import argparse
6
- from pathlib import Path
7
- from typing import Any, Dict
8
-
9
- from synth_ai.config.base_url import get_backend_from_env
10
-
11
- try:
12
- from dotenv import load_dotenv # type: ignore[reportMissingImports]
13
- except Exception: # pragma: no cover
14
-
15
- def load_dotenv(*args, **kwargs): # type: ignore[no-redef]
16
- return False
17
-
18
-
19
- STATE_PATH = Path(__file__).parent / "state.json"
20
-
21
-
22
- def _default_backend_url() -> str:
23
- base, _ = get_backend_from_env()
24
- base = base.rstrip("/")
25
- return base if base.endswith("/api") else f"{base}/api"
26
-
27
-
28
- def load_env(mode: str | None = None) -> tuple[str, str]:
29
- """Resolve backend base_url and api_key.
30
-
31
- Precedence:
32
- - SYNTH_BACKEND_URL_OVERRIDE=local|dev|prod (preferred)
33
- - explicit mode arg (local|dev|prod)
34
- - default prod
35
- """
36
- load_dotenv()
37
- # Prefer global override if present
38
- override = (os.getenv("SYNTH_BACKEND_URL_OVERRIDE", "") or "").strip().lower()
39
- if override in {"local", "dev", "prod"}:
40
- base, key = get_backend_from_env()
41
- base = base.rstrip("/")
42
- print(f"SYNTH backend: {base} (override={override})")
43
- # Print masked API key and source for clarity
44
- src = ""
45
- if override == "prod":
46
- if key and key == os.getenv("PROD_SYNTH_API_KEY", "").strip():
47
- src = "PROD_SYNTH_API_KEY"
48
- elif key and key == os.getenv("TESTING_PROD_SYNTH_API_KEY", "").strip():
49
- src = "TESTING_PROD_SYNTH_API_KEY"
50
- elif key and key == os.getenv("SYNTH_API_KEY", "").strip():
51
- src = "SYNTH_API_KEY"
52
- elif override == "dev":
53
- if key and key == os.getenv("DEV_SYNTH_API_KEY", "").strip():
54
- src = "DEV_SYNTH_API_KEY"
55
- else: # local
56
- if key and key == os.getenv("DEV_SYNTH_API_KEY", "").strip():
57
- src = "DEV_SYNTH_API_KEY"
58
- elif key and key == os.getenv("TESTING_LOCAL_SYNTH_API_KEY", "").strip():
59
- src = "TESTING_LOCAL_SYNTH_API_KEY"
60
- masked = ("*" * max(0, len(key) - 6)) + key[-6:] if key else "<empty>"
61
- print(f"SYNTH api key: {masked} (len={len(key)}, src={src or '<unknown>'})")
62
- return base, key
63
-
64
- # Fallback to explicit mode
65
- if mode is None:
66
- mode = os.getenv("SYNTH_MODE", "prod").strip().lower()
67
- if mode == "local":
68
- base_url = os.getenv("LOCAL_BACKEND_URL", "").strip()
69
- # Prefer DEV_SYNTH_API_KEY for local development; fall back to legacy var
70
- api_key = (
71
- os.getenv("DEV_SYNTH_API_KEY", "").strip()
72
- or os.getenv("TESTING_LOCAL_SYNTH_API_KEY", "").strip()
73
- )
74
- if not base_url or not api_key:
75
- raise RuntimeError(
76
- "Missing LOCAL_BACKEND_URL or DEV_SYNTH_API_KEY/TESTING_LOCAL_SYNTH_API_KEY in environment/.env"
77
- )
78
- elif mode == "dev":
79
- base_url = os.getenv("DEV_BACKEND_URL", "").strip()
80
- api_key = os.getenv("DEV_SYNTH_API_KEY", "").strip()
81
- if not base_url or not api_key:
82
- raise RuntimeError("Missing DEV_BACKEND_URL or DEV_SYNTH_API_KEY in environment/.env")
83
- else: # prod
84
- base_url = os.getenv("PROD_BACKEND_URL", "").strip() or _default_backend_url()
85
- api_key = (
86
- os.getenv("PROD_SYNTH_API_KEY", "").strip()
87
- or os.getenv("TESTING_PROD_SYNTH_API_KEY", "").strip()
88
- or os.getenv("SYNTH_API_KEY", "").strip()
89
- )
90
- if not api_key:
91
- raise RuntimeError(
92
- "Missing PROD_SYNTH_API_KEY/TESTING_PROD_SYNTH_API_KEY/SYNTH_API_KEY in environment/.env"
93
- )
94
- base_url = base_url.rstrip("/")
95
- print(f"SYNTH backend: {base_url} (mode={mode})")
96
- # Also print masked API key and source
97
- src = ""
98
- if mode == "prod":
99
- if api_key and api_key == os.getenv("PROD_SYNTH_API_KEY", "").strip():
100
- src = "PROD_SYNTH_API_KEY"
101
- elif api_key and api_key == os.getenv("TESTING_PROD_SYNTH_API_KEY", "").strip():
102
- src = "TESTING_PROD_SYNTH_API_KEY"
103
- elif api_key and api_key == os.getenv("SYNTH_API_KEY", "").strip():
104
- src = "SYNTH_API_KEY"
105
- elif mode == "dev":
106
- if api_key and api_key == os.getenv("DEV_SYNTH_API_KEY", "").strip():
107
- src = "DEV_SYNTH_API_KEY"
108
- else:
109
- if api_key and api_key == os.getenv("DEV_SYNTH_API_KEY", "").strip():
110
- src = "DEV_SYNTH_API_KEY"
111
- elif api_key and api_key == os.getenv("TESTING_LOCAL_SYNTH_API_KEY", "").strip():
112
- src = "TESTING_LOCAL_SYNTH_API_KEY"
113
- masked = ("*" * max(0, len(api_key) - 6)) + api_key[-6:] if api_key else "<empty>"
114
- print(f"SYNTH api key: {masked} (len={len(api_key)}, src={src or '<unknown>'})")
115
- return base_url, api_key
116
-
117
-
118
- def parse_args() -> argparse.Namespace:
119
- p = argparse.ArgumentParser()
120
- p.add_argument("--mode", choices=["prod", "dev", "local"], default=None, help="Backend mode")
121
- return p.parse_args()
122
-
123
-
124
- def save_state(obj: Dict[str, Any]) -> None:
125
- prev: Dict[str, Any] = {}
126
- if STATE_PATH.exists():
127
- try:
128
- prev = json.loads(STATE_PATH.read_text())
129
- except Exception:
130
- prev = {}
131
- prev.update(obj)
132
- STATE_PATH.write_text(json.dumps(prev, indent=2))
133
-
134
-
135
- def load_state() -> Dict[str, Any]:
136
- if not STATE_PATH.exists():
137
- return {}
138
- try:
139
- return json.loads(STATE_PATH.read_text())
140
- except Exception:
141
- return {}
142
-
143
-
144
- def validate_jsonl(path: str | Path) -> None:
145
- """Backwards-compatible wrapper that delegates to shared SDK validator.
146
-
147
- Prefer synth_ai.learning.validators.validate_training_jsonl to keep a single source
148
- of JSONL validation rules used across examples and tests.
149
- """
150
- from synth_ai.learning import validate_training_jsonl
151
-
152
- validate_training_jsonl(path)