synth-ai 0.2.9.dev4__py3-none-any.whl → 0.2.9.dev6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of synth-ai might be problematic. Click here for more details.

Files changed (353) hide show
  1. examples/__init__.py +16 -0
  2. examples/crafter_debug_render.py +23 -17
  3. examples/qwen_coder/README.md +102 -0
  4. examples/qwen_coder/_shared.py +113 -0
  5. examples/qwen_coder/configs/coder_lora_30b.toml +61 -0
  6. examples/qwen_coder/configs/coder_lora_4b.toml +57 -0
  7. examples/qwen_coder/configs/coder_lora_small.toml +58 -0
  8. examples/qwen_coder/generate_dataset.py +98 -0
  9. examples/qwen_coder/infer_ft_smoke.py +64 -0
  10. examples/qwen_coder/infer_prod_proxy.py +73 -0
  11. examples/qwen_coder/infer_via_synth.py +87 -0
  12. examples/qwen_coder/scripts/infer_coder.sh +18 -0
  13. examples/qwen_coder/scripts/train_coder_30b.sh +21 -0
  14. examples/qwen_coder/sft_full_17b.py +103 -0
  15. examples/qwen_coder/sft_lora_30b.py +110 -0
  16. examples/qwen_coder/subset_jsonl.py +38 -0
  17. examples/qwen_coder/validate_jsonl.py +59 -0
  18. examples/rl/configs/eval_base_qwen.toml +1 -1
  19. examples/rl/configs/rl_from_base_qwen17.toml +1 -1
  20. examples/rl/download_dataset.py +26 -10
  21. examples/rl/run_eval.py +53 -52
  22. examples/rl/run_rl_and_save.py +29 -12
  23. examples/rl/task_app/math_single_step.py +180 -41
  24. examples/rl/task_app/math_task_app.py +14 -6
  25. examples/sft/README.md +139 -0
  26. examples/sft/configs/crafter_fft_qwen0p6b.toml +44 -0
  27. examples/sft/configs/crafter_lora_qwen0p6b.toml +45 -0
  28. examples/sft/evaluate.py +117 -0
  29. examples/sft/export_dataset.py +117 -0
  30. examples/sft/generate_traces.py +162 -0
  31. examples/swe/__init__.py +12 -0
  32. examples/swe/task_app/README.md +105 -0
  33. examples/swe/task_app/__init__.py +2 -0
  34. examples/swe/task_app/grpo_swe_mini.py +571 -0
  35. examples/swe/task_app/grpo_swe_mini_task_app.py +136 -0
  36. examples/swe/task_app/hosted/README.md +173 -0
  37. examples/swe/task_app/hosted/__init__.py +5 -0
  38. examples/swe/task_app/hosted/branching.py +143 -0
  39. examples/swe/task_app/hosted/environment_routes.py +1289 -0
  40. examples/swe/task_app/hosted/envs/__init__.py +1 -0
  41. examples/swe/task_app/hosted/envs/crafter/__init__.py +6 -0
  42. examples/swe/task_app/hosted/envs/crafter/app.py +1 -0
  43. examples/swe/task_app/hosted/envs/crafter/environment.py +522 -0
  44. examples/swe/task_app/hosted/envs/crafter/policy.py +478 -0
  45. examples/swe/task_app/hosted/envs/crafter/react_agent.py +108 -0
  46. examples/swe/task_app/hosted/envs/crafter/shared.py +305 -0
  47. examples/swe/task_app/hosted/envs/crafter/tools.py +47 -0
  48. examples/swe/task_app/hosted/envs/mini_swe/__init__.py +8 -0
  49. examples/swe/task_app/hosted/envs/mini_swe/environment.py +1164 -0
  50. examples/swe/task_app/hosted/envs/mini_swe/policy.py +355 -0
  51. examples/swe/task_app/hosted/envs/mini_swe/shared.py +83 -0
  52. examples/swe/task_app/hosted/envs/mini_swe/tools.py +96 -0
  53. examples/swe/task_app/hosted/hosted_app.py +204 -0
  54. examples/swe/task_app/hosted/inference/__init__.py +5 -0
  55. examples/swe/task_app/hosted/inference/openai_client.py +618 -0
  56. examples/swe/task_app/hosted/main.py +100 -0
  57. examples/swe/task_app/hosted/policy_routes.py +1079 -0
  58. examples/swe/task_app/hosted/registry.py +195 -0
  59. examples/swe/task_app/hosted/rollout.py +1869 -0
  60. examples/swe/task_app/hosted/storage/__init__.py +5 -0
  61. examples/swe/task_app/hosted/storage/volume.py +211 -0
  62. examples/swe/task_app/hosted/test_agents.py +161 -0
  63. examples/swe/task_app/hosted/test_service.py +137 -0
  64. examples/swe/task_app/hosted/utils.py +62 -0
  65. examples/vlm/README.md +68 -0
  66. examples/vlm/configs/crafter_vlm_gpt4o.toml +44 -0
  67. examples/vlm/crafter_image_only_agent.py +207 -0
  68. examples/vlm/crafter_openai_vlm_agent.py +277 -0
  69. examples/vlm/filter_image_rows.py +63 -0
  70. examples/vlm/run_crafter_vlm_benchmark.py +316 -0
  71. examples/warming_up_to_rl/analyze_trace_db.py +12 -10
  72. examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +11 -1
  73. examples/warming_up_to_rl/export_trace_sft.py +218 -36
  74. examples/warming_up_to_rl/groq_test.py +15 -8
  75. examples/warming_up_to_rl/manage_secrets.py +29 -25
  76. examples/warming_up_to_rl/readme.md +9 -2
  77. examples/warming_up_to_rl/run_eval.py +137 -61
  78. examples/warming_up_to_rl/run_fft_and_save.py +131 -60
  79. examples/warming_up_to_rl/run_local_rollout.py +88 -39
  80. examples/warming_up_to_rl/run_local_rollout_modal.py +114 -28
  81. examples/warming_up_to_rl/run_local_rollout_parallel.py +81 -20
  82. examples/warming_up_to_rl/run_local_rollout_traced.py +126 -23
  83. examples/warming_up_to_rl/run_rl_and_save.py +35 -12
  84. examples/warming_up_to_rl/run_rollout_remote.py +44 -19
  85. examples/warming_up_to_rl/task_app/README.md +6 -2
  86. examples/warming_up_to_rl/task_app/grpo_crafter.py +319 -57
  87. examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +11 -30
  88. examples/warming_up_to_rl/task_app/synth_envs_hosted/__init__.py +1 -1
  89. examples/warming_up_to_rl/task_app/synth_envs_hosted/branching.py +9 -11
  90. examples/warming_up_to_rl/task_app/synth_envs_hosted/environment_routes.py +137 -182
  91. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/__init__.py +1 -1
  92. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/__init__.py +1 -1
  93. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/app.py +1 -1
  94. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/environment.py +150 -57
  95. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +105 -69
  96. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +19 -7
  97. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/shared.py +45 -42
  98. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/tools.py +1 -1
  99. examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +47 -45
  100. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/__init__.py +1 -1
  101. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +198 -92
  102. examples/warming_up_to_rl/task_app/synth_envs_hosted/main.py +0 -2
  103. examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +361 -263
  104. examples/warming_up_to_rl/task_app/synth_envs_hosted/registry.py +21 -23
  105. examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +394 -274
  106. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/__init__.py +1 -1
  107. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/volume.py +56 -62
  108. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_agents.py +1 -0
  109. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_service.py +6 -15
  110. examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +4 -3
  111. synth/__init__.py +14 -0
  112. synth_ai/__init__.py +20 -4
  113. synth_ai/api/models/supported.py +376 -0
  114. synth_ai/api/train/builders.py +157 -26
  115. synth_ai/api/train/cli.py +213 -57
  116. synth_ai/api/train/config_finder.py +65 -5
  117. synth_ai/api/train/env_resolver.py +33 -15
  118. synth_ai/api/train/pollers.py +13 -4
  119. synth_ai/api/train/supported_algos.py +139 -0
  120. synth_ai/api/train/task_app.py +5 -3
  121. synth_ai/api/train/utils.py +33 -48
  122. synth_ai/cli/__init__.py +19 -4
  123. synth_ai/cli/_modal_wrapper.py +28 -0
  124. synth_ai/cli/_typer_patch.py +49 -0
  125. synth_ai/cli/balance.py +2 -3
  126. synth_ai/cli/calc.py +1 -1
  127. synth_ai/cli/demo.py +21 -6
  128. synth_ai/cli/recent.py +2 -2
  129. synth_ai/cli/rl_demo.py +77 -17
  130. synth_ai/cli/root.py +116 -39
  131. synth_ai/cli/status.py +2 -2
  132. synth_ai/cli/task_apps.py +1709 -243
  133. synth_ai/cli/traces.py +7 -4
  134. synth_ai/cli/turso.py +73 -0
  135. synth_ai/cli/watch.py +12 -18
  136. synth_ai/core/experiment.py +0 -2
  137. synth_ai/demo_registry.py +68 -31
  138. synth_ai/demos/core/cli.py +516 -194
  139. synth_ai/demos/demo_task_apps/__init__.py +3 -3
  140. synth_ai/demos/demo_task_apps/core.py +64 -28
  141. synth_ai/demos/demo_task_apps/crafter/configs/crafter_fft_4b.toml +2 -3
  142. synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +37 -30
  143. synth_ai/demos/demo_task_apps/math/_common.py +1 -2
  144. synth_ai/demos/demo_task_apps/math/app.py +2 -1
  145. synth_ai/demos/demo_task_apps/math/deploy_modal.py +3 -6
  146. synth_ai/demos/demo_task_apps/math/modal_task_app.py +183 -82
  147. synth_ai/demos/demo_task_apps/math/task_app_entry.py +0 -2
  148. synth_ai/environments/examples/bandit/engine.py +12 -4
  149. synth_ai/environments/examples/bandit/taskset.py +4 -4
  150. synth_ai/environments/examples/crafter_classic/environment.py +76 -1
  151. synth_ai/environments/reproducibility/tree.py +5 -6
  152. synth_ai/environments/service/app.py +11 -12
  153. synth_ai/environments/service/core_routes.py +10 -9
  154. synth_ai/environments/stateful/engine.py +1 -1
  155. synth_ai/environments/tasks/core.py +1 -0
  156. synth_ai/environments/tasks/filters.py +5 -6
  157. synth_ai/environments/tasks/utils.py +4 -5
  158. synth_ai/evals/base.py +0 -2
  159. synth_ai/handshake.py +11 -9
  160. synth_ai/http.py +1 -1
  161. synth_ai/http_client.py +43 -11
  162. synth_ai/inference/__init__.py +0 -2
  163. synth_ai/inference/client.py +20 -6
  164. synth_ai/jobs/client.py +103 -78
  165. synth_ai/learning/__init__.py +41 -6
  166. synth_ai/learning/algorithms.py +14 -0
  167. synth_ai/learning/client.py +121 -29
  168. synth_ai/learning/config.py +2 -40
  169. synth_ai/learning/constants.py +0 -2
  170. synth_ai/learning/ft_client.py +4 -56
  171. synth_ai/learning/health.py +13 -7
  172. synth_ai/learning/jobs.py +43 -47
  173. synth_ai/{rl → learning/rl}/__init__.py +14 -5
  174. synth_ai/learning/rl/client.py +267 -0
  175. synth_ai/learning/rl/config.py +31 -0
  176. synth_ai/{rl → learning/rl}/contracts.py +5 -10
  177. synth_ai/{rl → learning/rl}/env_keys.py +45 -16
  178. synth_ai/learning/rl/secrets.py +13 -0
  179. synth_ai/learning/rl_client.py +2 -253
  180. synth_ai/learning/sft/__init__.py +29 -0
  181. synth_ai/learning/sft/client.py +68 -0
  182. synth_ai/learning/sft/config.py +270 -0
  183. synth_ai/learning/sft/data.py +295 -0
  184. synth_ai/learning/sse.py +25 -26
  185. synth_ai/learning/validators.py +25 -24
  186. synth_ai/lm/__init__.py +21 -47
  187. synth_ai/task/__init__.py +26 -27
  188. synth_ai/task/apps/__init__.py +18 -19
  189. synth_ai/task/auth.py +35 -23
  190. synth_ai/task/client.py +15 -13
  191. synth_ai/task/contracts.py +37 -35
  192. synth_ai/task/datasets.py +9 -6
  193. synth_ai/task/errors.py +11 -10
  194. synth_ai/task/health.py +17 -11
  195. synth_ai/task/json.py +58 -24
  196. synth_ai/task/proxy.py +15 -14
  197. synth_ai/task/rubrics.py +22 -15
  198. synth_ai/task/server.py +43 -17
  199. synth_ai/task/tracing_utils.py +12 -7
  200. synth_ai/task/validators.py +0 -1
  201. synth_ai/task/vendors.py +5 -7
  202. synth_ai/tracing_v3/__init__.py +2 -0
  203. synth_ai/tracing_v3/abstractions.py +21 -4
  204. synth_ai/tracing_v3/db_config.py +26 -1
  205. synth_ai/tracing_v3/decorators.py +18 -15
  206. synth_ai/tracing_v3/examples/basic_usage.py +3 -2
  207. synth_ai/tracing_v3/hooks.py +6 -4
  208. synth_ai/tracing_v3/llm_call_record_helpers.py +6 -6
  209. synth_ai/tracing_v3/replica_sync.py +1 -0
  210. synth_ai/tracing_v3/session_tracer.py +63 -16
  211. synth_ai/tracing_v3/storage/base.py +89 -1
  212. synth_ai/tracing_v3/storage/config.py +21 -8
  213. synth_ai/tracing_v3/storage/factory.py +10 -8
  214. synth_ai/tracing_v3/storage/utils.py +4 -2
  215. synth_ai/tracing_v3/turso/daemon.py +7 -2
  216. synth_ai/tracing_v3/turso/models.py +5 -2
  217. synth_ai/tracing_v3/turso/native_manager.py +1173 -0
  218. synth_ai/tracing_v3/utils.py +4 -3
  219. synth_ai/v0/api/__init__.py +8 -0
  220. synth_ai/v0/api/models/__init__.py +8 -0
  221. synth_ai/v0/api/models/supported.py +8 -0
  222. synth_ai/v0/config/__init__.py +15 -0
  223. synth_ai/v0/config/base_url.py +12 -0
  224. synth_ai/v0/lm/__init__.py +51 -0
  225. synth_ai/{lm → v0/lm}/caching/ephemeral.py +3 -5
  226. synth_ai/{lm → v0/lm}/caching/handler.py +4 -4
  227. synth_ai/{lm → v0/lm}/caching/initialize.py +1 -1
  228. synth_ai/{lm → v0/lm}/caching/persistent.py +1 -1
  229. synth_ai/{lm → v0/lm}/config.py +6 -1
  230. synth_ai/{lm → v0/lm}/core/all.py +9 -9
  231. synth_ai/{lm → v0/lm}/core/exceptions.py +0 -2
  232. synth_ai/{lm → v0/lm}/core/main.py +19 -7
  233. synth_ai/{lm → v0/lm}/core/main_v3.py +10 -10
  234. synth_ai/{lm → v0/lm}/core/synth_models.py +2 -15
  235. synth_ai/{lm → v0/lm}/core/vendor_clients.py +6 -4
  236. synth_ai/{lm → v0/lm}/overrides.py +4 -4
  237. synth_ai/{lm → v0/lm}/provider_support/anthropic.py +4 -4
  238. synth_ai/{lm → v0/lm}/provider_support/openai.py +5 -5
  239. synth_ai/{lm → v0/lm}/structured_outputs/handler.py +5 -5
  240. synth_ai/{lm → v0/lm}/structured_outputs/rehabilitate.py +1 -1
  241. synth_ai/{lm → v0/lm}/vendors/core/anthropic_api.py +16 -16
  242. synth_ai/{lm → v0/lm}/vendors/core/gemini_api.py +5 -5
  243. synth_ai/{lm → v0/lm}/vendors/core/mistral_api.py +5 -5
  244. synth_ai/{lm → v0/lm}/vendors/core/openai_api.py +12 -10
  245. synth_ai/{lm → v0/lm}/vendors/openai_standard.py +11 -9
  246. synth_ai/{lm → v0/lm}/vendors/openai_standard_responses.py +8 -5
  247. synth_ai/{lm → v0/lm}/vendors/supported/custom_endpoint.py +4 -6
  248. synth_ai/{lm → v0/lm}/vendors/supported/deepseek.py +2 -2
  249. synth_ai/{lm → v0/lm}/vendors/supported/grok.py +2 -2
  250. synth_ai/{lm → v0/lm}/vendors/supported/groq.py +1 -1
  251. synth_ai/{lm → v0/lm}/vendors/supported/ollama.py +1 -1
  252. synth_ai/{lm → v0/lm}/vendors/supported/openrouter.py +3 -3
  253. synth_ai/{lm → v0/lm}/vendors/supported/together.py +1 -1
  254. synth_ai/{lm → v0/lm}/vendors/synth_client.py +38 -11
  255. synth_ai/v0/tracing/upload.py +32 -135
  256. synth_ai/v0/tracing_v3/__init__.py +10 -0
  257. synth_ai/v0/tracing_v3/abstractions.py +3 -0
  258. synth_ai/v0/tracing_v3/decorators.py +3 -0
  259. synth_ai/v0/tracing_v3/llm_call_record_helpers.py +3 -0
  260. synth_ai/v0/tracing_v3/session_tracer.py +3 -0
  261. synth_ai-0.2.9.dev6.dist-info/METADATA +191 -0
  262. {synth_ai-0.2.9.dev4.dist-info → synth_ai-0.2.9.dev6.dist-info}/RECORD +291 -264
  263. {synth_ai-0.2.9.dev4.dist-info → synth_ai-0.2.9.dev6.dist-info}/top_level.txt +1 -0
  264. examples/common_old/backend.py +0 -21
  265. examples/evals_old/README.md +0 -98
  266. examples/evals_old/__init__.py +0 -6
  267. examples/evals_old/compare_models.py +0 -1037
  268. examples/evals_old/example_log.md +0 -145
  269. examples/evals_old/run_demo.sh +0 -126
  270. examples/evals_old/trace_analysis.py +0 -270
  271. examples/finetuning_old/_backup_synth_qwen/config.toml +0 -29
  272. examples/finetuning_old/_backup_synth_qwen/example_log.md +0 -324
  273. examples/finetuning_old/_backup_synth_qwen/filter_traces.py +0 -60
  274. examples/finetuning_old/_backup_synth_qwen/filter_traces_achievements.py +0 -239
  275. examples/finetuning_old/_backup_synth_qwen/purge_v3_traces.py +0 -109
  276. examples/finetuning_old/_backup_synth_qwen/react_agent_lm.py +0 -1924
  277. examples/finetuning_old/_backup_synth_qwen/readme.md +0 -49
  278. examples/finetuning_old/_backup_synth_qwen/run_crafter_qwen4b.py +0 -114
  279. examples/finetuning_old/_backup_synth_qwen/run_demo.sh +0 -195
  280. examples/finetuning_old/_backup_synth_qwen/sft_kickoff.py +0 -118
  281. examples/finetuning_old/synth_qwen_v1/README.md +0 -68
  282. examples/finetuning_old/synth_qwen_v1/filter_traces.py +0 -60
  283. examples/finetuning_old/synth_qwen_v1/filter_traces_achievements.py +0 -239
  284. examples/finetuning_old/synth_qwen_v1/finetune.py +0 -46
  285. examples/finetuning_old/synth_qwen_v1/hello_ft_model.py +0 -71
  286. examples/finetuning_old/synth_qwen_v1/infer.py +0 -37
  287. examples/finetuning_old/synth_qwen_v1/poll.py +0 -44
  288. examples/finetuning_old/synth_qwen_v1/prepare_data.py +0 -35
  289. examples/finetuning_old/synth_qwen_v1/purge_v3_traces.py +0 -109
  290. examples/finetuning_old/synth_qwen_v1/react_agent_lm.py +0 -1932
  291. examples/finetuning_old/synth_qwen_v1/run_crafter_sft_job.py +0 -207
  292. examples/finetuning_old/synth_qwen_v1/run_ft_job.py +0 -232
  293. examples/finetuning_old/synth_qwen_v1/upload_data.py +0 -34
  294. examples/finetuning_old/synth_qwen_v1/util.py +0 -147
  295. examples/rl_old/task_app.py +0 -962
  296. examples/warming_up_to_rl/old/event_rewards.md +0 -234
  297. examples/warming_up_to_rl/old/notes.md +0 -73
  298. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_stepwise_rewards.py +0 -58
  299. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/filter_traces_sft_turso.py +0 -738
  300. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/filter_traces_sft_turso.py +0 -580
  301. synth_ai/environments/examples/sokoban/units/astar_common.py +0 -95
  302. synth_ai/experimental/synth_oss.py +0 -446
  303. synth_ai/install_sqld.sh +0 -40
  304. synth_ai/learning/filtering.py +0 -0
  305. synth_ai/learning/offline/dpo.py +0 -0
  306. synth_ai/learning/offline/providers.py +0 -7
  307. synth_ai/learning/offline/sft.py +0 -0
  308. synth_ai/learning/offline/shared.py +0 -0
  309. synth_ai/learning/online/grpo.py +0 -0
  310. synth_ai/learning/online/irft.py +0 -0
  311. synth_ai/learning/prompts/banking77_injection_eval.py +0 -168
  312. synth_ai/learning/prompts/gepa.py +0 -0
  313. synth_ai/learning/prompts/hello_world_in_context_injection_ex.py +0 -213
  314. synth_ai/learning/prompts/mipro.py +0 -289
  315. synth_ai/learning/prompts/random_search.py +0 -246
  316. synth_ai/learning/prompts/run_mipro_banking77.py +0 -172
  317. synth_ai/learning/prompts/run_random_search_banking77.py +0 -324
  318. synth_ai/rl/secrets.py +0 -19
  319. synth_ai/scripts/verify_rewards.py +0 -100
  320. synth_ai/tracing/__init__.py +0 -30
  321. synth_ai/tracing_v1/__init__.py +0 -33
  322. synth_ai/tracing_v3/turso/__init__.py +0 -25
  323. synth_ai/tracing_v3/turso/manager.py +0 -774
  324. synth_ai/zyk/__init__.py +0 -30
  325. synth_ai-0.2.9.dev4.dist-info/METADATA +0 -131
  326. /synth_ai/{lm → v0/lm}/caching/__init__.py +0 -0
  327. /synth_ai/{lm → v0/lm}/caching/constants.py +0 -0
  328. /synth_ai/{lm → v0/lm}/caching/dbs.py +0 -0
  329. /synth_ai/{lm → v0/lm}/constants.py +0 -0
  330. /synth_ai/{lm → v0/lm}/core/__init__.py +0 -0
  331. /synth_ai/{lm → v0/lm}/cost/__init__.py +0 -0
  332. /synth_ai/{lm → v0/lm}/cost/monitor.py +0 -0
  333. /synth_ai/{lm → v0/lm}/cost/statefulness.py +0 -0
  334. /synth_ai/{lm → v0/lm}/injection.py +0 -0
  335. /synth_ai/{lm → v0/lm}/provider_support/__init__.py +0 -0
  336. /synth_ai/{lm → v0/lm}/provider_support/suppress_logging.py +0 -0
  337. /synth_ai/{lm → v0/lm}/structured_outputs/__init__.py +0 -0
  338. /synth_ai/{lm → v0/lm}/structured_outputs/inject.py +0 -0
  339. /synth_ai/{lm → v0/lm}/tools/__init__.py +0 -0
  340. /synth_ai/{lm → v0/lm}/tools/base.py +0 -0
  341. /synth_ai/{lm → v0/lm}/unified_interface.py +0 -0
  342. /synth_ai/{lm → v0/lm}/vendors/__init__.py +0 -0
  343. /synth_ai/{lm → v0/lm}/vendors/base.py +0 -0
  344. /synth_ai/{lm → v0/lm}/vendors/core/__init__.py +0 -0
  345. /synth_ai/{lm → v0/lm}/vendors/core/synth_dev_api.py +0 -0
  346. /synth_ai/{lm → v0/lm}/vendors/local/__init__.py +0 -0
  347. /synth_ai/{lm → v0/lm}/vendors/local/ollama.py +0 -0
  348. /synth_ai/{lm → v0/lm}/vendors/retries.py +0 -0
  349. /synth_ai/{lm → v0/lm}/vendors/supported/__init__.py +0 -0
  350. /synth_ai/{lm → v0/lm}/warmup.py +0 -0
  351. {synth_ai-0.2.9.dev4.dist-info → synth_ai-0.2.9.dev6.dist-info}/WHEEL +0 -0
  352. {synth_ai-0.2.9.dev4.dist-info → synth_ai-0.2.9.dev6.dist-info}/entry_points.txt +0 -0
  353. {synth_ai-0.2.9.dev4.dist-info → synth_ai-0.2.9.dev6.dist-info}/licenses/LICENSE +0 -0
