synth-ai 0.2.9.dev5__py3-none-any.whl → 0.2.10__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 (349) hide show
  1. examples/__init__.py +16 -0
  2. examples/crafter_debug_render.py +23 -17
  3. examples/dev/qwen3_32b_qlora_4xh100.toml +40 -0
  4. examples/multi_step/crafter_rl_lora.md +29 -0
  5. examples/qwen_coder/README.md +102 -0
  6. examples/qwen_coder/_shared.py +113 -0
  7. examples/qwen_coder/configs/coder_lora_30b.toml +61 -0
  8. examples/qwen_coder/configs/coder_lora_4b.toml +57 -0
  9. examples/qwen_coder/configs/coder_lora_small.toml +58 -0
  10. examples/qwen_coder/generate_dataset.py +98 -0
  11. examples/qwen_coder/infer_ft_smoke.py +65 -0
  12. examples/qwen_coder/infer_prod_proxy.py +73 -0
  13. examples/qwen_coder/infer_via_synth.py +87 -0
  14. examples/qwen_coder/scripts/infer_coder.sh +19 -0
  15. examples/qwen_coder/scripts/train_coder_30b.sh +22 -0
  16. examples/qwen_coder/sft_full_17b.py +103 -0
  17. examples/qwen_coder/sft_lora_30b.py +110 -0
  18. examples/qwen_coder/subset_jsonl.py +39 -0
  19. examples/qwen_coder/todos.md +38 -0
  20. examples/qwen_coder/validate_jsonl.py +60 -0
  21. examples/rl/configs/eval_base_qwen.toml +1 -1
  22. examples/rl/configs/rl_from_base_qwen17.toml +1 -1
  23. examples/rl/download_dataset.py +26 -10
  24. examples/rl/run_eval.py +53 -52
  25. examples/rl/run_rl_and_save.py +29 -12
  26. examples/rl/task_app/math_single_step.py +180 -41
  27. examples/rl/task_app/math_task_app.py +14 -6
  28. examples/sft/README.md +139 -0
  29. examples/sft/configs/crafter_fft_qwen0p6b.toml +44 -0
  30. examples/sft/configs/crafter_lora_qwen0p6b.toml +45 -0
  31. examples/sft/evaluate.py +117 -0
  32. examples/sft/export_dataset.py +117 -0
  33. examples/sft/generate_traces.py +162 -0
  34. examples/swe/__init__.py +12 -0
  35. examples/swe/task_app/README.md +105 -0
  36. examples/swe/task_app/__init__.py +2 -0
  37. examples/swe/task_app/grpo_swe_mini.py +571 -0
  38. examples/swe/task_app/grpo_swe_mini_task_app.py +136 -0
  39. examples/swe/task_app/hosted/README.md +173 -0
  40. examples/swe/task_app/hosted/__init__.py +5 -0
  41. examples/swe/task_app/hosted/branching.py +143 -0
  42. examples/swe/task_app/hosted/environment_routes.py +1289 -0
  43. examples/swe/task_app/hosted/envs/__init__.py +1 -0
  44. examples/swe/task_app/hosted/envs/crafter/__init__.py +6 -0
  45. examples/swe/task_app/hosted/envs/crafter/app.py +1 -0
  46. examples/swe/task_app/hosted/envs/crafter/environment.py +522 -0
  47. examples/swe/task_app/hosted/envs/crafter/policy.py +478 -0
  48. examples/swe/task_app/hosted/envs/crafter/react_agent.py +108 -0
  49. examples/swe/task_app/hosted/envs/crafter/shared.py +305 -0
  50. examples/swe/task_app/hosted/envs/crafter/tools.py +47 -0
  51. examples/swe/task_app/hosted/envs/mini_swe/__init__.py +8 -0
  52. examples/swe/task_app/hosted/envs/mini_swe/environment.py +1164 -0
  53. examples/swe/task_app/hosted/envs/mini_swe/policy.py +355 -0
  54. examples/swe/task_app/hosted/envs/mini_swe/shared.py +83 -0
  55. examples/swe/task_app/hosted/envs/mini_swe/tools.py +96 -0
  56. examples/swe/task_app/hosted/hosted_app.py +204 -0
  57. examples/swe/task_app/hosted/inference/__init__.py +5 -0
  58. examples/swe/task_app/hosted/inference/openai_client.py +618 -0
  59. examples/swe/task_app/hosted/main.py +100 -0
  60. examples/swe/task_app/hosted/policy_routes.py +1079 -0
  61. examples/swe/task_app/hosted/registry.py +195 -0
  62. examples/swe/task_app/hosted/rollout.py +1869 -0
  63. examples/swe/task_app/hosted/storage/__init__.py +5 -0
  64. examples/swe/task_app/hosted/storage/volume.py +211 -0
  65. examples/swe/task_app/hosted/test_agents.py +161 -0
  66. examples/swe/task_app/hosted/test_service.py +137 -0
  67. examples/swe/task_app/hosted/utils.py +62 -0
  68. examples/vlm/PROPOSAL.md +53 -0
  69. examples/vlm/README.md +68 -0
  70. examples/vlm/configs/crafter_vlm_gpt4o.toml +44 -0
  71. examples/vlm/crafter_image_only_agent.py +207 -0
  72. examples/vlm/crafter_openai_vlm_agent.py +277 -0
  73. examples/vlm/filter_image_rows.py +63 -0
  74. examples/vlm/run_crafter_vlm_benchmark.py +316 -0
  75. examples/warming_up_to_rl/analyze_trace_db.py +12 -10
  76. examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +11 -1
  77. examples/warming_up_to_rl/export_trace_sft.py +218 -36
  78. examples/warming_up_to_rl/groq_test.py +15 -8
  79. examples/warming_up_to_rl/manage_secrets.py +29 -25
  80. examples/warming_up_to_rl/readme.md +9 -2
  81. examples/warming_up_to_rl/run_eval.py +137 -61
  82. examples/warming_up_to_rl/run_fft_and_save.py +131 -60
  83. examples/warming_up_to_rl/run_local_rollout.py +88 -39
  84. examples/warming_up_to_rl/run_local_rollout_modal.py +114 -28
  85. examples/warming_up_to_rl/run_local_rollout_parallel.py +81 -20
  86. examples/warming_up_to_rl/run_local_rollout_traced.py +126 -23
  87. examples/warming_up_to_rl/run_rl_and_save.py +35 -12
  88. examples/warming_up_to_rl/run_rollout_remote.py +44 -19
  89. examples/warming_up_to_rl/task_app/README.md +6 -2
  90. examples/warming_up_to_rl/task_app/grpo_crafter.py +319 -57
  91. examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +11 -30
  92. examples/warming_up_to_rl/task_app/synth_envs_hosted/__init__.py +1 -1
  93. examples/warming_up_to_rl/task_app/synth_envs_hosted/branching.py +9 -11
  94. examples/warming_up_to_rl/task_app/synth_envs_hosted/environment_routes.py +137 -182
  95. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/__init__.py +1 -1
  96. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/__init__.py +1 -1
  97. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/app.py +1 -1
  98. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/environment.py +150 -57
  99. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +105 -69
  100. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +19 -7
  101. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/shared.py +45 -42
  102. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/tools.py +1 -1
  103. examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +47 -45
  104. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/__init__.py +1 -1
  105. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +198 -92
  106. examples/warming_up_to_rl/task_app/synth_envs_hosted/main.py +0 -2
  107. examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +361 -263
  108. examples/warming_up_to_rl/task_app/synth_envs_hosted/registry.py +21 -23
  109. examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +394 -274
  110. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/__init__.py +1 -1
  111. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/volume.py +56 -62
  112. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_agents.py +1 -0
  113. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_service.py +6 -15
  114. examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +4 -3
  115. synth_ai/__init__.py +1 -0
  116. synth_ai/api/models/supported.py +376 -0
  117. synth_ai/api/train/builders.py +157 -26
  118. synth_ai/api/train/cli.py +213 -57
  119. synth_ai/api/train/config_finder.py +65 -5
  120. synth_ai/api/train/env_resolver.py +33 -15
  121. synth_ai/api/train/pollers.py +13 -4
  122. synth_ai/api/train/supported_algos.py +139 -0
  123. synth_ai/api/train/task_app.py +5 -3
  124. synth_ai/api/train/utils.py +33 -48
  125. synth_ai/cli/__init__.py +19 -4
  126. synth_ai/cli/_modal_wrapper.py +28 -0
  127. synth_ai/cli/_typer_patch.py +49 -0
  128. synth_ai/cli/balance.py +2 -3
  129. synth_ai/cli/calc.py +1 -1
  130. synth_ai/cli/demo.py +21 -6
  131. synth_ai/cli/recent.py +2 -2
  132. synth_ai/cli/rl_demo.py +77 -17
  133. synth_ai/cli/root.py +116 -39
  134. synth_ai/cli/status.py +2 -2
  135. synth_ai/cli/task_apps.py +1699 -259
  136. synth_ai/cli/traces.py +7 -4
  137. synth_ai/cli/turso.py +73 -0
  138. synth_ai/cli/watch.py +12 -18
  139. synth_ai/core/experiment.py +0 -2
  140. synth_ai/demo_registry.py +68 -31
  141. synth_ai/demos/core/cli.py +516 -194
  142. synth_ai/demos/demo_task_apps/__init__.py +3 -3
  143. synth_ai/demos/demo_task_apps/core.py +64 -28
  144. synth_ai/demos/demo_task_apps/crafter/configs/crafter_fft_4b.toml +2 -3
  145. synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +37 -30
  146. synth_ai/demos/demo_task_apps/math/_common.py +1 -2
  147. synth_ai/demos/demo_task_apps/math/app.py +2 -1
  148. synth_ai/demos/demo_task_apps/math/deploy_modal.py +3 -6
  149. synth_ai/demos/demo_task_apps/math/modal_task_app.py +183 -82
  150. synth_ai/demos/demo_task_apps/math/task_app_entry.py +0 -2
  151. synth_ai/environments/examples/bandit/engine.py +12 -4
  152. synth_ai/environments/examples/bandit/taskset.py +4 -4
  153. synth_ai/environments/examples/crafter_classic/environment.py +76 -1
  154. synth_ai/environments/reproducibility/tree.py +5 -6
  155. synth_ai/environments/service/app.py +11 -12
  156. synth_ai/environments/service/core_routes.py +10 -9
  157. synth_ai/environments/stateful/engine.py +1 -1
  158. synth_ai/environments/tasks/core.py +1 -0
  159. synth_ai/environments/tasks/filters.py +5 -6
  160. synth_ai/environments/tasks/utils.py +4 -5
  161. synth_ai/evals/base.py +0 -2
  162. synth_ai/handshake.py +11 -9
  163. synth_ai/http.py +1 -1
  164. synth_ai/http_client.py +43 -11
  165. synth_ai/inference/__init__.py +0 -2
  166. synth_ai/inference/client.py +20 -6
  167. synth_ai/jobs/client.py +103 -78
  168. synth_ai/learning/__init__.py +41 -6
  169. synth_ai/learning/algorithms.py +14 -0
  170. synth_ai/learning/client.py +121 -29
  171. synth_ai/learning/config.py +2 -40
  172. synth_ai/learning/constants.py +0 -2
  173. synth_ai/learning/ft_client.py +4 -56
  174. synth_ai/learning/health.py +13 -7
  175. synth_ai/learning/jobs.py +43 -47
  176. synth_ai/{rl → learning/rl}/__init__.py +14 -5
  177. synth_ai/learning/rl/client.py +267 -0
  178. synth_ai/learning/rl/config.py +31 -0
  179. synth_ai/{rl → learning/rl}/contracts.py +5 -10
  180. synth_ai/{rl → learning/rl}/env_keys.py +45 -16
  181. synth_ai/learning/rl/secrets.py +13 -0
  182. synth_ai/learning/rl_client.py +2 -253
  183. synth_ai/learning/sft/__init__.py +29 -0
  184. synth_ai/learning/sft/client.py +68 -0
  185. synth_ai/learning/sft/config.py +270 -0
  186. synth_ai/learning/sft/data.py +295 -0
  187. synth_ai/learning/sse.py +25 -26
  188. synth_ai/learning/validators.py +25 -24
  189. synth_ai/lm/__init__.py +21 -47
  190. synth_ai/task/__init__.py +26 -27
  191. synth_ai/task/apps/__init__.py +18 -19
  192. synth_ai/task/auth.py +35 -23
  193. synth_ai/task/client.py +15 -13
  194. synth_ai/task/contracts.py +37 -35
  195. synth_ai/task/datasets.py +9 -6
  196. synth_ai/task/errors.py +11 -10
  197. synth_ai/task/health.py +17 -11
  198. synth_ai/task/json.py +58 -24
  199. synth_ai/task/proxy.py +15 -14
  200. synth_ai/task/rubrics.py +22 -15
  201. synth_ai/task/server.py +43 -17
  202. synth_ai/task/tracing_utils.py +12 -7
  203. synth_ai/task/validators.py +0 -1
  204. synth_ai/task/vendors.py +5 -7
  205. synth_ai/tracing_v3/__init__.py +2 -0
  206. synth_ai/tracing_v3/abstractions.py +21 -4
  207. synth_ai/tracing_v3/db_config.py +26 -1
  208. synth_ai/tracing_v3/decorators.py +18 -15
  209. synth_ai/tracing_v3/examples/basic_usage.py +3 -2
  210. synth_ai/tracing_v3/hooks.py +6 -4
  211. synth_ai/tracing_v3/llm_call_record_helpers.py +6 -6
  212. synth_ai/tracing_v3/replica_sync.py +1 -0
  213. synth_ai/tracing_v3/session_tracer.py +63 -16
  214. synth_ai/tracing_v3/storage/base.py +89 -1
  215. synth_ai/tracing_v3/storage/config.py +21 -8
  216. synth_ai/tracing_v3/storage/factory.py +10 -8
  217. synth_ai/tracing_v3/storage/utils.py +4 -2
  218. synth_ai/tracing_v3/turso/daemon.py +7 -2
  219. synth_ai/tracing_v3/turso/models.py +5 -2
  220. synth_ai/tracing_v3/turso/native_manager.py +1173 -0
  221. synth_ai/tracing_v3/utils.py +4 -3
  222. synth_ai/v0/api/__init__.py +8 -0
  223. synth_ai/v0/api/models/__init__.py +8 -0
  224. synth_ai/v0/api/models/supported.py +8 -0
  225. synth_ai/v0/config/__init__.py +15 -0
  226. synth_ai/v0/config/base_url.py +12 -0
  227. synth_ai/v0/lm/__init__.py +51 -0
  228. synth_ai/{lm → v0/lm}/caching/ephemeral.py +3 -5
  229. synth_ai/{lm → v0/lm}/caching/handler.py +4 -4
  230. synth_ai/{lm → v0/lm}/caching/initialize.py +1 -1
  231. synth_ai/{lm → v0/lm}/caching/persistent.py +1 -1
  232. synth_ai/{lm → v0/lm}/config.py +6 -1
  233. synth_ai/{lm → v0/lm}/core/all.py +9 -9
  234. synth_ai/{lm → v0/lm}/core/exceptions.py +0 -2
  235. synth_ai/{lm → v0/lm}/core/main.py +19 -7
  236. synth_ai/{lm → v0/lm}/core/main_v3.py +10 -10
  237. synth_ai/{lm → v0/lm}/core/synth_models.py +2 -15
  238. synth_ai/{lm → v0/lm}/core/vendor_clients.py +6 -4
  239. synth_ai/{lm → v0/lm}/overrides.py +4 -4
  240. synth_ai/{lm → v0/lm}/provider_support/anthropic.py +4 -4
  241. synth_ai/{lm → v0/lm}/provider_support/openai.py +5 -5
  242. synth_ai/{lm → v0/lm}/structured_outputs/handler.py +5 -5
  243. synth_ai/{lm → v0/lm}/structured_outputs/rehabilitate.py +1 -1
  244. synth_ai/{lm → v0/lm}/vendors/core/anthropic_api.py +16 -16
  245. synth_ai/{lm → v0/lm}/vendors/core/gemini_api.py +5 -5
  246. synth_ai/{lm → v0/lm}/vendors/core/mistral_api.py +5 -5
  247. synth_ai/{lm → v0/lm}/vendors/core/openai_api.py +12 -10
  248. synth_ai/{lm → v0/lm}/vendors/openai_standard.py +11 -9
  249. synth_ai/{lm → v0/lm}/vendors/openai_standard_responses.py +8 -5
  250. synth_ai/{lm → v0/lm}/vendors/supported/custom_endpoint.py +4 -6
  251. synth_ai/{lm → v0/lm}/vendors/supported/deepseek.py +2 -2
  252. synth_ai/{lm → v0/lm}/vendors/supported/grok.py +2 -2
  253. synth_ai/{lm → v0/lm}/vendors/supported/groq.py +1 -1
  254. synth_ai/{lm → v0/lm}/vendors/supported/ollama.py +1 -1
  255. synth_ai/{lm → v0/lm}/vendors/supported/openrouter.py +3 -3
  256. synth_ai/{lm → v0/lm}/vendors/supported/together.py +1 -1
  257. synth_ai/{lm → v0/lm}/vendors/synth_client.py +38 -11
  258. synth_ai/v0/tracing/upload.py +32 -135
  259. synth_ai/v0/tracing_v3/__init__.py +10 -0
  260. synth_ai/v0/tracing_v3/abstractions.py +3 -0
  261. synth_ai/v0/tracing_v3/decorators.py +3 -0
  262. synth_ai/v0/tracing_v3/llm_call_record_helpers.py +3 -0
  263. synth_ai/v0/tracing_v3/session_tracer.py +3 -0
  264. {synth_ai-0.2.9.dev5.dist-info → synth_ai-0.2.10.dist-info}/METADATA +10 -7
  265. {synth_ai-0.2.9.dev5.dist-info → synth_ai-0.2.10.dist-info}/RECORD +294 -258
  266. examples/common_old/backend.py +0 -21
  267. examples/evals_old/README.md +0 -98
  268. examples/evals_old/__init__.py +0 -6
  269. examples/evals_old/compare_models.py +0 -1037
  270. examples/evals_old/example_log.md +0 -145
  271. examples/evals_old/run_demo.sh +0 -126
  272. examples/evals_old/trace_analysis.py +0 -270
  273. examples/finetuning_old/_backup_synth_qwen/config.toml +0 -29
  274. examples/finetuning_old/_backup_synth_qwen/example_log.md +0 -324
  275. examples/finetuning_old/_backup_synth_qwen/filter_traces.py +0 -60
  276. examples/finetuning_old/_backup_synth_qwen/filter_traces_achievements.py +0 -239
  277. examples/finetuning_old/_backup_synth_qwen/purge_v3_traces.py +0 -109
  278. examples/finetuning_old/_backup_synth_qwen/react_agent_lm.py +0 -1924
  279. examples/finetuning_old/_backup_synth_qwen/readme.md +0 -49
  280. examples/finetuning_old/_backup_synth_qwen/run_crafter_qwen4b.py +0 -114
  281. examples/finetuning_old/_backup_synth_qwen/run_demo.sh +0 -195
  282. examples/finetuning_old/_backup_synth_qwen/sft_kickoff.py +0 -118
  283. examples/finetuning_old/synth_qwen_v1/README.md +0 -68
  284. examples/finetuning_old/synth_qwen_v1/filter_traces.py +0 -60
  285. examples/finetuning_old/synth_qwen_v1/filter_traces_achievements.py +0 -239
  286. examples/finetuning_old/synth_qwen_v1/finetune.py +0 -46
  287. examples/finetuning_old/synth_qwen_v1/hello_ft_model.py +0 -71
  288. examples/finetuning_old/synth_qwen_v1/infer.py +0 -37
  289. examples/finetuning_old/synth_qwen_v1/poll.py +0 -44
  290. examples/finetuning_old/synth_qwen_v1/prepare_data.py +0 -35
  291. examples/finetuning_old/synth_qwen_v1/purge_v3_traces.py +0 -109
  292. examples/finetuning_old/synth_qwen_v1/react_agent_lm.py +0 -1932
  293. examples/finetuning_old/synth_qwen_v1/run_crafter_sft_job.py +0 -207
  294. examples/finetuning_old/synth_qwen_v1/run_ft_job.py +0 -232
  295. examples/finetuning_old/synth_qwen_v1/upload_data.py +0 -34
  296. examples/finetuning_old/synth_qwen_v1/util.py +0 -147
  297. examples/rl_old/task_app.py +0 -962
  298. synth_ai/experimental/synth_oss.py +0 -446
  299. synth_ai/install_sqld.sh +0 -40
  300. synth_ai/learning/filtering.py +0 -0
  301. synth_ai/learning/offline/dpo.py +0 -0
  302. synth_ai/learning/offline/providers.py +0 -7
  303. synth_ai/learning/offline/sft.py +0 -0
  304. synth_ai/learning/offline/shared.py +0 -0
  305. synth_ai/learning/online/grpo.py +0 -0
  306. synth_ai/learning/online/irft.py +0 -0
  307. synth_ai/learning/prompts/banking77_injection_eval.py +0 -168
  308. synth_ai/learning/prompts/gepa.py +0 -0
  309. synth_ai/learning/prompts/hello_world_in_context_injection_ex.py +0 -213
  310. synth_ai/learning/prompts/mipro.py +0 -289
  311. synth_ai/learning/prompts/random_search.py +0 -246
  312. synth_ai/learning/prompts/run_mipro_banking77.py +0 -172
  313. synth_ai/learning/prompts/run_random_search_banking77.py +0 -324
  314. synth_ai/rl/secrets.py +0 -19
  315. synth_ai/scripts/verify_rewards.py +0 -100
  316. synth_ai/tracing/__init__.py +0 -30
  317. synth_ai/tracing_v1/__init__.py +0 -33
  318. synth_ai/tracing_v3/turso/__init__.py +0 -25
  319. synth_ai/tracing_v3/turso/manager.py +0 -774
  320. synth_ai/zyk/__init__.py +0 -30
  321. /synth_ai/{lm → v0/lm}/caching/__init__.py +0 -0
  322. /synth_ai/{lm → v0/lm}/caching/constants.py +0 -0
  323. /synth_ai/{lm → v0/lm}/caching/dbs.py +0 -0
  324. /synth_ai/{lm → v0/lm}/constants.py +0 -0
  325. /synth_ai/{lm → v0/lm}/core/__init__.py +0 -0
  326. /synth_ai/{lm → v0/lm}/cost/__init__.py +0 -0
  327. /synth_ai/{lm → v0/lm}/cost/monitor.py +0 -0
  328. /synth_ai/{lm → v0/lm}/cost/statefulness.py +0 -0
  329. /synth_ai/{lm → v0/lm}/injection.py +0 -0
  330. /synth_ai/{lm → v0/lm}/provider_support/__init__.py +0 -0
  331. /synth_ai/{lm → v0/lm}/provider_support/suppress_logging.py +0 -0
  332. /synth_ai/{lm → v0/lm}/structured_outputs/__init__.py +0 -0
  333. /synth_ai/{lm → v0/lm}/structured_outputs/inject.py +0 -0
  334. /synth_ai/{lm → v0/lm}/tools/__init__.py +0 -0
  335. /synth_ai/{lm → v0/lm}/tools/base.py +0 -0
  336. /synth_ai/{lm → v0/lm}/unified_interface.py +0 -0
  337. /synth_ai/{lm → v0/lm}/vendors/__init__.py +0 -0
  338. /synth_ai/{lm → v0/lm}/vendors/base.py +0 -0
  339. /synth_ai/{lm → v0/lm}/vendors/core/__init__.py +0 -0
  340. /synth_ai/{lm → v0/lm}/vendors/core/synth_dev_api.py +0 -0
  341. /synth_ai/{lm → v0/lm}/vendors/local/__init__.py +0 -0
  342. /synth_ai/{lm → v0/lm}/vendors/local/ollama.py +0 -0
  343. /synth_ai/{lm → v0/lm}/vendors/retries.py +0 -0
  344. /synth_ai/{lm → v0/lm}/vendors/supported/__init__.py +0 -0
  345. /synth_ai/{lm → v0/lm}/warmup.py +0 -0
  346. {synth_ai-0.2.9.dev5.dist-info → synth_ai-0.2.10.dist-info}/WHEEL +0 -0
  347. {synth_ai-0.2.9.dev5.dist-info → synth_ai-0.2.10.dist-info}/entry_points.txt +0 -0
  348. {synth_ai-0.2.9.dev5.dist-info → synth_ai-0.2.10.dist-info}/licenses/LICENSE +0 -0
  349. {synth_ai-0.2.9.dev5.dist-info → synth_ai-0.2.10.dist-info}/top_level.txt +0 -0
