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,14 +1,13 @@
1
- from __future__ import annotations
2
-
3
1
  """Modal task app for Hendrycks MATH single-step RL environment."""
4
2
 
3
+ from __future__ import annotations
4
+
5
5
  import os
6
+ from collections.abc import Iterable
7
+ from functools import lru_cache
6
8
  from pathlib import Path
7
9
 
8
10
  from modal import App, Image, Secret, asgi_app
9
- from functools import lru_cache
10
- from typing import Iterable
11
-
12
11
  from starlette.requests import Request
13
12
 
14
13
  try: # Backward compatibility with older installed SDKs
@@ -99,8 +98,7 @@ app = App("hendrycks-math-task-app")
99
98
  @asgi_app()
100
99
  def fastapi_app():
101
100
  import httpx
102
- from fastapi import Body, HTTPException, status
103
- from fastapi import FastAPI
101
+ from fastapi import Body, FastAPI, HTTPException, status
104
102
  from fastapi.middleware.cors import CORSMiddleware
105
103
  from fastapi.responses import JSONResponse
106
104
 
@@ -388,7 +386,7 @@ def fastapi_app():
388
386
  try:
389
387
  hdr = request.headers
390
388
  snapshot = {
391
- "path": str(getattr(request, "url").path),
389
+ "path": str(request.url.path),
392
390
  "have_x_api_key": bool(hdr.get("x-api-key")),
393
391
  "have_x_api_keys": bool(hdr.get("x-api-keys")),
394
392
  "have_authorization": bool(hdr.get("authorization")),
@@ -412,32 +410,32 @@ def fastapi_app():
412
410
  env_key = (
413
411
  os.environ.get("ENVIRONMENT_API_KEY")
414
412
  or os.environ.get("DEV_ENVIRONMENT_API_KEY")
415
- or os.environ.get("dev_environment_api_key")
413
+ or os.environ.get("DEV_ENVIRONMENT_API_KEY")
416
414
  )
417
415
  if not env_key:
418
416
  raise RuntimeError("ENVIRONMENT_API_KEY missing in task app environment")
419
417
 
420
- OPENAI_REMOVE_FIELDS = (
418
+ openai_remove_fields = (
421
419
  "stop_after_tool_calls",
422
420
  "thinking_mode",
423
421
  "thinking_budget",
424
422
  "reasoning",
425
423
  )
426
- OPENAI_REMOVE_SAMPLING_FIELDS = ("temperature", "top_p")
427
- TOOL_CHOICE_FORCE = {"type": "function", "function": {"name": "submit_answer"}}
424
+ openai_remove_sampling_fields = ("temperature", "top_p")
425
+ tool_choice_force = {"type": "function", "function": {"name": "submit_answer"}}
428
426
 
429
427
  def _prepare_openai_payload(model: str | None, payload: dict[str, object]) -> dict[str, object]:
430
428
  sanitized = dict(payload)
431
- for key in OPENAI_REMOVE_FIELDS:
429
+ for key in openai_remove_fields:
432
430
  sanitized.pop(key, None)
433
431
  if model and "gpt-5" in model:
434
432
  if "max_tokens" in sanitized and "max_completion_tokens" not in sanitized:
435
433
  sanitized["max_completion_tokens"] = sanitized.pop("max_tokens")
436
434
  else:
437
435
  sanitized.pop("max_tokens", None)
438
- for field in OPENAI_REMOVE_SAMPLING_FIELDS:
436
+ for field in openai_remove_sampling_fields:
439
437
  sanitized.pop(field, None)
440
- sanitized["tool_choice"] = TOOL_CHOICE_FORCE
438
+ sanitized["tool_choice"] = tool_choice_force
441
439
  sanitized["parallel_tool_calls"] = False
442
440
  return sanitized
443
441
 
@@ -470,8 +468,8 @@ def fastapi_app():
470
468
  # Minimal math rollout endpoint: alternates agent/env; calls inference_url chat/completions
471
469
  @api.post("/rollout")
472
470
  def rollout(request: dict[str, object] = Body(...)):
473
- from typing import Any
474
471
  import json as _json
472
+ from typing import Any
475
473
 
476
474
  run_id = str(request.get("run_id"))
477
475
  data = request if isinstance(request, dict) else {}
@@ -563,7 +561,7 @@ def fastapi_app():
563
561
 
564
562
  user_prompt = (
565
563
  str(question)
566
- if isinstance(question, (str, int, float)) and str(question).strip()
564
+ if isinstance(question, str | int | float) and str(question).strip()
567
565
  else "Solve the problem. Provide answer steps succinctly."
568
566
  )
569
567
  payload = {
@@ -597,7 +595,7 @@ def fastapi_app():
597
595
  name = fn.get("name")
598
596
  if isinstance(name, str):
599
597
  tool_names.append(name)
600
- print(f"[math] system: <none>", flush=True)
598
+ print("[math] system: <none>", flush=True)
601
599
  print(f"[math] user: {user_prompt}", flush=True)
602
600
  print(f"[math] tools: {tool_names}", flush=True)
603
601
  except Exception:
@@ -5,7 +5,6 @@ from __future__ import annotations
5
5
  from synth_ai.task.apps import ModalDeploymentConfig, TaskAppEntry, register_task_app
6
6
  from synth_ai.task.apps.math_single_step import build_config as base_build_config
7
7
 
8
-
9
8
  DEMO_MODAL_CONFIG = ModalDeploymentConfig(
10
9
  app_name="hendrycks-math-task-app",
11
10
  pip_packages=(
@@ -2,11 +2,16 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import base64
5
6
  import dataclasses
6
7
  import logging
7
8
  import time
9
+ from io import BytesIO
8
10
  from typing import Any, Dict, List, Optional, Union
9
11
 
12
+ import numpy as np
13
+ from PIL import Image
14
+
10
15
  # Import tracing abstractions
11
16
  from synth_ai.tracing_v3.abstractions import (
12
17
  RuntimeEvent,
@@ -43,6 +48,51 @@ from synth_ai.environments.reproducibility.core import ReproducibleEnvironment
43
48
  from synth_ai.environments.stateful.core import StatefulEnvironment
44
49
 
45
50
 
51
+ def _convert_numpy_to_python(obj: Any) -> Any:
52
+ if isinstance(obj, np.integer):
53
+ return int(obj)
54
+ if isinstance(obj, np.floating):
55
+ return float(obj)
56
+ if isinstance(obj, np.ndarray):
57
+ return obj.tolist()
58
+ if isinstance(obj, dict):
59
+ return {k: _convert_numpy_to_python(v) for k, v in obj.items()}
60
+ if isinstance(obj, (list, tuple)):
61
+ return [_convert_numpy_to_python(item) for item in obj]
62
+ return obj
63
+
64
+
65
+ def _encode_image_to_base64(image_array: Any) -> dict[str, Any] | None:
66
+ if not isinstance(image_array, np.ndarray):
67
+ return None
68
+ if image_array.ndim != 3 or image_array.shape[-1] not in (1, 3, 4):
69
+ return None
70
+ try:
71
+ array_uint8 = (
72
+ image_array.astype("uint8")
73
+ if image_array.dtype != np.uint8
74
+ else image_array # pragma: no cover - fast path
75
+ )
76
+ mode = "L" if array_uint8.shape[-1] == 1 else "RGB"
77
+ if array_uint8.shape[-1] == 4:
78
+ mode = "RGBA"
79
+ img = Image.fromarray(array_uint8, mode=mode)
80
+ buffer = BytesIO()
81
+ img.save(buffer, format="PNG")
82
+ encoded = base64.b64encode(buffer.getvalue()).decode("ascii")
83
+ width = int(array_uint8.shape[1])
84
+ height = int(array_uint8.shape[0])
85
+ return {
86
+ "format": "png",
87
+ "width": width,
88
+ "height": height,
89
+ "data": encoded,
90
+ "data_url": f"data:image/png;base64,{encoded}",
91
+ }
92
+ except Exception:
93
+ return None
94
+
95
+
46
96
  # --- Tool Definition ---
47
97
  class CrafterActionInput(BaseModel):
48
98
  action: int = Field(..., description="Integer action for the Crafter environment.")
@@ -362,7 +412,8 @@ class CrafterClassicEnvironment(StatefulEnvironment, ReproducibleEnvironment[Cra
362
412
  state_before = {"private_state": priv, "public_state": pub}
363
413
 
364
414
  active_obs_cb = obs_cb or SynthCrafterObservationCallable()
365
- observation = await active_obs_cb.get_observation(pub, priv)
415
+ raw_observation = await active_obs_cb.get_observation(pub, priv)
416
+ observation = self._prepare_observation(raw_observation)
366
417
  if extra_obs and isinstance(observation, dict):
367
418
  observation.update(extra_obs)
368
419
 
@@ -385,6 +436,30 @@ class CrafterClassicEnvironment(StatefulEnvironment, ReproducibleEnvironment[Cra
385
436
 
386
437
  return observation
387
438
 
439
+ def _prepare_observation(self, observation: Any) -> dict[str, Any]:
440
+ obs_dict: dict[str, Any]
441
+ image_payload: dict[str, Any] | None = None
442
+
443
+ if isinstance(observation, dict):
444
+ image_payload = _encode_image_to_base64(observation.get("observation_image"))
445
+ sanitized = dict(observation)
446
+ sanitized.pop("observation_image", None)
447
+ obs_dict = _convert_numpy_to_python(sanitized) or {}
448
+ else:
449
+ obs_dict = _convert_numpy_to_python(observation) or {}
450
+
451
+ if not isinstance(obs_dict, dict):
452
+ obs_dict = {"value": obs_dict}
453
+
454
+ if image_payload:
455
+ obs_dict["observation_image_base64"] = image_payload["data"]
456
+ obs_dict["observation_image_format"] = image_payload["format"]
457
+ obs_dict["observation_image_width"] = image_payload["width"]
458
+ obs_dict["observation_image_height"] = image_payload["height"]
459
+ obs_dict["observation_image_data_url"] = image_payload["data_url"]
460
+
461
+ return obs_dict
462
+
388
463
  # ────────────────────────────────────────────────────────────────────
389
464
  # ReproducibleEnvironment plumbing
390
465
  # ────────────────────────────────────────────────────────────────────
@@ -14,8 +14,10 @@ big “backend.production” code-base.
14
14
  from __future__ import annotations
15
15
 
16
16
  import gzip
17
+ import hashlib
17
18
  import json
18
19
  import logging
20
+ import os
19
21
  import pickle
20
22
  import sqlite3
21
23
  from collections.abc import Iterable
@@ -32,11 +34,6 @@ log = logging.getLogger(__name__)
32
34
  # --------------------------------------------------------------------------- #
33
35
  # lightweight metadata record #
34
36
  # --------------------------------------------------------------------------- #
35
- import hashlib
36
- import logging
37
- import os
38
-
39
- log = logging.getLogger(__name__)
40
37
 
41
38
  # Default directory for storing snapshots relative to some base path
42
39
  # This could be configured via environment variables or settings later.
@@ -1,6 +1,17 @@
1
+ import logging
1
2
  import os # Added to ensure os is available before use
2
3
  import sys
3
4
 
5
+ import synth_ai.environments.examples.crafter_classic.environment as cc
6
+ import synth_ai.environments.examples.crafter_custom.environment as ccustom
7
+ from fastapi import FastAPI
8
+ from synth_ai.environments.service.core_routes import api_router
9
+ from synth_ai.environments.service.external_registry import (
10
+ ExternalRegistryConfig,
11
+ load_external_environments,
12
+ )
13
+ from synth_ai.environments.service.registry import list_supported_env_types, register_environment
14
+
4
15
  # Ensure repository root is on PYTHONPATH for dev installs
5
16
  # Current file path: <repo>/synth_ai/environments/service/app.py
6
17
  # We want sys.path to include <repo>, NOT <repo>/synth_ai to avoid shadowing stdlib 'http'
@@ -16,15 +27,6 @@ if _repo_root not in sys.path:
16
27
  sys.path.insert(0, _repo_root)
17
28
 
18
29
  print(f"SYS.PATH IN APP.PY: {sys.path}")
19
- import logging
20
-
21
- from fastapi import FastAPI
22
- from synth_ai.environments.service.core_routes import api_router
23
- from synth_ai.environments.service.external_registry import (
24
- ExternalRegistryConfig,
25
- load_external_environments,
26
- )
27
- from synth_ai.environments.service.registry import list_supported_env_types, register_environment
28
30
 
29
31
  # Configure logging with more detail
30
32
  logging.basicConfig(
@@ -38,11 +40,8 @@ logger = logging.getLogger(__name__)
38
40
  logging.getLogger("uvicorn.access").setLevel(logging.INFO)
39
41
 
40
42
  # Register built-in environments at import time
41
- import synth_ai.environments.examples.crafter_classic.environment as cc
42
43
 
43
44
  register_environment("CrafterClassic", cc.CrafterClassicEnvironment)
44
- import synth_ai.environments.examples.crafter_custom.environment as ccustom
45
-
46
45
  register_environment("CrafterCustom", ccustom.CrafterCustomEnvironment)
47
46
 
48
47
  # Register Wordle example environment
@@ -97,15 +97,12 @@ def create_task_instance_for_environment(
97
97
  task.initial_engine_snapshot["seed"] = config["seed"]
98
98
 
99
99
  # For CrafterClassic, also handle difficulty
100
- if env_name == "CrafterClassic" and config:
101
- if "difficulty" in config:
102
- task.initial_engine_snapshot["difficulty"] = config["difficulty"]
100
+ if env_name == "CrafterClassic" and config and "difficulty" in config:
101
+ task.initial_engine_snapshot["difficulty"] = config["difficulty"]
103
102
 
104
103
  # For MiniGrid, handle environment selection
105
- if env_name == "MiniGrid" and config:
106
- # Check if a specific environment is requested
107
- if "env_name" in config:
108
- task.initial_engine_snapshot["env_name"] = config["env_name"]
104
+ if env_name == "MiniGrid" and config and "env_name" in config:
105
+ task.initial_engine_snapshot["env_name"] = config["env_name"]
109
106
 
110
107
  return task
111
108
 
@@ -14,7 +14,7 @@ class StatefulEngine(Engine):
14
14
  pass
15
15
 
16
16
  @classmethod
17
- async def deserialize(self, engine_snapshot: StatefulEngineSnapshot):
17
+ async def deserialize(cls, engine_snapshot: StatefulEngineSnapshot):
18
18
  pass
19
19
 
20
20
  async def _step_engine(self):
@@ -4,6 +4,7 @@ from dataclasses import dataclass, field
4
4
  from typing import Any, Optional
5
5
  from uuid import UUID
6
6
 
7
+ from synth_ai.environments.stateful.engine import StatefulEngineSnapshot
7
8
  from synth_ai.environments.v0_observability.history import SynthGlobalTrajectory
8
9
 
9
10
 
@@ -29,13 +29,12 @@ class RangeFilter(TaskInstanceMetadataFilter):
29
29
  # If the attribute doesn't exist on the metadata, it can't be in range.
30
30
  return False
31
31
 
32
- if not isinstance(instance_value, (int, float)):
32
+ if not isinstance(instance_value, int | float):
33
33
  # If the attribute is not a number, it can't be in a numerical range.
34
34
  # Or, we could raise an error, depending on desired strictness.
35
35
  return False
36
36
 
37
- if self.min_val is not None and instance_value < self.min_val:
38
- return False
39
- if self.max_val is not None and instance_value > self.max_val:
40
- return False
41
- return True
37
+ return not (
38
+ (self.min_val is not None and instance_value < self.min_val)
39
+ or (self.max_val is not None and instance_value > self.max_val)
40
+ )
@@ -54,11 +54,10 @@ class RangeFilter(TaskInstanceMetadataFilter):
54
54
 
55
55
  def __call__(self, instance: TaskInstance) -> bool:
56
56
  value = getattr(instance.metadata, self.key, None)
57
- if self.min_value is not None and (value is None or value < self.min_value):
58
- return False
59
- if self.max_value is not None and (value is None or value > self.max_value):
60
- return False
61
- return True
57
+ return not (
58
+ (self.min_value is not None and (value is None or value < self.min_value))
59
+ or (self.max_value is not None and (value is None or value > self.max_value))
60
+ )
62
61
 
63
62
 
64
63
  def make_taskset(
synth_ai/handshake.py CHANGED
@@ -1,8 +1,10 @@
1
1
  from __future__ import annotations
2
+
3
+ import contextlib
2
4
  import os
3
5
  import time
4
6
  import webbrowser
5
- from typing import Any, Dict, Tuple
7
+ from typing import Any
6
8
  from urllib.parse import urljoin, urlsplit, urlunsplit
7
9
 
8
10
  import requests
@@ -43,7 +45,7 @@ def _split_origin(origin: str) -> tuple[str, str]:
43
45
  return bare, path
44
46
 
45
47
 
46
- def _ensure_verification_uri(data: Dict[str, Any], base_with_path: str) -> None:
48
+ def _ensure_verification_uri(data: dict[str, Any], base_with_path: str) -> None:
47
49
  uri = data.get("verification_uri")
48
50
  if not isinstance(uri, str) or not uri:
49
51
  return
@@ -52,7 +54,7 @@ def _ensure_verification_uri(data: Dict[str, Any], base_with_path: str) -> None:
52
54
  data["verification_uri"] = urljoin(base_with_path.rstrip("/") + "/", uri.lstrip("/"))
53
55
 
54
56
 
55
- def start_handshake_session(origin: str | None = None) -> Tuple[str, str, int, int]:
57
+ def start_handshake_session(origin: str | None = None) -> tuple[str, str, int, int]:
56
58
  base = (origin or _origin()).rstrip("/")
57
59
  api_origin, _ = _split_origin(base)
58
60
  url = urljoin(api_origin.rstrip("/") + "/", "api/sdk/handshake/init")
@@ -74,7 +76,7 @@ def start_handshake_session(origin: str | None = None) -> Tuple[str, str, int, i
74
76
 
75
77
  def poll_handshake_token(
76
78
  device_code: str, origin: str | None = None, *, timeout_s: int | None = None
77
- ) -> Dict[str, Any]:
79
+ ) -> dict[str, Any]:
78
80
  base = (origin or _origin()).rstrip("/")
79
81
  api_origin, _ = _split_origin(base)
80
82
  url = urljoin(api_origin.rstrip("/") + "/", "api/sdk/handshake/token")
@@ -84,7 +86,7 @@ def poll_handshake_token(
84
86
  raise HandshakeError("handshake timed out")
85
87
  try:
86
88
  r = requests.post(url, json={"device_code": device_code}, timeout=10)
87
- except Exception as e:
89
+ except Exception:
88
90
  time.sleep(2)
89
91
  continue
90
92
  if r.status_code == 200:
@@ -100,10 +102,8 @@ def poll_handshake_token(
100
102
  time.sleep(2)
101
103
 
102
104
 
103
- def run_handshake(origin: str | None = None) -> Dict[str, Any]:
105
+ def run_handshake(origin: str | None = None) -> dict[str, Any]:
104
106
  device_code, verification_uri, expires_in, interval = start_handshake_session(origin)
105
- try:
107
+ with contextlib.suppress(Exception):
106
108
  webbrowser.open(verification_uri)
107
- except Exception:
108
- pass
109
109
  return poll_handshake_token(device_code, origin, timeout_s=expires_in)
synth_ai/http.py CHANGED
@@ -18,7 +18,7 @@ except Exception:
18
18
  _client_path = _here.parent / "http_client.py"
19
19
  _spec = _ilu.spec_from_file_location("http_client", str(_client_path))
20
20
  if not _spec or not _spec.loader:
21
- raise ImportError("Could not load http_client module")
21
+ raise ImportError("Could not load http_client module") from None
22
22
  _mod = _ilu.module_from_spec(_spec)
23
23
  _spec.loader.exec_module(_mod)
24
24
  _sys.modules["synth_ai.http_client"] = _mod
synth_ai/http_client.py CHANGED
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ import os
4
5
  from dataclasses import dataclass
5
- from typing import Any, Dict, Optional
6
+ from typing import Any
6
7
 
7
8
  import aiohttp
8
9
 
@@ -27,11 +28,18 @@ class AsyncHttpClient:
27
28
  self._base_url = base_url.rstrip("/")
28
29
  self._api_key = api_key
29
30
  self._timeout = aiohttp.ClientTimeout(total=timeout)
30
- self._session: Optional[aiohttp.ClientSession] = None
31
+ self._session: aiohttp.ClientSession | None = None
31
32
 
32
- async def __aenter__(self) -> "AsyncHttpClient":
33
+ async def __aenter__(self) -> AsyncHttpClient:
33
34
  if self._session is None:
34
35
  headers = {"authorization": f"Bearer {self._api_key}"}
36
+ # Optional dev overrides for user/org context
37
+ user_id = os.getenv("SYNTH_USER_ID") or os.getenv("X_USER_ID") or os.getenv("USER_ID")
38
+ if user_id:
39
+ headers["X-User-ID"] = user_id
40
+ org_id = os.getenv("SYNTH_ORG_ID") or os.getenv("X_ORG_ID") or os.getenv("ORG_ID")
41
+ if org_id:
42
+ headers["X-Org-ID"] = org_id
35
43
  self._session = aiohttp.ClientSession(headers=headers, timeout=self._timeout)
36
44
  return self
37
45
 
@@ -52,8 +60,8 @@ class AsyncHttpClient:
52
60
  self,
53
61
  path: str,
54
62
  *,
55
- params: Optional[Dict[str, Any]] = None,
56
- headers: Optional[Dict[str, str]] = None,
63
+ params: dict[str, Any] | None = None,
64
+ headers: dict[str, str] | None = None,
57
65
  ) -> Any:
58
66
  url = self._abs(path)
59
67
  assert self._session is not None, "AsyncHttpClient must be used as an async context manager"
@@ -61,7 +69,7 @@ class AsyncHttpClient:
61
69
  return await self._handle_response(resp, url)
62
70
 
63
71
  async def post_json(
64
- self, path: str, *, json: Dict[str, Any], headers: Optional[Dict[str, str]] = None
72
+ self, path: str, *, json: dict[str, Any], headers: dict[str, str] | None = None
65
73
  ) -> Any:
66
74
  url = self._abs(path)
67
75
  assert self._session is not None, "AsyncHttpClient must be used as an async context manager"
@@ -72,9 +80,9 @@ class AsyncHttpClient:
72
80
  self,
73
81
  path: str,
74
82
  *,
75
- data: Dict[str, Any],
76
- files: Dict[str, tuple[str, bytes, str | None]],
77
- headers: Optional[Dict[str, str]] = None,
83
+ data: dict[str, Any],
84
+ files: dict[str, tuple[str, bytes, str | None]],
85
+ headers: dict[str, str] | None = None,
78
86
  ) -> Any:
79
87
  url = self._abs(path)
80
88
  assert self._session is not None, "AsyncHttpClient must be used as an async context manager"
@@ -91,7 +99,7 @@ class AsyncHttpClient:
91
99
  async with self._session.post(url, data=form, headers=headers) as resp:
92
100
  return await self._handle_response(resp, url)
93
101
 
94
- async def delete(self, path: str, *, headers: Optional[Dict[str, str]] = None) -> Any:
102
+ async def delete(self, path: str, *, headers: dict[str, str] | None = None) -> Any:
95
103
  url = self._abs(path)
96
104
  assert self._session is not None, "AsyncHttpClient must be used as an async context manager"
97
105
  async with self._session.delete(url, headers=headers) as resp:
@@ -1,6 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any, Dict
3
+ from typing import Any
4
+
5
+ from synth_ai.api.models.supported import (
6
+ UnsupportedModelError,
7
+ normalize_model_identifier,
8
+ )
4
9
 
5
10
  from ..http import AsyncHttpClient
6
11
 
@@ -13,12 +18,17 @@ class InferenceClient:
13
18
 
14
19
  async def create_chat_completion(
15
20
  self, *, model: str, messages: list[dict], **kwargs: Any
16
- ) -> Dict[str, Any]:
17
- body: Dict[str, Any] = {"model": model, "messages": messages}
21
+ ) -> dict[str, Any]:
22
+ try:
23
+ normalized_model = normalize_model_identifier(model)
24
+ except UnsupportedModelError as exc:
25
+ raise ValueError(str(exc)) from exc
26
+
27
+ body: dict[str, Any] = {"model": normalized_model, "messages": messages}
18
28
  body.update(kwargs)
19
29
  # Backend now expects an explicit thinking_budget; provide a sensible default if omitted
20
30
  if "thinking_budget" not in body:
21
31
  body["thinking_budget"] = 256
22
32
  async with AsyncHttpClient(self._base_url, self._api_key, timeout=self._timeout) as http:
23
- # Public learning-v2 inference path mounted under /api/v1
24
- return await http.post_json("/api/v1/chat/completions", json=body)
33
+ # Route through backend inference proxy to Modal
34
+ return await http.post_json("/api/inference/v1/chat/completions", json=body)