synth_ai/task/errors.py CHANGED
@@ -1,14 +1,16 @@
1
- from __future__ import annotations
2
-
3
1
  """Error helpers used across Task App implementations."""
4
2
 
5
- from typing import Any, Dict, Optional
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
6
 
7
7
  from .json import to_jsonable
8
8
 
9
9
 
10
- def error_payload(code: str, message: str, *, extra: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
11
- payload: Dict[str, Any] = {"error": {"code": code, "message": message}}
10
+ def error_payload(
11
+ code: str, message: str, *, extra: dict[str, Any] | None = None
12
+ ) -> dict[str, Any]:
13
+ payload: dict[str, Any] = {"error": {"code": code, "message": message}}
12
14
  if extra:
13
15
  payload["error"].update(extra)
14
16
  return payload
@@ -19,8 +21,8 @@ def http_exception(
19
21
  code: str,
20
22
  message: str,
21
23
  *,
22
- extra: Optional[Dict[str, Any]] = None,
23
- headers: Optional[Dict[str, str]] = None,
24
+ extra: dict[str, Any] | None = None,
25
+ headers: dict[str, str] | None = None,
24
26
  ):
25
27
  try:
26
28
  from fastapi import HTTPException # type: ignore
@@ -36,8 +38,8 @@ def json_error_response(
36
38
  code: str,
37
39
  message: str,
38
40
  *,
39
- extra: Optional[Dict[str, Any]] = None,
40
- headers: Optional[Dict[str, str]] = None,
41
+ extra: dict[str, Any] | None = None,
42
+ headers: dict[str, str] | None = None,
41
43
  ):
42
44
  try:
43
45
  from fastapi.responses import JSONResponse # type: ignore
@@ -46,4 +48,3 @@ def json_error_response(
46
48
 
47
49
  payload = error_payload(code, message, extra=extra)
48
50
  return JSONResponse(status_code=status_code, content=to_jsonable(payload), headers=headers)
49
-
synth_ai/task/health.py CHANGED
@@ -1,10 +1,13 @@
1
+ """Helpers for probing Task App health endpoints."""
2
+
1
3
  from __future__ import annotations
2
4
 
3
- from typing import Any, Dict
5
+ from typing import Any
6
+
4
7
  import aiohttp
5
8
 
6
9
 
7
- async def task_app_health(task_app_url: str) -> Dict[str, Any]:
10
+ async def task_app_health(task_app_url: str) -> dict[str, Any]:
8
11
  """Probe a Task App base URL for basic reachability.
9
12
 
10
13
  Behavior:
@@ -12,17 +15,20 @@ async def task_app_health(task_app_url: str) -> Dict[str, Any]:
12
15
  - Fallback to GET if HEAD is unsupported
13
16
  - Returns {ok: bool, status?: int, error?: str}
14
17
  """
18
+
19
+ async def _try_request(session: aiohttp.ClientSession, method: str) -> dict[str, Any] | None:
20
+ request = getattr(session, method)
21
+ async with request(task_app_url, allow_redirects=True) as response:
22
+ if 200 <= response.status < 400:
23
+ return {"ok": True, "status": response.status}
24
+ return None
25
+
15
26
  try:
16
27
  async with aiohttp.ClientSession() as session:
17
- async with session.head(task_app_url, allow_redirects=True) as r:
18
- if 200 <= r.status < 400:
19
- return {"ok": True, "status": r.status}
20
- async with aiohttp.ClientSession() as session:
21
- async with session.get(task_app_url, allow_redirects=True) as r2:
22
- if 200 <= r2.status < 400:
23
- return {"ok": True, "status": r2.status}
28
+ for method in ("head", "get"):
29
+ result = await _try_request(session, method)
30
+ if result is not None:
31
+ return result
24
32
  return {"ok": False, "status": None}
25
33
  except Exception as e:
26
34
  return {"ok": False, "error": f"{type(e).__name__}: {e}"}
27
-
28
-
synth_ai/task/json.py CHANGED
@@ -1,9 +1,9 @@
1
- from __future__ import annotations
2
-
3
1
  """Shared JSON sanitisation helpers for Task Apps."""
4
2
 
3
+ from __future__ import annotations
4
+
5
5
  from collections.abc import Mapping, Sequence
6
- from dataclasses import is_dataclass, asdict
6
+ from dataclasses import asdict, is_dataclass
7
7
  from enum import Enum
8
8
  from typing import Any
9
9
 
@@ -13,13 +13,19 @@ except Exception: # pragma: no cover - handled at runtime
13
13
  _np = None # type: ignore
14
14
 
15
15
 
16
- def _mask_numpy_array(arr: "_np.ndarray") -> str:
16
+ def _mask_numpy_array(arr: Any) -> str:
17
17
  shape = getattr(arr, "shape", None)
18
18
  dtype = getattr(arr, "dtype", None)
19
19
  return f"<ndarray shape={shape} dtype={dtype}>"
20
20
 
21
21
 
22
- def to_jsonable(value: Any) -> Any:
22
+ def to_jsonable(
23
+ value: Any,
24
+ *,
25
+ _visited: set[int] | None = None,
26
+ _depth: int = 0,
27
+ _max_depth: int = 32,
28
+ ) -> Any:
23
29
  """Convert `value` into structures compatible with JSON serialisation.
24
30
 
25
31
  - numpy scalars are converted to their Python counterparts
@@ -29,25 +35,33 @@ def to_jsonable(value: Any) -> Any:
29
35
  - non-serialisable objects fall back to `repr`
30
36
  """
31
37
 
32
- if value is None or isinstance(value, (str, bool, int, float)):
38
+ if _visited is None:
39
+ _visited = set()
40
+
41
+ if _depth > _max_depth:
42
+ return f"<max_depth type={type(value).__name__}>"
43
+
44
+ if value is None or isinstance(value, str | bool | int | float):
33
45
  return value
34
46
 
35
47
  # numpy scalars / arrays
36
48
  if _np is not None:
37
- if isinstance(value, (_np.integer,)):
49
+ if isinstance(value, _np.integer):
38
50
  return int(value)
39
- if isinstance(value, (_np.floating,)):
51
+ if isinstance(value, _np.floating):
40
52
  return float(value)
41
- if isinstance(value, (_np.bool_,)):
53
+ if isinstance(value, _np.bool_):
42
54
  return bool(value)
43
- if isinstance(value, (_np.ndarray,)):
55
+ if isinstance(value, _np.ndarray):
44
56
  return _mask_numpy_array(value)
45
57
 
46
58
  if isinstance(value, Enum):
47
- return to_jsonable(value.value)
59
+ return to_jsonable(value.value, _visited=_visited, _depth=_depth + 1, _max_depth=_max_depth)
48
60
 
49
61
  if is_dataclass(value):
50
- return to_jsonable(asdict(value))
62
+ return to_jsonable(
63
+ asdict(value), _visited=_visited, _depth=_depth + 1, _max_depth=_max_depth
64
+ )
51
65
 
52
66
  # pydantic BaseModel / attrs objects
53
67
  for attr in ("model_dump", "dict", "to_dict", "to_json"):
@@ -56,22 +70,42 @@ def to_jsonable(value: Any) -> Any:
56
70
  dumped = getattr(value, attr)() # type: ignore[misc]
57
71
  except TypeError:
58
72
  dumped = getattr(value, attr)(exclude_none=False) # pragma: no cover
59
- return to_jsonable(dumped)
73
+ return to_jsonable(
74
+ dumped, _visited=_visited, _depth=_depth + 1, _max_depth=_max_depth
75
+ )
60
76
 
61
- if isinstance(value, Mapping):
62
- return {str(k): to_jsonable(v) for k, v in value.items()}
63
-
64
- if isinstance(value, (set, tuple)):
65
- return [to_jsonable(v) for v in value]
66
-
67
- if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
68
- return [to_jsonable(v) for v in value]
77
+ obj_id = id(value)
78
+ if obj_id in _visited:
79
+ return f"<circular type={type(value).__name__}>"
69
80
 
70
- if isinstance(value, (bytes, bytearray)):
81
+ if isinstance(value, Mapping):
82
+ _visited.add(obj_id)
83
+ return {
84
+ str(k): to_jsonable(v, _visited=_visited, _depth=_depth + 1, _max_depth=_max_depth)
85
+ for k, v in value.items()
86
+ }
87
+
88
+ if isinstance(value, set | tuple):
89
+ _visited.add(obj_id)
90
+ return [
91
+ to_jsonable(v, _visited=_visited, _depth=_depth + 1, _max_depth=_max_depth)
92
+ for v in value
93
+ ]
94
+
95
+ if isinstance(value, Sequence) and not isinstance(value, str | bytes | bytearray):
96
+ _visited.add(obj_id)
97
+ return [
98
+ to_jsonable(v, _visited=_visited, _depth=_depth + 1, _max_depth=_max_depth)
99
+ for v in value
100
+ ]
101
+
102
+ if isinstance(value, bytes | bytearray):
71
103
  return f"<bytes len={len(value)}>"
72
104
 
73
105
  if hasattr(value, "__dict__"):
74
- return to_jsonable(vars(value))
106
+ _visited.add(obj_id)
107
+ return to_jsonable(
108
+ vars(value), _visited=_visited, _depth=_depth + 1, _max_depth=_max_depth
109
+ )
75
110
 
76
111
  return repr(value)
77
-
synth_ai/task/proxy.py CHANGED
@@ -1,14 +1,14 @@
1
- from __future__ import annotations
2
-
3
1
  """Shared helpers for Task App proxy endpoints (OpenAI, Groq, etc.)."""
4
2
 
3
+ from __future__ import annotations
4
+
5
5
  import copy
6
6
  import json
7
7
  import re
8
- from typing import Any, Iterable, List, Tuple
9
-
8
+ from collections.abc import Iterable
9
+ from typing import Any
10
10
 
11
- INTERACT_TOOL_SCHEMA: List[dict[str, Any]] = [
11
+ INTERACT_TOOL_SCHEMA: list[dict[str, Any]] = [
12
12
  {
13
13
  "type": "function",
14
14
  "function": {
@@ -80,9 +80,13 @@ def prepare_for_groq(model: str | None, payload: dict[str, Any]) -> dict[str, An
80
80
 
81
81
  sanitized = prepare_for_openai(model, payload)
82
82
  # Groq supports `max_tokens`; prefer their native parameter when present
83
- if model and "gpt-5" not in (model or ""):
84
- if "max_completion_tokens" in sanitized and "max_tokens" not in payload:
85
- sanitized["max_tokens"] = sanitized.pop("max_completion_tokens")
83
+ if (
84
+ model
85
+ and "gpt-5" not in model
86
+ and "max_completion_tokens" in sanitized
87
+ and "max_tokens" not in payload
88
+ ):
89
+ sanitized["max_tokens"] = sanitized.pop("max_completion_tokens")
86
90
  return sanitized
87
91
 
88
92
 
@@ -146,7 +150,7 @@ def _parse_actions_from_json_candidate(candidate: Any) -> tuple[list[str], str]:
146
150
  return actions, reasoning
147
151
 
148
152
 
149
- def parse_tool_call_from_text(text: str) -> Tuple[list[str], str]:
153
+ def parse_tool_call_from_text(text: str) -> tuple[list[str], str]:
150
154
  """Derive tool-call actions and reasoning from assistant text."""
151
155
 
152
156
  text = (text or "").strip()
@@ -179,7 +183,7 @@ def parse_tool_call_from_text(text: str) -> Tuple[list[str], str]:
179
183
  if m:
180
184
  items = [part.strip() for part in m.group(1).split(",") if part.strip()]
181
185
  if items:
182
- reasoning = text[:m.start()].strip()
186
+ reasoning = text[: m.start()].strip()
183
187
  return items, reasoning
184
188
 
185
189
  # Patterns like "Action 1: move_right"
@@ -242,9 +246,7 @@ def synthesize_tool_call_if_missing(openai_response: dict[str, Any]) -> dict[str
242
246
  return openai_response
243
247
 
244
248
  new_message = copy.deepcopy(message)
245
- new_message["tool_calls"] = [
246
- _build_tool_call(actions, reasoning)
247
- ]
249
+ new_message["tool_calls"] = [_build_tool_call(actions, reasoning)]
248
250
  if "content" not in new_message:
249
251
  new_message["content"] = None
250
252
 
@@ -255,4 +257,3 @@ def synthesize_tool_call_if_missing(openai_response: dict[str, Any]) -> dict[str
255
257
  result = copy.deepcopy(openai_response)
256
258
  result["choices"] = new_choices
257
259
  return result
258
-
synth_ai/task/rubrics.py CHANGED
@@ -1,10 +1,11 @@
1
- from __future__ import annotations
2
-
3
1
  """Rubric schema, loading, and scoring helpers for Task Apps."""
4
2
 
3
+ from __future__ import annotations
4
+
5
5
  import json
6
+ from collections.abc import Iterable
6
7
  from pathlib import Path
7
- from typing import Any, Dict, Iterable, Optional
8
+ from typing import Any
8
9
 
9
10
  from pydantic import BaseModel, Field, field_validator
10
11
 
@@ -48,14 +49,14 @@ class Rubric(BaseModel):
48
49
  return criteria
49
50
 
50
51
 
51
- def _load_text(source: str) -> tuple[str, Optional[str]]:
52
+ def _load_text(source: str) -> tuple[str, str | None]:
52
53
  path = Path(source)
53
54
  if path.exists():
54
55
  return path.read_text(encoding="utf-8"), path.suffix.lower()
55
56
  return source, None
56
57
 
57
58
 
58
- def _parse_structured(text: str, suffix: Optional[str]) -> Dict[str, Any]:
59
+ def _parse_structured(text: str, suffix: str | None) -> dict[str, Any]:
59
60
  text = text.strip()
60
61
  if not text:
61
62
  raise ValueError("Rubric source is empty")
@@ -66,7 +67,7 @@ def _parse_structured(text: str, suffix: Optional[str]) -> Dict[str, Any]:
66
67
  raise RuntimeError("PyYAML is required to load YAML rubrics") from exc
67
68
  data = yaml.safe_load(text)
68
69
  if not isinstance(data, dict):
69
- raise ValueError("Rubric YAML must produce a mapping")
70
+ raise ValueError("Rubric YAML must produce a mapping") from None
70
71
  return data
71
72
  if text.startswith("{"):
72
73
  return json.loads(text)
@@ -85,7 +86,7 @@ def _parse_structured(text: str, suffix: Optional[str]) -> Dict[str, Any]:
85
86
  raise RuntimeError("PyYAML is required to load rubric text") from exc
86
87
  data = yaml.safe_load(text)
87
88
  if not isinstance(data, dict):
88
- raise ValueError("Rubric text must decode to a mapping")
89
+ raise ValueError("Rubric text must decode to a mapping") from None
89
90
  return data
90
91
 
91
92
 
@@ -148,17 +149,19 @@ def blend_rubrics(base: Rubric | None, override: Rubric | None) -> Rubric | None
148
149
  )
149
150
 
150
151
 
151
- def _as_float(value: Any) -> Optional[float]:
152
+ def _as_float(value: Any) -> float | None:
152
153
  try:
153
154
  return float(value)
154
155
  except Exception:
155
156
  return None
156
157
 
157
158
 
158
- def _score(criteria: Iterable[Criterion], values: Dict[str, float], aggregation: str) -> Dict[str, Any]:
159
+ def _score(
160
+ criteria: Iterable[Criterion], values: dict[str, float], aggregation: str
161
+ ) -> dict[str, Any]:
159
162
  if aggregation == "inherit":
160
163
  aggregation = "weighted_sum"
161
- per_criterion: Dict[str, Dict[str, Any]] = {}
164
+ per_criterion: dict[str, dict[str, Any]] = {}
162
165
  total = 0.0
163
166
  total_weight = 0.0
164
167
  for criterion in criteria:
@@ -184,10 +187,12 @@ def _score(criteria: Iterable[Criterion], values: Dict[str, float], aggregation:
184
187
  }
185
188
 
186
189
 
187
- def score_events_against_rubric(events: list[dict[str, Any]], rubric: Rubric | None) -> Dict[str, Any]:
190
+ def score_events_against_rubric(
191
+ events: list[dict[str, Any]], rubric: Rubric | None
192
+ ) -> dict[str, Any]:
188
193
  if rubric is None:
189
194
  return {"aggregation": "none", "score": None, "per_criterion": {}}
190
- values: Dict[str, float] = {}
195
+ values: dict[str, float] = {}
191
196
  for event in events or []:
192
197
  if not isinstance(event, dict):
193
198
  continue
@@ -198,12 +203,14 @@ def score_events_against_rubric(events: list[dict[str, Any]], rubric: Rubric | N
198
203
  return _score(rubric.criteria, values, rubric.aggregation)
199
204
 
200
205
 
201
- def score_outcome_against_rubric(outcome: dict[str, Any], rubric: Rubric | None) -> Dict[str, Any]:
206
+ def score_outcome_against_rubric(outcome: dict[str, Any], rubric: Rubric | None) -> dict[str, Any]:
202
207
  if rubric is None:
203
208
  return {"aggregation": "none", "score": None, "per_criterion": {}}
204
- values: Dict[str, float] = {}
209
+ values: dict[str, float] = {}
205
210
  if isinstance(outcome, dict):
206
- candidates = outcome.get("criteria") if isinstance(outcome.get("criteria"), dict) else outcome
211
+ candidates = (
212
+ outcome.get("criteria") if isinstance(outcome.get("criteria"), dict) else outcome
213
+ )
207
214
  if isinstance(candidates, dict):
208
215
  for key, value in candidates.items():
209
216
  score = _as_float(value)
synth_ai/task/server.py CHANGED
@@ -1,39 +1,34 @@
1
- from __future__ import annotations
2
-
3
1
  """FastAPI scaffolding for Task Apps (local dev + deployment)."""
4
2
 
3
+ from __future__ import annotations
4
+
5
5
  import asyncio
6
6
  import inspect
7
7
  import os
8
+ from collections.abc import Awaitable, Callable, Iterable, Mapping, MutableMapping, Sequence
8
9
  from dataclasses import dataclass, field
9
10
  from pathlib import Path
10
- from typing import Any, Awaitable, Callable, Iterable, Mapping, MutableMapping, Sequence
11
+ from typing import Any
11
12
 
12
13
  import httpx
13
14
  from fastapi import APIRouter, Depends, FastAPI, Query, Request
14
15
  from fastapi.middleware.cors import CORSMiddleware
15
- from fastapi.responses import JSONResponse
16
16
  from starlette.middleware import Middleware
17
17
 
18
- from .auth import (
19
- is_api_key_header_authorized,
20
- normalize_environment_api_key,
21
- require_api_key_dependency,
22
- )
18
+ from .auth import normalize_environment_api_key, require_api_key_dependency
23
19
  from .contracts import RolloutRequest, RolloutResponse, TaskInfo
24
20
  from .datasets import TaskDatasetRegistry
25
21
  from .errors import http_exception
26
22
  from .json import to_jsonable
27
23
  from .proxy import (
24
+ inject_system_hint,
28
25
  prepare_for_groq,
29
26
  prepare_for_openai,
30
- inject_system_hint,
31
27
  synthesize_tool_call_if_missing,
32
28
  )
33
29
  from .rubrics import Rubric
34
30
  from .vendors import get_groq_key_or_503, get_openai_key_or_503, normalize_vendor_keys
35
31
 
36
-
37
32
  TasksetDescriptor = Callable[[], Mapping[str, Any] | Awaitable[Mapping[str, Any]]]
38
33
  InstanceProvider = Callable[[Sequence[int]], Iterable[TaskInfo] | Awaitable[Iterable[TaskInfo]]]
39
34
  RolloutExecutor = Callable[[RolloutRequest, Request], Any | Awaitable[Any]]
@@ -81,7 +76,7 @@ class TaskAppConfig:
81
76
  startup_hooks: Sequence[Callable[[], None | Awaitable[None]]] = field(default_factory=tuple)
82
77
  shutdown_hooks: Sequence[Callable[[], None | Awaitable[None]]] = field(default_factory=tuple)
83
78
 
84
- def clone(self) -> "TaskAppConfig":
79
+ def clone(self) -> TaskAppConfig:
85
80
  """Return a shallow copy safe to mutate when wiring the app."""
86
81
 
87
82
  return TaskAppConfig(
@@ -120,7 +115,9 @@ def _ensure_task_info(obj: Any) -> TaskInfo:
120
115
  return obj
121
116
  if isinstance(obj, MutableMapping):
122
117
  return TaskInfo.model_validate(obj)
123
- raise TypeError(f"Task instance provider must yield TaskInfo-compatible objects (got {type(obj)!r})")
118
+ raise TypeError(
119
+ f"Task instance provider must yield TaskInfo-compatible objects (got {type(obj)!r})"
120
+ )
124
121
 
125
122
 
126
123
  def _normalise_seeds(values: Sequence[int]) -> list[int]:
@@ -140,7 +137,9 @@ def _build_proxy_routes(
140
137
  if not proxy:
141
138
  return
142
139
 
143
- async def _call_vendor(url: str, payload: dict[str, Any], headers: dict[str, str]) -> dict[str, Any]:
140
+ async def _call_vendor(
141
+ url: str, payload: dict[str, Any], headers: dict[str, str]
142
+ ) -> dict[str, Any]:
144
143
  async with httpx.AsyncClient(timeout=httpx.Timeout(600.0), follow_redirects=True) as client:
145
144
  response = await client.post(url, json=payload, headers=headers)
146
145
  data = (
@@ -168,13 +167,17 @@ def _build_proxy_routes(
168
167
  msg_count = len(messages) if isinstance(messages, list) else 0
169
168
  tool_count = len(payload.get("tools") or []) if isinstance(payload, dict) else 0
170
169
  model = payload.get("model") if isinstance(payload, dict) else None
171
- print(f"[task:proxy:{route}] model={model} messages={msg_count} tools={tool_count}", flush=True)
170
+ print(
171
+ f"[task:proxy:{route}] model={model} messages={msg_count} tools={tool_count}",
172
+ flush=True,
173
+ )
172
174
  except Exception: # pragma: no cover - best effort logging
173
175
  pass
174
176
 
175
177
  system_hint = proxy.system_hint
176
178
 
177
179
  if proxy.enable_openai:
180
+
178
181
  @app.post("/proxy/v1/chat/completions", dependencies=[Depends(auth_dependency)])
179
182
  async def proxy_openai(body: dict[str, Any], request: Request) -> Any: # type: ignore[no-redef]
180
183
  key = get_openai_key_or_503()
@@ -187,6 +190,7 @@ def _build_proxy_routes(
187
190
  return to_jsonable(sanitized)
188
191
 
189
192
  if proxy.enable_groq:
193
+
190
194
  @app.post("/proxy/groq/v1/chat/completions", dependencies=[Depends(auth_dependency)])
191
195
  async def proxy_groq(body: dict[str, Any], request: Request) -> Any: # type: ignore[no-redef]
192
196
  key = get_groq_key_or_503()
@@ -194,7 +198,9 @@ def _build_proxy_routes(
194
198
  payload = prepare_for_groq(model, body)
195
199
  payload = inject_system_hint(payload, system_hint or "")
196
200
  _log_proxy("groq", payload)
197
- data = await _call_vendor(proxy.groq_url.rstrip("/"), payload, {"Authorization": f"Bearer {key}"})
201
+ data = await _call_vendor(
202
+ proxy.groq_url.rstrip("/"), payload, {"Authorization": f"Bearer {key}"}
203
+ )
198
204
  sanitized = synthesize_tool_call_if_missing(data)
199
205
  return to_jsonable(sanitized)
200
206
 
@@ -278,7 +284,20 @@ def create_task_app(config: TaskAppConfig) -> FastAPI:
278
284
  async def health(request: Request) -> Mapping[str, Any]:
279
285
  # If we got here, auth_dependency already verified the key exactly matches
280
286
  expected = normalize_environment_api_key()
281
- return to_jsonable({"healthy": True, "auth": {"required": True, "expected_prefix": (expected[:6] + '...') if expected else '<unset>'}})
287
+ return to_jsonable(
288
+ {
289
+ "healthy": True,
290
+ "auth": {
291
+ "required": True,
292
+ "expected_prefix": (expected[:6] + "...") if expected else "<unset>",
293
+ },
294
+ }
295
+ )
296
+
297
+ @app.post("/done", dependencies=[Depends(auth_dependency)])
298
+ async def done() -> Mapping[str, Any]:
299
+ # Coordination endpoint for tests and automation; indicates app is reachable
300
+ return to_jsonable({"ok": True, "service": cfg.app_id})
282
301
 
283
302
  @app.get("/info", dependencies=[Depends(auth_dependency)])
284
303
  async def info() -> Mapping[str, Any]:
@@ -335,6 +354,7 @@ def create_task_app(config: TaskAppConfig) -> FastAPI:
335
354
  raise TypeError("Rollout executor must return RolloutResponse or mapping")
336
355
 
337
356
  if cfg.expose_debug_env:
357
+
338
358
  @app.get("/debug/env", dependencies=[Depends(auth_dependency)])
339
359
  async def debug_env() -> Mapping[str, Any]:
340
360
  def _mask(value: str | None) -> str:
@@ -387,6 +407,12 @@ def run_task_app(
387
407
  print(f"[task:server] Loaded environment from: {', '.join(loaded_files)}", flush=True)
388
408
 
389
409
  config = config_factory()
410
+ # Defensive: ensure the factory produced a valid TaskAppConfig to avoid
411
+ # confusing attribute errors later in the boot sequence.
412
+ if not isinstance(config, TaskAppConfig): # type: ignore[arg-type]
413
+ raise TypeError(
414
+ f"Task app config_factory must return TaskAppConfig, got {type(config).__name__}"
415
+ )
390
416
  app = create_task_app(config)
391
417
 
392
418
  try:
@@ -1,11 +1,11 @@
1
- from __future__ import annotations
2
-
3
1
  """Utilities for wiring tracing_v3 into task apps."""
4
2
 
3
+ from __future__ import annotations
4
+
5
5
  import os
6
- import time
6
+ from collections.abc import Callable
7
7
  from pathlib import Path
8
- from typing import Any, Callable
8
+ from typing import Any
9
9
 
10
10
 
11
11
  def tracing_env_enabled(default: bool = False) -> bool:
@@ -45,7 +45,9 @@ def resolve_tracing_db_url() -> str | None:
45
45
  return f"sqlite+aiosqlite:///{fallback_path}"
46
46
 
47
47
 
48
- def build_tracer_factory(make_tracer: Callable[..., Any], *, enabled: bool, db_url: str | None) -> Callable[[], Any] | None:
48
+ def build_tracer_factory(
49
+ make_tracer: Callable[..., Any], *, enabled: bool, db_url: str | None
50
+ ) -> Callable[[], Any] | None:
49
51
  """Return a factory that instantiates a tracer when enabled, else None."""
50
52
 
51
53
  if not enabled:
@@ -74,6 +76,9 @@ def resolve_sft_output_dir() -> str | None:
74
76
  def unique_sft_path(base_dir: str, *, run_id: str) -> Path:
75
77
  """Return a unique JSONL path for an SFT record batch."""
76
78
 
77
- ts = int(time.time() * 1000)
78
- name = f"{run_id}_{ts}.jsonl"
79
+ from datetime import datetime
80
+
81
+ now = datetime.now()
82
+ timestamp = now.strftime("%Y-%m-%d_%H-%M-%S")
83
+ name = f"{run_id}_{timestamp}.jsonl"
79
84
  return Path(base_dir) / name
@@ -9,4 +9,3 @@ def validate_task_app_url(url: str, *, name: str = "TASK_APP_BASE_URL") -> None:
9
9
  p = urlparse(url)
10
10
  if p.scheme not in ("http", "https") or not p.netloc:
11
11
  raise ValueError(f"Invalid {name}: malformed: {url}")
12
-
synth_ai/task/vendors.py CHANGED
@@ -1,9 +1,8 @@
1
- from __future__ import annotations
2
-
3
1
  """Vendor API key helpers shared by Task Apps."""
4
2
 
3
+ from __future__ import annotations
4
+
5
5
  import os
6
- from typing import Optional
7
6
 
8
7
  from .errors import http_exception
9
8
 
@@ -20,7 +19,7 @@ def _mask(value: str, *, prefix: int = 4) -> str:
20
19
  return f"{visible}{'…' if len(value) > prefix else ''}"
21
20
 
22
21
 
23
- def _normalize_single(key: str) -> Optional[str]:
22
+ def _normalize_single(key: str) -> str | None:
24
23
  direct = os.getenv(key)
25
24
  if direct:
26
25
  return direct
@@ -37,10 +36,10 @@ def _normalize_single(key: str) -> Optional[str]:
37
36
  return None
38
37
 
39
38
 
40
- def normalize_vendor_keys() -> dict[str, Optional[str]]:
39
+ def normalize_vendor_keys() -> dict[str, str | None]:
41
40
  """Normalise known vendor keys from dev fallbacks and return the mapping."""
42
41
 
43
- resolved: dict[str, Optional[str]] = {}
42
+ resolved: dict[str, str | None] = {}
44
43
  for key in _VENDOR_KEYS:
45
44
  resolved[key] = _normalize_single(key)
46
45
  return resolved
@@ -58,4 +57,3 @@ def get_groq_key_or_503() -> str:
58
57
  if not key:
59
58
  raise http_exception(503, "missing_groq_api_key", "GROQ_API_KEY is not configured")
60
59
  return key
61
-
@@ -75,6 +75,7 @@ from .abstractions import (
75
75
  EnvironmentEvent,
76
76
  RuntimeEvent,
77
77
  SessionEventMarkovBlanketMessage,
78
+ SessionMessageContent,
78
79
  SessionTimeStep,
79
80
  SessionTrace,
80
81
  TimeRecord,
@@ -90,6 +91,7 @@ __all__ = [
90
91
  "RuntimeEvent",
91
92
  "EnvironmentEvent",
92
93
  "SessionEventMarkovBlanketMessage",
94
+ "SessionMessageContent",
93
95
  "TimeRecord",
94
96
  "TursoConfig",
95
97
  ]