synth_ai/jobs/client.py CHANGED
@@ -1,22 +1,36 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any, Dict, Optional
3
+ from typing import Any
4
4
 
5
+ from synth_ai.api.models.supported import normalize_model_identifier
5
6
  from synth_ai.http import AsyncHttpClient
7
+ from synth_ai.learning.sft.config import prepare_sft_job_payload
6
8
 
7
9
 
8
10
  class FilesApi:
9
11
  def __init__(self, http: AsyncHttpClient) -> None:
10
12
  self._http = http
11
13
 
12
- async def upload(self, *, filename: str, content: bytes, purpose: str, content_type: Optional[str] = None, idempotency_key: Optional[str] = None) -> Dict[str, Any]:
14
+ async def upload(
15
+ self,
16
+ *,
17
+ filename: str,
18
+ content: bytes,
19
+ purpose: str,
20
+ content_type: str | None = None,
21
+ idempotency_key: str | None = None,
22
+ ) -> dict[str, Any]:
13
23
  data = {"purpose": purpose}
14
24
  files = {"file": (filename, content, content_type)}
15
25
  headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
16
- return await self._http.post_multipart("/api/files", data=data, files=files, headers=headers)
26
+ return await self._http.post_multipart(
27
+ "/api/files", data=data, files=files, headers=headers
28
+ )
17
29
 
18
- async def list(self, *, purpose: Optional[str] = None, after: Optional[str] = None, limit: int = 20) -> Dict[str, Any]:
19
- params: Dict[str, Any] = {}
30
+ async def list(
31
+ self, *, purpose: str | None = None, after: str | None = None, limit: int = 20
32
+ ) -> dict[str, Any]:
33
+ params: dict[str, Any] = {}
20
34
  if purpose is not None:
21
35
  params["purpose"] = purpose
22
36
  if after is not None:
@@ -24,14 +38,16 @@ class FilesApi:
24
38
  params["limit"] = limit
25
39
  return await self._http.get("/api/files", params=params)
26
40
 
27
- async def retrieve(self, file_id: str) -> Dict[str, Any]:
41
+ async def retrieve(self, file_id: str) -> dict[str, Any]:
28
42
  return await self._http.get(f"/api/files/{file_id}")
29
43
 
30
44
  async def delete(self, file_id: str) -> Any:
31
45
  return await self._http.delete(f"/api/files/{file_id}")
32
46
 
33
- async def list_jobs(self, file_id: str, *, after: Optional[str] = None, limit: int = 20) -> Dict[str, Any]:
34
- params: Dict[str, Any] = {"limit": limit}
47
+ async def list_jobs(
48
+ self, file_id: str, *, after: str | None = None, limit: int = 20
49
+ ) -> dict[str, Any]:
50
+ params: dict[str, Any] = {"limit": limit}
35
51
  if after is not None:
36
52
  params["after"] = after
37
53
  return await self._http.get(f"/api/files/{file_id}/jobs", params=params)
@@ -46,42 +62,40 @@ class SftJobsApi:
46
62
  *,
47
63
  training_file: str,
48
64
  model: str,
49
- validation_file: Optional[str] = None,
50
- hyperparameters: Optional[Dict[str, Any]] = None,
51
- suffix: Optional[str] = None,
52
- integrations: Optional[Dict[str, Any]] = None,
53
- metadata: Optional[Dict[str, Any]] = None,
54
- idempotency_key: Optional[str] = None,
55
- ) -> Dict[str, Any]:
56
- payload: Dict[str, Any] = {
57
- "training_file": training_file,
58
- "model": model,
59
- }
60
- if validation_file is not None:
61
- payload["validation_file"] = validation_file
62
- if hyperparameters is not None:
63
- payload["hyperparameters"] = hyperparameters
64
- if suffix is not None:
65
- payload["suffix"] = suffix
66
- if integrations is not None:
67
- payload["integrations"] = integrations
68
- if metadata is not None:
69
- payload["metadata"] = metadata
65
+ validation_file: str | None = None,
66
+ hyperparameters: dict[str, Any] | None = None,
67
+ suffix: str | None = None,
68
+ integrations: dict[str, Any] | None = None,
69
+ metadata: dict[str, Any] | None = None,
70
+ idempotency_key: str | None = None,
71
+ ) -> dict[str, Any]:
72
+ payload = prepare_sft_job_payload(
73
+ model=model,
74
+ training_file=training_file,
75
+ hyperparameters=hyperparameters,
76
+ metadata=metadata,
77
+ training_type=None,
78
+ validation_file=validation_file,
79
+ suffix=suffix,
80
+ integrations=integrations,
81
+ training_file_field="training_file",
82
+ require_training_file=True,
83
+ )
70
84
  headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
71
85
  return await self._http.post_json("/api/sft/jobs", json=payload, headers=headers)
72
86
 
73
87
  async def list(
74
88
  self,
75
89
  *,
76
- status: Optional[str] = None,
77
- model: Optional[str] = None,
78
- file_id: Optional[str] = None,
79
- created_after: Optional[int] = None,
80
- created_before: Optional[int] = None,
81
- after: Optional[str] = None,
90
+ status: str | None = None,
91
+ model: str | None = None,
92
+ file_id: str | None = None,
93
+ created_after: int | None = None,
94
+ created_before: int | None = None,
95
+ after: str | None = None,
82
96
  limit: int = 20,
83
- ) -> Dict[str, Any]:
84
- params: Dict[str, Any] = {"limit": limit}
97
+ ) -> dict[str, Any]:
98
+ params: dict[str, Any] = {"limit": limit}
85
99
  if status is not None:
86
100
  params["status"] = status
87
101
  if model is not None:
@@ -96,18 +110,22 @@ class SftJobsApi:
96
110
  params["after"] = after
97
111
  return await self._http.get("/api/sft/jobs", params=params)
98
112
 
99
- async def retrieve(self, job_id: str) -> Dict[str, Any]:
113
+ async def retrieve(self, job_id: str) -> dict[str, Any]:
100
114
  return await self._http.get(f"/api/sft/jobs/{job_id}")
101
115
 
102
- async def cancel(self, job_id: str) -> Dict[str, Any]:
116
+ async def cancel(self, job_id: str) -> dict[str, Any]:
103
117
  return await self._http.post_json(f"/api/sft/jobs/{job_id}/cancel", json={})
104
118
 
105
- async def list_events(self, job_id: str, *, since_seq: int = 0, limit: int = 200) -> Dict[str, Any]:
119
+ async def list_events(
120
+ self, job_id: str, *, since_seq: int = 0, limit: int = 200
121
+ ) -> dict[str, Any]:
106
122
  params = {"since_seq": since_seq, "limit": limit}
107
123
  return await self._http.get(f"/api/sft/jobs/{job_id}/events", params=params)
108
124
 
109
- async def checkpoints(self, job_id: str, *, after: Optional[str] = None, limit: int = 10) -> Dict[str, Any]:
110
- params: Dict[str, Any] = {"limit": limit}
125
+ async def checkpoints(
126
+ self, job_id: str, *, after: str | None = None, limit: int = 10
127
+ ) -> dict[str, Any]:
128
+ params: dict[str, Any] = {"limit": limit}
111
129
  if after is not None:
112
130
  params["after"] = after
113
131
  return await self._http.get(f"/api/sft/jobs/{job_id}/checkpoints", params=params)
@@ -123,14 +141,14 @@ class RlJobsApi:
123
141
  model: str,
124
142
  endpoint_base_url: str,
125
143
  trainer_id: str,
126
- trainer: Optional[Dict[str, Any]] = None,
127
- job_config_id: Optional[str] = None,
128
- config: Optional[Dict[str, Any]] = None,
129
- metadata: Optional[Dict[str, Any]] = None,
130
- idempotency_key: Optional[str] = None,
131
- ) -> Dict[str, Any]:
132
- payload: Dict[str, Any] = {
133
- "model": model,
144
+ trainer: dict[str, Any] | None = None,
145
+ job_config_id: str | None = None,
146
+ config: dict[str, Any] | None = None,
147
+ metadata: dict[str, Any] | None = None,
148
+ idempotency_key: str | None = None,
149
+ ) -> dict[str, Any]:
150
+ payload: dict[str, Any] = {
151
+ "model": normalize_model_identifier(model),
134
152
  "endpoint_base_url": endpoint_base_url,
135
153
  "trainer_id": trainer_id,
136
154
  }
@@ -148,14 +166,14 @@ class RlJobsApi:
148
166
  async def list(
149
167
  self,
150
168
  *,
151
- status: Optional[str] = None,
152
- model: Optional[str] = None,
153
- created_after: Optional[int] = None,
154
- created_before: Optional[int] = None,
155
- after: Optional[str] = None,
169
+ status: str | None = None,
170
+ model: str | None = None,
171
+ created_after: int | None = None,
172
+ created_before: int | None = None,
173
+ after: str | None = None,
156
174
  limit: int = 20,
157
- ) -> Dict[str, Any]:
158
- params: Dict[str, Any] = {"limit": limit}
175
+ ) -> dict[str, Any]:
176
+ params: dict[str, Any] = {"limit": limit}
159
177
  if status is not None:
160
178
  params["status"] = status
161
179
  if model is not None:
@@ -168,17 +186,21 @@ class RlJobsApi:
168
186
  params["after"] = after
169
187
  return await self._http.get("/api/rl/jobs", params=params)
170
188
 
171
- async def retrieve(self, job_id: str) -> Dict[str, Any]:
189
+ async def retrieve(self, job_id: str) -> dict[str, Any]:
172
190
  return await self._http.get(f"/api/rl/jobs/{job_id}")
173
191
 
174
- async def cancel(self, job_id: str) -> Dict[str, Any]:
192
+ async def cancel(self, job_id: str) -> dict[str, Any]:
175
193
  return await self._http.post_json(f"/api/rl/jobs/{job_id}/cancel", json={})
176
194
 
177
- async def list_events(self, job_id: str, *, since_seq: int = 0, limit: int = 200) -> Dict[str, Any]:
195
+ async def list_events(
196
+ self, job_id: str, *, since_seq: int = 0, limit: int = 200
197
+ ) -> dict[str, Any]:
178
198
  params = {"since_seq": since_seq, "limit": limit}
179
199
  return await self._http.get(f"/api/rl/jobs/{job_id}/events", params=params)
180
200
 
181
- async def metrics(self, job_id: str, *, after_step: int = -1, limit: int = 200) -> Dict[str, Any]:
201
+ async def metrics(
202
+ self, job_id: str, *, after_step: int = -1, limit: int = 200
203
+ ) -> dict[str, Any]:
182
204
  params = {"after_step": after_step, "limit": limit}
183
205
  return await self._http.get(f"/api/rl/jobs/{job_id}/metrics", params=params)
184
206
 
@@ -190,13 +212,13 @@ class ModelsApi:
190
212
  async def list(
191
213
  self,
192
214
  *,
193
- source: Optional[str] = None,
194
- base_model: Optional[str] = None,
195
- status: Optional[str] = None,
196
- after: Optional[str] = None,
215
+ source: str | None = None,
216
+ base_model: str | None = None,
217
+ status: str | None = None,
218
+ after: str | None = None,
197
219
  limit: int = 20,
198
- ) -> Dict[str, Any]:
199
- params: Dict[str, Any] = {"limit": limit}
220
+ ) -> dict[str, Any]:
221
+ params: dict[str, Any] = {"limit": limit}
200
222
  if source is not None:
201
223
  params["source"] = source
202
224
  if base_model is not None:
@@ -207,28 +229,31 @@ class ModelsApi:
207
229
  params["after"] = after
208
230
  return await self._http.get("/api/models", params=params)
209
231
 
210
- async def retrieve(self, model_id: str) -> Dict[str, Any]:
232
+ async def retrieve(self, model_id: str) -> dict[str, Any]:
211
233
  return await self._http.get(f"/api/models/{model_id}")
212
234
 
213
235
  async def delete(self, model_id: str) -> Any:
214
236
  return await self._http.delete(f"/api/models/{model_id}")
215
237
 
216
- async def list_jobs(self, model_id: str, *, after: Optional[str] = None, limit: int = 20) -> Dict[str, Any]:
217
- params: Dict[str, Any] = {"limit": limit}
238
+ async def list_jobs(
239
+ self, model_id: str, *, after: str | None = None, limit: int = 20
240
+ ) -> dict[str, Any]:
241
+ params: dict[str, Any] = {"limit": limit}
218
242
  if after is not None:
219
243
  params["after"] = after
220
244
  return await self._http.get(f"/api/models/{model_id}/jobs", params=params)
221
245
 
222
246
 
223
247
  class JobsClient:
224
- """High-level client aggregating job APIs.
248
+ """High-level client aggregating job APIs."""
225
249
 
226
- Usage:
227
- async with JobsClient(base_url, api_key) as c:
228
- await c.files.list()
229
- """
230
-
231
- def __init__(self, base_url: str, api_key: str, timeout: float = 30.0, http: Optional[AsyncHttpClient] = None) -> None:
250
+ def __init__(
251
+ self,
252
+ base_url: str,
253
+ api_key: str,
254
+ timeout: float = 30.0,
255
+ http: AsyncHttpClient | None = None,
256
+ ) -> None:
232
257
  self._base_url = base_url
233
258
  self._api_key = api_key
234
259
  self._timeout = timeout
@@ -238,7 +263,7 @@ class JobsClient:
238
263
  self.rl = RlJobsApi(self._http)
239
264
  self.models = ModelsApi(self._http)
240
265
 
241
- async def __aenter__(self) -> "JobsClient":
266
+ async def __aenter__(self) -> JobsClient:
242
267
  await self._http.__aenter__()
243
268
  return self
244
269
 
@@ -1,16 +1,51 @@
1
+ from synth_ai.task import task_app_health, validate_task_app_url
2
+
1
3
  from .client import LearningClient
2
- from .rl_client import RlClient
3
- from .ft_client import FtClient
4
- from .validators import validate_training_jsonl, validate_trainer_cfg_rl
5
- from synth_ai.task import validate_task_app_url, task_app_health
6
- from .health import backend_health, pricing_preflight, balance_autumn_normalized
7
- from .sse import stream_events as stream_job_events
4
+ from .health import backend_health, balance_autumn_normalized, pricing_preflight
8
5
  from .jobs import JobHandle, JobsApiResolver
6
+ from .rl import (
7
+ MAX_ENVIRONMENT_API_KEY_BYTES,
8
+ RlClient,
9
+ RLJobConfig,
10
+ RolloutEnvSpec,
11
+ RolloutMetrics,
12
+ RolloutPolicySpec,
13
+ RolloutRecordConfig,
14
+ RolloutRequest,
15
+ RolloutResponse,
16
+ RolloutSafetyConfig,
17
+ RolloutStep,
18
+ RolloutTrajectory,
19
+ encrypt_for_backend,
20
+ mint_environment_api_key,
21
+ setup_environment_api_key,
22
+ )
23
+ from .sft import FtClient
24
+ from .sft.config import SFTJobConfig, prepare_sft_job_payload
25
+ from .sse import stream_events as stream_job_events
26
+ from .validators import validate_trainer_cfg_rl, validate_training_jsonl
9
27
 
10
28
  __all__ = [
11
29
  "LearningClient",
12
30
  "RlClient",
31
+ "RLJobConfig",
13
32
  "FtClient",
33
+ "SFTJobConfig",
34
+ "prepare_sft_job_payload",
35
+ "RolloutEnvSpec",
36
+ "RolloutPolicySpec",
37
+ "RolloutRecordConfig",
38
+ "RolloutSafetyConfig",
39
+ "RolloutRequest",
40
+ "RolloutStep",
41
+ "RolloutTrajectory",
42
+ "RolloutMetrics",
43
+ "RolloutResponse",
44
+ "mint_environment_api_key",
45
+ "encrypt_for_backend",
46
+ "setup_environment_api_key",
47
+ "MAX_ENVIRONMENT_API_KEY_BYTES",
48
+ # convenience re-export for typing
14
49
  "validate_training_jsonl",
15
50
  "validate_trainer_cfg_rl",
16
51
  "validate_task_app_url",
@@ -0,0 +1,14 @@
1
+ # class LearningModality(str, enum.Enum):
2
+ # """Modality of learning."""
3
+
4
+ # online_on_policy = "online_on_policy"
5
+ # online_off_policy = "online_off_policy"
6
+ # offline = "offline"
7
+
8
+
9
+ # class LearningAlgorithm(str, enum.Enum):
10
+ # """Algorithm of learning."""
11
+
12
+ # gspo = "gspo"
13
+ # reinforce = "reinforce"
14
+ # sft = "sft"
@@ -1,7 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections.abc import Callable
4
+ from contextlib import suppress
3
5
  from pathlib import Path
4
- from typing import Any, Callable, Dict, List, Optional
6
+ from typing import Any, TypedDict
7
+
8
+ from synth_ai.api.models.supported import (
9
+ UnsupportedModelError,
10
+ normalize_model_identifier,
11
+ )
12
+ from synth_ai.learning.sft.config import prepare_sft_job_payload
5
13
 
6
14
  from ..http import AsyncHttpClient, HTTPError, sleep
7
15
 
@@ -20,7 +28,12 @@ class LearningClient:
20
28
  files = {"file": (p.name, content, _infer_content_type(p.name))}
21
29
  js = await http.post_multipart("/api/learning/files", data=data, files=files)
22
30
  if not isinstance(js, dict) or "id" not in js:
23
- raise HTTPError(status=500, url="/api/learning/files", message="invalid_upload_response", body_snippet=str(js)[:200])
31
+ raise HTTPError(
32
+ status=500,
33
+ url="/api/learning/files",
34
+ message="invalid_upload_response",
35
+ body_snippet=str(js)[:200],
36
+ )
24
37
  return str(js["id"])
25
38
 
26
39
  async def create_job(
@@ -29,28 +42,56 @@ class LearningClient:
29
42
  training_type: str,
30
43
  model: str,
31
44
  training_file_id: str,
32
- hyperparameters: Optional[Dict[str, Any]] = None,
33
- metadata: Optional[Dict[str, Any]] = None,
34
- ) -> Dict[str, Any]:
35
- body = {
36
- "training_type": training_type,
37
- "model": model,
38
- "training_file_id": training_file_id,
39
- "hyperparameters": hyperparameters or {},
40
- "metadata": metadata or {},
41
- }
45
+ hyperparameters: dict[str, Any] | None = None,
46
+ metadata: dict[str, Any] | None = None,
47
+ validation_file: str | None = None,
48
+ ) -> dict[str, Any]:
49
+ lower_type = (training_type or "").strip().lower()
50
+ require_base = (
51
+ lower_type.startswith("sft")
52
+ or lower_type.startswith("fft")
53
+ or lower_type.startswith("qft")
54
+ )
55
+ try:
56
+ normalized_model = normalize_model_identifier(
57
+ model, allow_finetuned_prefixes=not require_base
58
+ )
59
+ except UnsupportedModelError as exc:
60
+ raise ValueError(str(exc)) from exc
61
+
62
+ if lower_type.startswith("sft") or lower_type in {"fft", "qft"}:
63
+ body = prepare_sft_job_payload(
64
+ model=model,
65
+ training_file=training_file_id,
66
+ hyperparameters=hyperparameters,
67
+ metadata=metadata,
68
+ training_type=training_type or "sft_offline",
69
+ validation_file=validation_file,
70
+ training_file_field="training_file_id",
71
+ require_training_file=True,
72
+ )
73
+ else:
74
+ body = {
75
+ "training_type": training_type,
76
+ "model": normalized_model,
77
+ "training_file_id": training_file_id,
78
+ "hyperparameters": hyperparameters or {},
79
+ "metadata": metadata or {},
80
+ }
42
81
  async with AsyncHttpClient(self._base_url, self._api_key, timeout=self._timeout) as http:
43
82
  return await http.post_json("/api/learning/jobs", json=body)
44
83
 
45
- async def start_job(self, job_id: str) -> Dict[str, Any]:
84
+ async def start_job(self, job_id: str) -> dict[str, Any]:
46
85
  async with AsyncHttpClient(self._base_url, self._api_key, timeout=self._timeout) as http:
47
86
  return await http.post_json(f"/api/learning/jobs/{job_id}/start", json={})
48
87
 
49
- async def get_job(self, job_id: str) -> Dict[str, Any]:
88
+ async def get_job(self, job_id: str) -> dict[str, Any]:
50
89
  async with AsyncHttpClient(self._base_url, self._api_key, timeout=self._timeout) as http:
51
90
  return await http.get(f"/api/learning/jobs/{job_id}")
52
91
 
53
- async def get_events(self, job_id: str, *, since_seq: int = 0, limit: int = 200) -> List[Dict[str, Any]]:
92
+ async def get_events(
93
+ self, job_id: str, *, since_seq: int = 0, limit: int = 200
94
+ ) -> list[dict[str, Any]]:
54
95
  params = {"since_seq": since_seq, "limit": limit}
55
96
  async with AsyncHttpClient(self._base_url, self._api_key, timeout=self._timeout) as http:
56
97
  js = await http.get(f"/api/learning/jobs/{job_id}/events", params=params)
@@ -58,8 +99,16 @@ class LearningClient:
58
99
  return js["events"]
59
100
  return []
60
101
 
61
- async def get_metrics(self, job_id: str, *, name: str | None = None, after_step: int | None = None, limit: int = 500, run_id: str | None = None) -> List[Dict[str, Any]]:
62
- params: Dict[str, Any] = {"limit": limit}
102
+ async def get_metrics(
103
+ self,
104
+ job_id: str,
105
+ *,
106
+ name: str | None = None,
107
+ after_step: int | None = None,
108
+ limit: int = 500,
109
+ run_id: str | None = None,
110
+ ) -> list[dict[str, Any]]:
111
+ params: dict[str, Any] = {"limit": limit}
63
112
  if name is not None:
64
113
  params["name"] = name
65
114
  if after_step is not None:
@@ -72,7 +121,7 @@ class LearningClient:
72
121
  return js["points"]
73
122
  return []
74
123
 
75
- async def get_timeline(self, job_id: str, *, limit: int = 200) -> List[Dict[str, Any]]:
124
+ async def get_timeline(self, job_id: str, *, limit: int = 200) -> list[dict[str, Any]]:
76
125
  params = {"limit": limit}
77
126
  async with AsyncHttpClient(self._base_url, self._api_key, timeout=self._timeout) as http:
78
127
  js = await http.get(f"/api/learning/jobs/{job_id}/timeline", params=params)
@@ -86,8 +135,8 @@ class LearningClient:
86
135
  *,
87
136
  interval_seconds: float = 2.0,
88
137
  max_seconds: float | None = 3600,
89
- on_event: Callable[[Dict[str, Any]], None] | None = None,
90
- ) -> Dict[str, Any]:
138
+ on_event: Callable[[dict[str, Any]], None] | None = None,
139
+ ) -> dict[str, Any]:
91
140
  last_seq = 0
92
141
  elapsed = 0.0
93
142
  while True:
@@ -97,10 +146,8 @@ class LearningClient:
97
146
  if isinstance(e, dict) and isinstance(e.get("seq"), int):
98
147
  last_seq = max(last_seq, int(e["seq"]))
99
148
  if on_event:
100
- try:
149
+ with suppress(Exception):
101
150
  on_event(e)
102
- except Exception:
103
- pass
104
151
 
105
152
  # Status
106
153
  job = await self.get_job(job_id)
@@ -115,7 +162,9 @@ class LearningClient:
115
162
  raise TimeoutError(f"Polling timed out after {elapsed} seconds for job {job_id}")
116
163
 
117
164
  # --- Optional diagnostics ---
118
- async def pricing_preflight(self, *, job_type: str, gpu_type: str, estimated_seconds: float, container_count: int) -> Dict[str, Any]:
165
+ async def pricing_preflight(
166
+ self, *, job_type: str, gpu_type: str, estimated_seconds: float, container_count: int
167
+ ) -> dict[str, Any]:
119
168
  body = {
120
169
  "job_type": job_type,
121
170
  "gpu_type": gpu_type,
@@ -125,17 +174,62 @@ class LearningClient:
125
174
  async with AsyncHttpClient(self._base_url, self._api_key, timeout=self._timeout) as http:
126
175
  js = await http.post_json("/api/v1/pricing/preflight", json=body)
127
176
  if not isinstance(js, dict):
128
- raise HTTPError(status=500, url="/api/v1/pricing/preflight", message="invalid_preflight_response", body_snippet=str(js)[:200])
177
+ raise HTTPError(
178
+ status=500,
179
+ url="/api/v1/pricing/preflight",
180
+ message="invalid_preflight_response",
181
+ body_snippet=str(js)[:200],
182
+ )
129
183
  return js
130
184
 
131
- async def balance_autumn_normalized(self) -> Dict[str, Any]:
185
+ async def balance_autumn_normalized(self) -> dict[str, Any]:
132
186
  async with AsyncHttpClient(self._base_url, self._api_key, timeout=self._timeout) as http:
133
187
  js = await http.get("/api/v1/balance/autumn-normalized")
134
188
  if not isinstance(js, dict):
135
- raise HTTPError(status=500, url="/api/v1/balance/autumn-normalized", message="invalid_balance_response", body_snippet=str(js)[:200])
189
+ raise HTTPError(
190
+ status=500,
191
+ url="/api/v1/balance/autumn-normalized",
192
+ message="invalid_balance_response",
193
+ body_snippet=str(js)[:200],
194
+ )
136
195
  return js
137
196
 
138
197
 
198
+ class FineTunedModelInfo(TypedDict, total=False):
199
+ id: str
200
+ base_model: str | None
201
+ created_at: int | None
202
+ job_id: str | None
203
+ status: str | None
204
+
205
+
206
+ class LearningClient(LearningClient): # type: ignore[misc]
207
+ async def list_fine_tuned_models(self) -> list[FineTunedModelInfo]:
208
+ """Return completed fine‑tuned models for the caller's organization.
209
+
210
+ Calls backend route `/api/learning/models` and returns a compact list.
211
+ """
212
+ async with AsyncHttpClient(self._base_url, self._api_key, timeout=self._timeout) as http:
213
+ js = await http.get("/api/learning/models")
214
+ if isinstance(js, dict) and isinstance(js.get("data"), list):
215
+ out: list[FineTunedModelInfo] = []
216
+ for item in js["data"]:
217
+ if not isinstance(item, dict):
218
+ continue
219
+ rec: FineTunedModelInfo = {
220
+ "id": str(item.get("id")),
221
+ "base_model": item.get("base_model"),
222
+ "created_at": item.get("created_at"),
223
+ "job_id": item.get("job_id"),
224
+ "status": item.get("status"),
225
+ }
226
+ if rec.get("id"):
227
+ out.append(rec)
228
+ return out
229
+ # Fallback: empty list on unexpected shape
230
+ return []
231
+
232
+
139
233
  def _infer_content_type(filename: str) -> str:
140
234
  name = filename.lower()
141
235
  if name.endswith(".jsonl"):
@@ -145,5 +239,3 @@ def _infer_content_type(filename: str) -> str:
145
239
  if name.endswith(".txt"):
146
240
  return "text/plain"
147
241
  return "application/octet-stream"
148
-
149
-
@@ -1,43 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- from dataclasses import dataclass
4
- from typing import Any, Dict, Optional
5
-
6
-
7
- @dataclass
8
- class FTJobConfig:
9
- model: str
10
- training_file_id: str
11
- n_epochs: int = 1
12
- batch_size: int = 1
13
- upload_to_wasabi: bool = True
14
-
15
- def hyperparameters(self) -> Dict[str, Any]:
16
- if self.n_epochs < 1:
17
- raise ValueError("n_epochs must be >= 1")
18
- if self.batch_size < 1:
19
- raise ValueError("batch_size must be >= 1")
20
- return {"n_epochs": int(self.n_epochs), "batch_size": int(self.batch_size)}
21
-
22
- def metadata(self) -> Dict[str, Any]: # type: ignore[override]
23
- return {"upload_to_wasabi": bool(self.upload_to_wasabi)}
24
-
25
-
26
- @dataclass
27
- class RLJobConfig:
28
- model: str
29
- task_app_url: str
30
- trainer_id: str
31
- batch_size: int = 1
32
- group_size: int = 2
33
- job_config_id: Optional[str] = None
34
- inline_config: Optional[Dict[str, Any]] = None
35
-
36
- def trainer_dict(self) -> Dict[str, Any]:
37
- if self.batch_size < 1:
38
- raise ValueError("batch_size must be >= 1")
39
- if self.group_size < 2:
40
- raise ValueError("group_size must be >= 2")
41
- return {"batch_size": int(self.batch_size), "group_size": int(self.group_size)}
42
-
3
+ from .rl.config import RLJobConfig
43
4
 
5
+ __all__ = ["RLJobConfig"]
@@ -25,5 +25,3 @@ TERMINAL_EVENT_FAILURE = {
25
25
  "rl.job.failed",
26
26
  "workflow.failed",
27
27
  }
28
-
29
-