synth-ai 0.2.9.dev7__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.
- examples/__init__.py +16 -0
- examples/crafter_debug_render.py +8 -11
- examples/dev/qwen3_32b_qlora_4xh100.toml +40 -0
- examples/multi_step/crafter_rl_lora.md +29 -0
- examples/qwen_coder/README.md +102 -0
- examples/qwen_coder/_shared.py +113 -0
- examples/qwen_coder/configs/coder_lora_30b.toml +61 -0
- examples/qwen_coder/configs/coder_lora_4b.toml +57 -0
- examples/qwen_coder/configs/coder_lora_small.toml +58 -0
- examples/qwen_coder/generate_dataset.py +98 -0
- examples/qwen_coder/infer_ft_smoke.py +65 -0
- examples/qwen_coder/infer_prod_proxy.py +73 -0
- examples/qwen_coder/infer_via_synth.py +87 -0
- examples/qwen_coder/scripts/infer_coder.sh +19 -0
- examples/qwen_coder/scripts/train_coder_30b.sh +22 -0
- examples/qwen_coder/sft_full_17b.py +103 -0
- examples/qwen_coder/sft_lora_30b.py +110 -0
- examples/qwen_coder/subset_jsonl.py +39 -0
- examples/qwen_coder/todos.md +38 -0
- examples/qwen_coder/validate_jsonl.py +60 -0
- examples/rl/run_eval.py +36 -37
- examples/rl/run_rl_and_save.py +5 -5
- examples/rl/task_app/math_single_step.py +65 -43
- examples/rl/task_app/math_task_app.py +3 -3
- examples/sft/README.md +139 -0
- examples/sft/configs/crafter_fft_qwen0p6b.toml +44 -0
- examples/sft/configs/crafter_lora_qwen0p6b.toml +45 -0
- examples/sft/evaluate.py +117 -0
- examples/sft/export_dataset.py +117 -0
- examples/sft/generate_traces.py +162 -0
- examples/swe/__init__.py +12 -0
- examples/swe/task_app/README.md +105 -0
- examples/swe/task_app/__init__.py +2 -0
- examples/swe/task_app/grpo_swe_mini.py +571 -0
- examples/swe/task_app/grpo_swe_mini_task_app.py +136 -0
- examples/swe/task_app/hosted/README.md +173 -0
- examples/swe/task_app/hosted/__init__.py +5 -0
- examples/swe/task_app/hosted/branching.py +143 -0
- examples/swe/task_app/hosted/environment_routes.py +1289 -0
- examples/swe/task_app/hosted/envs/__init__.py +1 -0
- examples/swe/task_app/hosted/envs/crafter/__init__.py +6 -0
- examples/swe/task_app/hosted/envs/crafter/app.py +1 -0
- examples/swe/task_app/hosted/envs/crafter/environment.py +522 -0
- examples/swe/task_app/hosted/envs/crafter/policy.py +478 -0
- examples/swe/task_app/hosted/envs/crafter/react_agent.py +108 -0
- examples/swe/task_app/hosted/envs/crafter/shared.py +305 -0
- examples/swe/task_app/hosted/envs/crafter/tools.py +47 -0
- examples/swe/task_app/hosted/envs/mini_swe/__init__.py +8 -0
- examples/swe/task_app/hosted/envs/mini_swe/environment.py +1164 -0
- examples/swe/task_app/hosted/envs/mini_swe/policy.py +355 -0
- examples/swe/task_app/hosted/envs/mini_swe/shared.py +83 -0
- examples/swe/task_app/hosted/envs/mini_swe/tools.py +96 -0
- examples/swe/task_app/hosted/hosted_app.py +204 -0
- examples/swe/task_app/hosted/inference/__init__.py +5 -0
- examples/swe/task_app/hosted/inference/openai_client.py +618 -0
- examples/swe/task_app/hosted/main.py +100 -0
- examples/swe/task_app/hosted/policy_routes.py +1079 -0
- examples/swe/task_app/hosted/registry.py +195 -0
- examples/swe/task_app/hosted/rollout.py +1869 -0
- examples/swe/task_app/hosted/storage/__init__.py +5 -0
- examples/swe/task_app/hosted/storage/volume.py +211 -0
- examples/swe/task_app/hosted/test_agents.py +161 -0
- examples/swe/task_app/hosted/test_service.py +137 -0
- examples/swe/task_app/hosted/utils.py +62 -0
- examples/vlm/PROPOSAL.md +53 -0
- examples/vlm/README.md +68 -0
- examples/vlm/configs/crafter_vlm_gpt4o.toml +44 -0
- examples/vlm/crafter_image_only_agent.py +207 -0
- examples/vlm/crafter_openai_vlm_agent.py +277 -0
- examples/vlm/filter_image_rows.py +63 -0
- examples/vlm/run_crafter_vlm_benchmark.py +316 -0
- examples/warming_up_to_rl/analyze_trace_db.py +5 -5
- examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +11 -1
- examples/warming_up_to_rl/export_trace_sft.py +78 -21
- examples/warming_up_to_rl/groq_test.py +4 -4
- examples/warming_up_to_rl/manage_secrets.py +13 -18
- examples/warming_up_to_rl/run_eval.py +42 -44
- examples/warming_up_to_rl/run_fft_and_save.py +11 -16
- examples/warming_up_to_rl/run_local_rollout.py +1 -3
- examples/warming_up_to_rl/run_local_rollout_modal.py +2 -4
- examples/warming_up_to_rl/run_local_rollout_parallel.py +1 -4
- examples/warming_up_to_rl/run_local_rollout_traced.py +3 -5
- examples/warming_up_to_rl/run_rl_and_save.py +5 -6
- examples/warming_up_to_rl/run_rollout_remote.py +8 -10
- examples/warming_up_to_rl/task_app/README.md +6 -2
- examples/warming_up_to_rl/task_app/grpo_crafter.py +234 -35
- examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +2 -3
- examples/warming_up_to_rl/task_app/synth_envs_hosted/__init__.py +1 -1
- examples/warming_up_to_rl/task_app/synth_envs_hosted/branching.py +9 -11
- examples/warming_up_to_rl/task_app/synth_envs_hosted/environment_routes.py +131 -114
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/environment.py +101 -41
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +73 -51
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +14 -6
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/shared.py +16 -16
- examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +32 -34
- examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +94 -31
- examples/warming_up_to_rl/task_app/synth_envs_hosted/main.py +0 -2
- examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +303 -203
- examples/warming_up_to_rl/task_app/synth_envs_hosted/registry.py +21 -23
- examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +328 -225
- examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/volume.py +13 -13
- examples/warming_up_to_rl/task_app/synth_envs_hosted/test_agents.py +1 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/test_service.py +1 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +4 -3
- synth_ai/api/models/supported.py +376 -0
- synth_ai/api/train/builders.py +128 -21
- synth_ai/api/train/cli.py +80 -64
- synth_ai/api/train/config_finder.py +7 -2
- synth_ai/api/train/env_resolver.py +1 -1
- synth_ai/api/train/pollers.py +2 -1
- synth_ai/api/train/supported_algos.py +139 -0
- synth_ai/api/train/task_app.py +1 -2
- synth_ai/api/train/utils.py +13 -44
- synth_ai/cli/__init__.py +8 -0
- synth_ai/cli/_modal_wrapper.py +28 -0
- synth_ai/cli/_typer_patch.py +49 -0
- synth_ai/cli/balance.py +1 -2
- synth_ai/cli/calc.py +1 -1
- synth_ai/cli/demo.py +2 -1
- synth_ai/cli/recent.py +2 -2
- synth_ai/cli/rl_demo.py +2 -1
- synth_ai/cli/root.py +11 -13
- synth_ai/cli/status.py +2 -2
- synth_ai/cli/task_apps.py +529 -179
- synth_ai/cli/traces.py +6 -4
- synth_ai/cli/watch.py +12 -18
- synth_ai/demo_registry.py +1 -1
- synth_ai/demos/core/cli.py +36 -43
- synth_ai/demos/demo_task_apps/__init__.py +3 -3
- synth_ai/demos/demo_task_apps/core.py +17 -25
- synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +3 -4
- synth_ai/demos/demo_task_apps/math/app.py +2 -1
- synth_ai/demos/demo_task_apps/math/deploy_modal.py +3 -4
- synth_ai/demos/demo_task_apps/math/modal_task_app.py +16 -18
- synth_ai/demos/demo_task_apps/math/task_app_entry.py +0 -1
- synth_ai/environments/examples/crafter_classic/environment.py +76 -1
- synth_ai/environments/reproducibility/tree.py +2 -5
- synth_ai/environments/service/app.py +11 -12
- synth_ai/environments/service/core_routes.py +4 -7
- synth_ai/environments/stateful/engine.py +1 -1
- synth_ai/environments/tasks/core.py +1 -0
- synth_ai/environments/tasks/filters.py +5 -6
- synth_ai/environments/tasks/utils.py +4 -5
- synth_ai/handshake.py +9 -9
- synth_ai/http.py +1 -1
- synth_ai/http_client.py +18 -10
- synth_ai/inference/client.py +15 -5
- synth_ai/jobs/client.py +78 -83
- synth_ai/learning/__init__.py +41 -6
- synth_ai/learning/algorithms.py +14 -0
- synth_ai/learning/client.py +91 -24
- synth_ai/learning/config.py +2 -38
- synth_ai/learning/ft_client.py +4 -59
- synth_ai/learning/health.py +5 -6
- synth_ai/learning/jobs.py +31 -47
- synth_ai/{rl → learning/rl}/__init__.py +14 -4
- synth_ai/learning/rl/client.py +267 -0
- synth_ai/learning/rl/config.py +31 -0
- synth_ai/{rl → learning/rl}/contracts.py +5 -8
- synth_ai/{rl → learning/rl}/env_keys.py +39 -15
- synth_ai/learning/rl/secrets.py +13 -0
- synth_ai/learning/rl_client.py +2 -281
- synth_ai/learning/sft/__init__.py +29 -0
- synth_ai/learning/sft/client.py +68 -0
- synth_ai/learning/sft/config.py +270 -0
- synth_ai/learning/sft/data.py +295 -0
- synth_ai/learning/sse.py +25 -24
- synth_ai/learning/validators.py +25 -28
- synth_ai/lm/__init__.py +21 -47
- synth_ai/task/__init__.py +25 -27
- synth_ai/task/apps/__init__.py +7 -8
- synth_ai/task/auth.py +8 -8
- synth_ai/task/client.py +14 -14
- synth_ai/task/contracts.py +36 -35
- synth_ai/task/datasets.py +6 -5
- synth_ai/task/errors.py +10 -10
- synth_ai/task/health.py +17 -9
- synth_ai/task/json.py +58 -23
- synth_ai/task/proxy.py +13 -9
- synth_ai/task/rubrics.py +16 -15
- synth_ai/task/server.py +12 -12
- synth_ai/task/tracing_utils.py +4 -4
- synth_ai/task/vendors.py +5 -6
- synth_ai/tracing_v3/__init__.py +2 -0
- synth_ai/tracing_v3/abstractions.py +21 -4
- synth_ai/tracing_v3/decorators.py +18 -16
- synth_ai/tracing_v3/hooks.py +5 -5
- synth_ai/tracing_v3/llm_call_record_helpers.py +6 -6
- synth_ai/tracing_v3/session_tracer.py +40 -14
- synth_ai/tracing_v3/storage/base.py +85 -0
- synth_ai/tracing_v3/storage/config.py +21 -8
- synth_ai/tracing_v3/storage/factory.py +10 -7
- synth_ai/tracing_v3/storage/utils.py +4 -2
- synth_ai/tracing_v3/turso/daemon.py +7 -2
- synth_ai/tracing_v3/turso/models.py +2 -2
- synth_ai/tracing_v3/turso/native_manager.py +1173 -0
- synth_ai/tracing_v3/utils.py +4 -4
- synth_ai/v0/api/__init__.py +8 -0
- synth_ai/v0/api/models/__init__.py +8 -0
- synth_ai/v0/api/models/supported.py +8 -0
- synth_ai/v0/config/__init__.py +15 -0
- synth_ai/v0/config/base_url.py +12 -0
- synth_ai/v0/lm/__init__.py +51 -0
- synth_ai/{lm → v0/lm}/caching/ephemeral.py +2 -2
- synth_ai/{lm → v0/lm}/caching/handler.py +4 -4
- synth_ai/{lm → v0/lm}/caching/initialize.py +1 -1
- synth_ai/{lm → v0/lm}/caching/persistent.py +1 -1
- synth_ai/{lm → v0/lm}/config.py +6 -1
- synth_ai/{lm → v0/lm}/core/all.py +9 -9
- synth_ai/{lm → v0/lm}/core/main.py +6 -6
- synth_ai/{lm → v0/lm}/core/main_v3.py +10 -10
- synth_ai/{lm → v0/lm}/core/synth_models.py +2 -14
- synth_ai/{lm → v0/lm}/core/vendor_clients.py +2 -2
- synth_ai/{lm → v0/lm}/overrides.py +2 -2
- synth_ai/{lm → v0/lm}/provider_support/anthropic.py +4 -4
- synth_ai/{lm → v0/lm}/provider_support/openai.py +5 -5
- synth_ai/{lm → v0/lm}/structured_outputs/handler.py +5 -5
- synth_ai/{lm → v0/lm}/structured_outputs/rehabilitate.py +1 -1
- synth_ai/{lm → v0/lm}/vendors/core/anthropic_api.py +9 -9
- synth_ai/{lm → v0/lm}/vendors/core/gemini_api.py +5 -5
- synth_ai/{lm → v0/lm}/vendors/core/mistral_api.py +5 -5
- synth_ai/{lm → v0/lm}/vendors/core/openai_api.py +10 -10
- synth_ai/{lm → v0/lm}/vendors/openai_standard.py +8 -8
- synth_ai/{lm → v0/lm}/vendors/openai_standard_responses.py +2 -2
- synth_ai/{lm → v0/lm}/vendors/supported/custom_endpoint.py +3 -3
- synth_ai/{lm → v0/lm}/vendors/supported/deepseek.py +2 -2
- synth_ai/{lm → v0/lm}/vendors/supported/grok.py +2 -2
- synth_ai/{lm → v0/lm}/vendors/supported/groq.py +1 -1
- synth_ai/{lm → v0/lm}/vendors/supported/ollama.py +1 -1
- synth_ai/{lm → v0/lm}/vendors/supported/openrouter.py +3 -3
- synth_ai/{lm → v0/lm}/vendors/supported/together.py +1 -1
- synth_ai/{lm → v0/lm}/vendors/synth_client.py +1 -1
- synth_ai/v0/tracing_v3/__init__.py +10 -0
- synth_ai/v0/tracing_v3/abstractions.py +3 -0
- synth_ai/v0/tracing_v3/decorators.py +3 -0
- synth_ai/v0/tracing_v3/llm_call_record_helpers.py +3 -0
- synth_ai/v0/tracing_v3/session_tracer.py +3 -0
- {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.10.dist-info}/METADATA +10 -7
- {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.10.dist-info}/RECORD +269 -233
- examples/common_old/backend.py +0 -20
- examples/evals_old/README.md +0 -98
- examples/evals_old/__init__.py +0 -6
- examples/evals_old/compare_models.py +0 -1038
- examples/evals_old/example_log.md +0 -145
- examples/evals_old/run_demo.sh +0 -126
- examples/evals_old/trace_analysis.py +0 -270
- examples/finetuning_old/_backup_synth_qwen/config.toml +0 -29
- examples/finetuning_old/_backup_synth_qwen/example_log.md +0 -324
- examples/finetuning_old/_backup_synth_qwen/filter_traces.py +0 -60
- examples/finetuning_old/_backup_synth_qwen/filter_traces_achievements.py +0 -243
- examples/finetuning_old/_backup_synth_qwen/purge_v3_traces.py +0 -109
- examples/finetuning_old/_backup_synth_qwen/react_agent_lm.py +0 -1924
- examples/finetuning_old/_backup_synth_qwen/readme.md +0 -49
- examples/finetuning_old/_backup_synth_qwen/run_crafter_qwen4b.py +0 -114
- examples/finetuning_old/_backup_synth_qwen/run_demo.sh +0 -195
- examples/finetuning_old/_backup_synth_qwen/sft_kickoff.py +0 -119
- examples/finetuning_old/synth_qwen_v1/README.md +0 -68
- examples/finetuning_old/synth_qwen_v1/filter_traces.py +0 -60
- examples/finetuning_old/synth_qwen_v1/filter_traces_achievements.py +0 -243
- examples/finetuning_old/synth_qwen_v1/finetune.py +0 -46
- examples/finetuning_old/synth_qwen_v1/hello_ft_model.py +0 -71
- examples/finetuning_old/synth_qwen_v1/infer.py +0 -36
- examples/finetuning_old/synth_qwen_v1/poll.py +0 -46
- examples/finetuning_old/synth_qwen_v1/prepare_data.py +0 -35
- examples/finetuning_old/synth_qwen_v1/purge_v3_traces.py +0 -109
- examples/finetuning_old/synth_qwen_v1/react_agent_lm.py +0 -1933
- examples/finetuning_old/synth_qwen_v1/run_crafter_sft_job.py +0 -210
- examples/finetuning_old/synth_qwen_v1/run_ft_job.py +0 -237
- examples/finetuning_old/synth_qwen_v1/upload_data.py +0 -34
- examples/finetuning_old/synth_qwen_v1/util.py +0 -152
- examples/rl_old/task_app.py +0 -1131
- synth_ai/experimental/synth_oss.py +0 -445
- synth_ai/learning/filtering.py +0 -0
- synth_ai/learning/offline/dpo.py +0 -0
- synth_ai/learning/offline/providers.py +0 -7
- synth_ai/learning/offline/sft.py +0 -0
- synth_ai/learning/offline/shared.py +0 -0
- synth_ai/learning/online/grpo.py +0 -0
- synth_ai/learning/online/irft.py +0 -0
- synth_ai/learning/prompts/banking77_injection_eval.py +0 -168
- synth_ai/learning/prompts/gepa.py +0 -0
- synth_ai/learning/prompts/hello_world_in_context_injection_ex.py +0 -211
- synth_ai/learning/prompts/mipro.py +0 -289
- synth_ai/learning/prompts/random_search.py +0 -249
- synth_ai/learning/prompts/run_mipro_banking77.py +0 -172
- synth_ai/learning/prompts/run_random_search_banking77.py +0 -329
- synth_ai/rl/secrets.py +0 -19
- synth_ai/scripts/verify_rewards.py +0 -100
- synth_ai/tracing/__init__.py +0 -30
- synth_ai/tracing_v1/__init__.py +0 -33
- synth_ai/tracing_v3/turso/__init__.py +0 -25
- synth_ai/tracing_v3/turso/manager.py +0 -838
- synth_ai/zyk/__init__.py +0 -30
- /synth_ai/{lm → v0/lm}/caching/__init__.py +0 -0
- /synth_ai/{lm → v0/lm}/caching/constants.py +0 -0
- /synth_ai/{lm → v0/lm}/caching/dbs.py +0 -0
- /synth_ai/{lm → v0/lm}/constants.py +0 -0
- /synth_ai/{lm → v0/lm}/core/__init__.py +0 -0
- /synth_ai/{lm → v0/lm}/core/exceptions.py +0 -0
- /synth_ai/{lm → v0/lm}/cost/__init__.py +0 -0
- /synth_ai/{lm → v0/lm}/cost/monitor.py +0 -0
- /synth_ai/{lm → v0/lm}/cost/statefulness.py +0 -0
- /synth_ai/{lm → v0/lm}/injection.py +0 -0
- /synth_ai/{lm → v0/lm}/provider_support/__init__.py +0 -0
- /synth_ai/{lm → v0/lm}/provider_support/suppress_logging.py +0 -0
- /synth_ai/{lm → v0/lm}/structured_outputs/__init__.py +0 -0
- /synth_ai/{lm → v0/lm}/structured_outputs/inject.py +0 -0
- /synth_ai/{lm → v0/lm}/tools/__init__.py +0 -0
- /synth_ai/{lm → v0/lm}/tools/base.py +0 -0
- /synth_ai/{lm → v0/lm}/unified_interface.py +0 -0
- /synth_ai/{lm → v0/lm}/vendors/__init__.py +0 -0
- /synth_ai/{lm → v0/lm}/vendors/base.py +0 -0
- /synth_ai/{lm → v0/lm}/vendors/core/__init__.py +0 -0
- /synth_ai/{lm → v0/lm}/vendors/core/synth_dev_api.py +0 -0
- /synth_ai/{lm → v0/lm}/vendors/local/__init__.py +0 -0
- /synth_ai/{lm → v0/lm}/vendors/local/ollama.py +0 -0
- /synth_ai/{lm → v0/lm}/vendors/retries.py +0 -0
- /synth_ai/{lm → v0/lm}/vendors/supported/__init__.py +0 -0
- /synth_ai/{lm → v0/lm}/warmup.py +0 -0
- {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.10.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.10.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.10.dist-info}/licenses/LICENSE +0 -0
- {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.10.dist-info}/top_level.txt +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
"""Helpers for uploading RL environment credentials to the backend."""
|
|
4
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
5
|
import base64
|
|
6
6
|
import binascii
|
|
7
7
|
import json
|
|
8
|
-
from typing import Any, Dict
|
|
9
8
|
import os
|
|
9
|
+
from typing import Any
|
|
10
10
|
|
|
11
11
|
import requests
|
|
12
12
|
from nacl.public import PublicKey, SealedBox
|
|
@@ -18,14 +18,12 @@ _ALGORITHM = "libsodium.sealedbox.v1"
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def encrypt_for_backend(pubkey_b64: str, secret: str | bytes) -> str:
|
|
21
|
-
"""Encrypt ``secret`` for storage by the backend using libsodium sealed boxes."""
|
|
22
|
-
|
|
23
21
|
if not isinstance(pubkey_b64, str) or not pubkey_b64.strip():
|
|
24
22
|
raise ValueError("public key must be a non-empty base64 string")
|
|
25
23
|
|
|
26
24
|
try:
|
|
27
25
|
key_bytes = base64.b64decode(pubkey_b64, validate=True)
|
|
28
|
-
except binascii.Error as exc:
|
|
26
|
+
except binascii.Error as exc:
|
|
29
27
|
raise ValueError("public key must be base64-encoded") from exc
|
|
30
28
|
|
|
31
29
|
if len(key_bytes) != 32:
|
|
@@ -35,7 +33,7 @@ def encrypt_for_backend(pubkey_b64: str, secret: str | bytes) -> str:
|
|
|
35
33
|
secret_bytes = secret.encode("utf-8")
|
|
36
34
|
elif isinstance(secret, bytes):
|
|
37
35
|
secret_bytes = secret
|
|
38
|
-
else:
|
|
36
|
+
else:
|
|
39
37
|
raise TypeError("secret must be str or bytes")
|
|
40
38
|
|
|
41
39
|
if not secret_bytes:
|
|
@@ -52,20 +50,17 @@ def setup_environment_api_key(
|
|
|
52
50
|
token: str | None = None,
|
|
53
51
|
*,
|
|
54
52
|
timeout: float = 15.0,
|
|
55
|
-
) ->
|
|
56
|
-
"""Upload an ENVIRONMENT_API_KEY to the backend."""
|
|
57
|
-
|
|
53
|
+
) -> dict[str, Any]:
|
|
58
54
|
backend = backend_base.rstrip("/")
|
|
59
55
|
if not backend:
|
|
60
56
|
raise ValueError("backend_base must be provided")
|
|
61
57
|
if not synth_api_key:
|
|
62
58
|
raise ValueError("synth_api_key must be provided")
|
|
63
59
|
|
|
64
|
-
# Require caller-provided plaintext. If not provided, read from ENVIRONMENT_API_KEY.
|
|
65
60
|
plaintext = token if token is not None else os.getenv("ENVIRONMENT_API_KEY", "").strip()
|
|
66
61
|
if not plaintext:
|
|
67
62
|
raise ValueError("ENVIRONMENT_API_KEY must be set (or pass token=...) to upload")
|
|
68
|
-
if not isinstance(plaintext, str):
|
|
63
|
+
if not isinstance(plaintext, str):
|
|
69
64
|
raise TypeError("token must be a string")
|
|
70
65
|
|
|
71
66
|
token_bytes = plaintext.encode("utf-8")
|
|
@@ -81,7 +76,7 @@ def setup_environment_api_key(
|
|
|
81
76
|
|
|
82
77
|
try:
|
|
83
78
|
doc = response.json()
|
|
84
|
-
except ValueError as exc:
|
|
79
|
+
except ValueError as exc:
|
|
85
80
|
raise RuntimeError("backend returned invalid JSON for public key") from exc
|
|
86
81
|
|
|
87
82
|
if not isinstance(doc, dict):
|
|
@@ -91,16 +86,45 @@ def setup_environment_api_key(
|
|
|
91
86
|
if not isinstance(pubkey, str) or not pubkey:
|
|
92
87
|
raise RuntimeError("backend response missing public_key")
|
|
93
88
|
|
|
94
|
-
# The backend currently returns a single algorithm identifier; keep a guard in
|
|
95
|
-
# case future versions change the value and we need to surface that to callers.
|
|
96
89
|
alg = doc.get("alg")
|
|
97
90
|
if alg is not None and alg != _ALGORITHM:
|
|
98
91
|
raise RuntimeError(f"unsupported sealed box algorithm: {alg}")
|
|
99
92
|
|
|
93
|
+
# Diagnostics: safe previews and hashes to correlate with backend logs
|
|
94
|
+
try:
|
|
95
|
+
import hashlib as _hash
|
|
96
|
+
|
|
97
|
+
pk_bytes = base64.b64decode(pubkey, validate=True)
|
|
98
|
+
pk_sha256 = _hash.sha256(pk_bytes).hexdigest()
|
|
99
|
+
print(
|
|
100
|
+
f"[env-keys] public_key: b64_len={len(pubkey)} sha256={pk_sha256} head={pubkey[:16]} tail={pubkey[-16:]}"
|
|
101
|
+
)
|
|
102
|
+
_plen = len(plaintext)
|
|
103
|
+
_ppref = (plaintext[:6] + "…") if _plen > 10 else plaintext
|
|
104
|
+
_psuf = ("…" + plaintext[-4:]) if _plen > 10 else ""
|
|
105
|
+
_has_ws = any(ch.isspace() for ch in plaintext)
|
|
106
|
+
print(
|
|
107
|
+
f"[env-keys] plaintext: len={_plen} preview={_ppref}{_psuf} has_ws={bool(_has_ws)}"
|
|
108
|
+
)
|
|
109
|
+
except Exception:
|
|
110
|
+
pass
|
|
111
|
+
|
|
100
112
|
ciphertext_b64 = encrypt_for_backend(pubkey, token_bytes)
|
|
101
113
|
|
|
102
114
|
body = {"name": "ENVIRONMENT_API_KEY", "ciphertext_b64": ciphertext_b64}
|
|
103
115
|
post_url = f"{backend}/api/v1/env-keys"
|
|
116
|
+
# Ciphertext diagnostics
|
|
117
|
+
try:
|
|
118
|
+
import hashlib as _hash
|
|
119
|
+
|
|
120
|
+
_ct_bytes = base64.b64decode(ciphertext_b64, validate=True)
|
|
121
|
+
_ct_sha = _hash.sha256(_ct_bytes).hexdigest()
|
|
122
|
+
print(
|
|
123
|
+
f"[env-keys] ciphertext: b64_len={len(ciphertext_b64)} sha256={_ct_sha} head={ciphertext_b64[:16]} tail={ciphertext_b64[-16:]}"
|
|
124
|
+
)
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
|
|
104
128
|
response2 = requests.post(
|
|
105
129
|
post_url,
|
|
106
130
|
headers={**headers, "Content-Type": "application/json"},
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Helpers for generating RL environment credentials."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import secrets
|
|
6
|
+
|
|
7
|
+
__all__ = ["mint_environment_api_key"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def mint_environment_api_key() -> str:
|
|
11
|
+
"""Mint a random ENVIRONMENT_API_KEY value."""
|
|
12
|
+
|
|
13
|
+
return secrets.token_hex(32)
|
synth_ai/learning/rl_client.py
CHANGED
|
@@ -1,284 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
import os
|
|
5
|
-
import time
|
|
3
|
+
from .rl.client import RlClient
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def _api_base(b: str) -> str:
|
|
11
|
-
b = (b or "").rstrip("/")
|
|
12
|
-
return b if b.endswith("/api") else f"{b}/api"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class RlClient:
|
|
16
|
-
"""Lightweight RL client for provider-agnostic job control.
|
|
17
|
-
|
|
18
|
-
Notes:
|
|
19
|
-
- Uses learning/* for status/events/metrics and rl/* for creation/start.
|
|
20
|
-
- Trainer endpoints are resolved server-side via trainer_id.
|
|
21
|
-
"""
|
|
22
|
-
|
|
23
|
-
def __init__(self, base_url: str, api_key: str, *, timeout: float = 600.0) -> None:
|
|
24
|
-
self._base_url = base_url.rstrip("/")
|
|
25
|
-
self._api_key = api_key
|
|
26
|
-
self._timeout = timeout
|
|
27
|
-
|
|
28
|
-
async def resolve_trainer_start_url(self, trainer_id: str) -> str:
|
|
29
|
-
"""GET /api/rl/services/{id} → { training_start_url }"""
|
|
30
|
-
path = f"/api/rl/services/{trainer_id}"
|
|
31
|
-
async with AsyncHttpClient(self._base_url, self._api_key, timeout=30.0) as http:
|
|
32
|
-
js = await http.get(path)
|
|
33
|
-
if not isinstance(js, dict):
|
|
34
|
-
raise HTTPError(
|
|
35
|
-
status=500, url=path, message="invalid_service_response", body_snippet=str(js)[:200]
|
|
36
|
-
)
|
|
37
|
-
start_url = js.get("training_start_url")
|
|
38
|
-
if not isinstance(start_url, str) or not start_url:
|
|
39
|
-
raise HTTPError(
|
|
40
|
-
status=500,
|
|
41
|
-
url=path,
|
|
42
|
-
message="missing_training_start_url",
|
|
43
|
-
body_snippet=str(js)[:200],
|
|
44
|
-
)
|
|
45
|
-
return start_url
|
|
46
|
-
|
|
47
|
-
async def create_job(
|
|
48
|
-
self,
|
|
49
|
-
*,
|
|
50
|
-
model: str,
|
|
51
|
-
task_app_url: str,
|
|
52
|
-
trainer: Dict[str, Any],
|
|
53
|
-
trainer_id: Optional[str] = None,
|
|
54
|
-
job_config_id: Optional[str] = None,
|
|
55
|
-
inline_config: Optional[Dict[str, Any]] = None,
|
|
56
|
-
) -> Dict[str, Any]:
|
|
57
|
-
body = {
|
|
58
|
-
"job_type": "rl",
|
|
59
|
-
"data": {
|
|
60
|
-
"model": model,
|
|
61
|
-
"endpoint_base_url": task_app_url,
|
|
62
|
-
**({"job_config_id": job_config_id} if job_config_id else {}),
|
|
63
|
-
**({"config": inline_config} if inline_config else {}),
|
|
64
|
-
"trainer": {
|
|
65
|
-
"batch_size": int(trainer.get("batch_size", 1)),
|
|
66
|
-
"group_size": max(2, int(trainer.get("group_size", 2))),
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
}
|
|
70
|
-
async with AsyncHttpClient(self._base_url, self._api_key, timeout=self._timeout) as http:
|
|
71
|
-
js = await http.post_json(f"{_api_base(self._base_url)}/rl/jobs", json=body)
|
|
72
|
-
if not isinstance(js, dict):
|
|
73
|
-
raise HTTPError(
|
|
74
|
-
status=500,
|
|
75
|
-
url="/api/rl/jobs",
|
|
76
|
-
message="invalid_create_response",
|
|
77
|
-
body_snippet=str(js)[:200],
|
|
78
|
-
)
|
|
79
|
-
return js
|
|
80
|
-
|
|
81
|
-
async def start_job_if_supported(self, job_id: str) -> Optional[Dict[str, Any]]:
|
|
82
|
-
path = f"{_api_base(self._base_url)}/rl/jobs/{job_id}/start"
|
|
83
|
-
try:
|
|
84
|
-
async with AsyncHttpClient(self._base_url, self._api_key, timeout=30.0) as http:
|
|
85
|
-
return await http.post_json(path, json={})
|
|
86
|
-
except HTTPError as he: # noqa: PERF203
|
|
87
|
-
if he.status == 404:
|
|
88
|
-
return None
|
|
89
|
-
raise
|
|
90
|
-
|
|
91
|
-
async def get_job(self, job_id: str) -> Dict[str, Any]:
|
|
92
|
-
async with AsyncHttpClient(self._base_url, self._api_key, timeout=30.0) as http:
|
|
93
|
-
return await http.get(f"{_api_base(self._base_url)}/learning/jobs/{job_id}")
|
|
94
|
-
|
|
95
|
-
async def get_events(
|
|
96
|
-
self, job_id: str, *, since_seq: int = 0, limit: int = 200
|
|
97
|
-
) -> List[Dict[str, Any]]:
|
|
98
|
-
params = {"since_seq": since_seq, "limit": limit}
|
|
99
|
-
async with AsyncHttpClient(self._base_url, self._api_key, timeout=30.0) as http:
|
|
100
|
-
try:
|
|
101
|
-
js = await http.get(
|
|
102
|
-
f"{_api_base(self._base_url)}/learning/jobs/{job_id}/events", params=params
|
|
103
|
-
)
|
|
104
|
-
except HTTPError as he:
|
|
105
|
-
try:
|
|
106
|
-
print(
|
|
107
|
-
f"[poll] events HTTPError status={he.status} url={he.url} since_seq={since_seq} body={(he.body_snippet or '')[:200]}"
|
|
108
|
-
)
|
|
109
|
-
except Exception:
|
|
110
|
-
pass
|
|
111
|
-
raise
|
|
112
|
-
if isinstance(js, dict):
|
|
113
|
-
evs = js.get("events") or js.get("data")
|
|
114
|
-
if isinstance(evs, list):
|
|
115
|
-
return evs
|
|
116
|
-
return []
|
|
117
|
-
|
|
118
|
-
async def get_metrics(
|
|
119
|
-
self, job_id: str, *, after_step: int = -1, limit: int = 200
|
|
120
|
-
) -> List[Dict[str, Any]]:
|
|
121
|
-
params = {"after_step": after_step, "limit": limit}
|
|
122
|
-
async with AsyncHttpClient(self._base_url, self._api_key, timeout=30.0) as http:
|
|
123
|
-
js = await http.get(
|
|
124
|
-
f"{_api_base(self._base_url)}/learning/jobs/{job_id}/metrics", params=params
|
|
125
|
-
)
|
|
126
|
-
if isinstance(js, dict) and isinstance(js.get("points"), list):
|
|
127
|
-
return js["points"]
|
|
128
|
-
return []
|
|
129
|
-
|
|
130
|
-
async def poll_until_terminal(
|
|
131
|
-
self,
|
|
132
|
-
job_id: str,
|
|
133
|
-
*,
|
|
134
|
-
interval_seconds: float = 2.0,
|
|
135
|
-
max_seconds: float | None = None,
|
|
136
|
-
empty_polls_threshold: int = 5,
|
|
137
|
-
startup_deadline_s: int = 45,
|
|
138
|
-
on_event: Optional[Callable[[Dict[str, Any]], None]] = None,
|
|
139
|
-
on_metric: Optional[Callable[[Dict[str, Any]], None]] = None,
|
|
140
|
-
) -> Dict[str, Any]:
|
|
141
|
-
last_seq_by_stream: Dict[str, int] = {}
|
|
142
|
-
events_job_id: Optional[str] = None
|
|
143
|
-
last_status: Optional[str] = None
|
|
144
|
-
last_step_by_name: Dict[str, int] = {}
|
|
145
|
-
empty_polls = 0
|
|
146
|
-
saw_any_event = False
|
|
147
|
-
start_t = time.time()
|
|
148
|
-
terminal = {"succeeded", "failed", "cancelled", "canceled", "error", "completed"}
|
|
149
|
-
|
|
150
|
-
while True:
|
|
151
|
-
status_data: Optional[Dict[str, Any]] = None
|
|
152
|
-
try:
|
|
153
|
-
status_data = await self.get_job(job_id)
|
|
154
|
-
except Exception:
|
|
155
|
-
status_data = None
|
|
156
|
-
if status_data is None:
|
|
157
|
-
try:
|
|
158
|
-
print(f"[poll] get_job returned None base={self._base_url} job_id={job_id}")
|
|
159
|
-
except Exception:
|
|
160
|
-
pass
|
|
161
|
-
status = str((status_data or {}).get("status") or "").lower()
|
|
162
|
-
if status_data:
|
|
163
|
-
linked = status_data.get("linked_job_id")
|
|
164
|
-
if isinstance(linked, str) and linked and linked != events_job_id:
|
|
165
|
-
events_job_id = linked
|
|
166
|
-
try:
|
|
167
|
-
print(f"[poll] discovered linked_job_id stream={events_job_id}")
|
|
168
|
-
except Exception:
|
|
169
|
-
pass
|
|
170
|
-
if status and status != last_status:
|
|
171
|
-
last_status = status
|
|
172
|
-
# Status transitions only to avoid log spam
|
|
173
|
-
if on_event:
|
|
174
|
-
try:
|
|
175
|
-
on_event({"type": "rl.status", "message": status})
|
|
176
|
-
except Exception:
|
|
177
|
-
pass
|
|
178
|
-
|
|
179
|
-
# Events
|
|
180
|
-
stream_ids = [job_id]
|
|
181
|
-
if events_job_id and events_job_id not in stream_ids:
|
|
182
|
-
stream_ids.append(events_job_id)
|
|
183
|
-
try:
|
|
184
|
-
print(
|
|
185
|
-
f"[poll] streams={stream_ids} intervals={interval_seconds}s since_map={last_seq_by_stream} empty_polls={empty_polls}"
|
|
186
|
-
)
|
|
187
|
-
except Exception:
|
|
188
|
-
pass
|
|
189
|
-
total_events_this_cycle = 0
|
|
190
|
-
terminal_event_seen = False
|
|
191
|
-
terminal_event_status: Optional[str] = None
|
|
192
|
-
for ev_id in stream_ids:
|
|
193
|
-
since = last_seq_by_stream.get(ev_id, 0)
|
|
194
|
-
try:
|
|
195
|
-
events = await self.get_events(ev_id, since_seq=since, limit=200)
|
|
196
|
-
except HTTPError as he:
|
|
197
|
-
try:
|
|
198
|
-
print(
|
|
199
|
-
f"[poll] get_events error status={he.status} url={he.url} since={since} body={(he.body_snippet or '')[:200]}"
|
|
200
|
-
)
|
|
201
|
-
except Exception:
|
|
202
|
-
pass
|
|
203
|
-
events = []
|
|
204
|
-
except Exception as e:
|
|
205
|
-
try:
|
|
206
|
-
print(
|
|
207
|
-
f"[poll] get_events unexpected error ev_id={ev_id} since={since} err={type(e).__name__}: {e}"
|
|
208
|
-
)
|
|
209
|
-
except Exception:
|
|
210
|
-
pass
|
|
211
|
-
events = []
|
|
212
|
-
total_events_this_cycle += len(events)
|
|
213
|
-
if events:
|
|
214
|
-
saw_any_event = True
|
|
215
|
-
for e in events:
|
|
216
|
-
seq_val = int(e.get("seq") or 0)
|
|
217
|
-
if seq_val <= last_seq_by_stream.get(ev_id, 0):
|
|
218
|
-
continue
|
|
219
|
-
last_seq_by_stream[ev_id] = seq_val
|
|
220
|
-
if on_event:
|
|
221
|
-
try:
|
|
222
|
-
on_event(e)
|
|
223
|
-
except Exception:
|
|
224
|
-
pass
|
|
225
|
-
et = str(e.get("type") or e.get("event_type") or "").lower()
|
|
226
|
-
if et in ("rl.job.completed", "workflow.completed", "rl.train.completed"):
|
|
227
|
-
terminal_event_seen = True
|
|
228
|
-
terminal_event_status = "succeeded"
|
|
229
|
-
elif et in ("rl.job.failed", "workflow.failed"):
|
|
230
|
-
terminal_event_seen = True
|
|
231
|
-
terminal_event_status = "failed"
|
|
232
|
-
|
|
233
|
-
# Metrics
|
|
234
|
-
try:
|
|
235
|
-
after = max(last_step_by_name.values()) if last_step_by_name else -1
|
|
236
|
-
points = await self.get_metrics(job_id, after_step=after, limit=200)
|
|
237
|
-
for p in points:
|
|
238
|
-
name = str(p.get("name") or "")
|
|
239
|
-
step = int(p.get("step") or -1)
|
|
240
|
-
if step <= last_step_by_name.get(name, -1):
|
|
241
|
-
continue
|
|
242
|
-
last_step_by_name[name] = step
|
|
243
|
-
if on_metric:
|
|
244
|
-
try:
|
|
245
|
-
on_metric(p)
|
|
246
|
-
except Exception:
|
|
247
|
-
pass
|
|
248
|
-
except Exception:
|
|
249
|
-
pass
|
|
250
|
-
|
|
251
|
-
if terminal_event_seen:
|
|
252
|
-
return {"status": terminal_event_status or status or "completed", "job_id": job_id}
|
|
253
|
-
if status and status in terminal:
|
|
254
|
-
return {"status": status, "job_id": job_id}
|
|
255
|
-
|
|
256
|
-
if total_events_this_cycle == 0:
|
|
257
|
-
empty_polls += 1
|
|
258
|
-
else:
|
|
259
|
-
empty_polls = 0
|
|
260
|
-
if empty_polls >= max(1, int(empty_polls_threshold)):
|
|
261
|
-
try:
|
|
262
|
-
print(
|
|
263
|
-
f"[poll] threshold hit: empty_polls={empty_polls} >= {empty_polls_threshold} streams={stream_ids} last_seq_map={last_seq_by_stream}"
|
|
264
|
-
)
|
|
265
|
-
except Exception:
|
|
266
|
-
pass
|
|
267
|
-
raise AssertionError(
|
|
268
|
-
f"No new events detected for {empty_polls_threshold} consecutive polls. Check event ingestion."
|
|
269
|
-
)
|
|
270
|
-
|
|
271
|
-
if not saw_any_event and (time.time() - start_t) > int(startup_deadline_s):
|
|
272
|
-
try:
|
|
273
|
-
print(
|
|
274
|
-
f"[poll] startup window exceeded: {startup_deadline_s}s base={self._base_url} job={job_id} streams={stream_ids} last_seq_map={last_seq_by_stream}"
|
|
275
|
-
)
|
|
276
|
-
except Exception:
|
|
277
|
-
pass
|
|
278
|
-
raise AssertionError(
|
|
279
|
-
f"No events observed within startup window ({startup_deadline_s}s). Investigate event streaming."
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
await sleep(interval_seconds)
|
|
283
|
-
if max_seconds is not None and (time.time() - start_t) >= max_seconds:
|
|
284
|
-
raise TimeoutError(f"Polling timed out after {max_seconds}s for job {job_id}")
|
|
5
|
+
__all__ = ["RlClient"]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from .client import FtClient
|
|
2
|
+
from .data import (
|
|
3
|
+
SFTDataError,
|
|
4
|
+
SFTExample,
|
|
5
|
+
SFTMessage,
|
|
6
|
+
SFTToolCall,
|
|
7
|
+
SFTToolDefinition,
|
|
8
|
+
coerce_example,
|
|
9
|
+
collect_sft_jsonl_errors,
|
|
10
|
+
iter_sft_examples,
|
|
11
|
+
load_jsonl,
|
|
12
|
+
parse_jsonl_line,
|
|
13
|
+
validate_jsonl_or_raise,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"FtClient",
|
|
18
|
+
"SFTDataError",
|
|
19
|
+
"SFTExample",
|
|
20
|
+
"SFTMessage",
|
|
21
|
+
"SFTToolCall",
|
|
22
|
+
"SFTToolDefinition",
|
|
23
|
+
"collect_sft_jsonl_errors",
|
|
24
|
+
"coerce_example",
|
|
25
|
+
"iter_sft_examples",
|
|
26
|
+
"load_jsonl",
|
|
27
|
+
"parse_jsonl_line",
|
|
28
|
+
"validate_jsonl_or_raise",
|
|
29
|
+
]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ...http import AsyncHttpClient, HTTPError
|
|
7
|
+
from .config import prepare_sft_job_payload
|
|
8
|
+
from .data import validate_jsonl_or_raise
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FtClient:
|
|
12
|
+
def __init__(self, base_url: str, api_key: str, *, timeout: float = 30.0) -> None:
|
|
13
|
+
self._base_url = base_url.rstrip("/")
|
|
14
|
+
self._api_key = api_key
|
|
15
|
+
self._timeout = timeout
|
|
16
|
+
|
|
17
|
+
async def upload_training_file(self, path: str | Path, *, purpose: str = "fine-tune") -> str:
|
|
18
|
+
p = Path(path)
|
|
19
|
+
if p.suffix.lower() == ".jsonl" and purpose == "fine-tune":
|
|
20
|
+
validate_jsonl_or_raise(p, min_messages=2)
|
|
21
|
+
content = p.read_bytes()
|
|
22
|
+
async with AsyncHttpClient(self._base_url, self._api_key, timeout=self._timeout) as http:
|
|
23
|
+
data = {"purpose": purpose}
|
|
24
|
+
files = {"file": (p.name, content, _infer_content_type(p.name))}
|
|
25
|
+
js = await http.post_multipart("/api/learning/files", data=data, files=files)
|
|
26
|
+
if not isinstance(js, dict) or "id" not in js:
|
|
27
|
+
raise HTTPError(
|
|
28
|
+
status=500,
|
|
29
|
+
url="/api/learning/files",
|
|
30
|
+
message="invalid_upload_response",
|
|
31
|
+
body_snippet=str(js)[:200],
|
|
32
|
+
)
|
|
33
|
+
return str(js["id"])
|
|
34
|
+
|
|
35
|
+
async def create_sft_job(
|
|
36
|
+
self,
|
|
37
|
+
*,
|
|
38
|
+
model: str,
|
|
39
|
+
training_file_id: str,
|
|
40
|
+
hyperparameters: dict[str, Any],
|
|
41
|
+
metadata: dict[str, Any] | None = None,
|
|
42
|
+
) -> dict[str, Any]:
|
|
43
|
+
body = prepare_sft_job_payload(
|
|
44
|
+
model=model,
|
|
45
|
+
training_file=training_file_id,
|
|
46
|
+
hyperparameters=hyperparameters,
|
|
47
|
+
metadata=metadata,
|
|
48
|
+
training_type="sft_offline",
|
|
49
|
+
training_file_field="training_file_id",
|
|
50
|
+
require_training_file=True,
|
|
51
|
+
)
|
|
52
|
+
async with AsyncHttpClient(self._base_url, self._api_key, timeout=self._timeout) as http:
|
|
53
|
+
return await http.post_json("/api/learning/jobs", json=body)
|
|
54
|
+
|
|
55
|
+
async def start_job(self, job_id: str) -> dict[str, Any]:
|
|
56
|
+
async with AsyncHttpClient(self._base_url, self._api_key, timeout=self._timeout) as http:
|
|
57
|
+
return await http.post_json(f"/api/learning/jobs/{job_id}/start", json={})
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _infer_content_type(filename: str) -> str:
|
|
61
|
+
name = filename.lower()
|
|
62
|
+
if name.endswith(".jsonl"):
|
|
63
|
+
return "application/jsonl"
|
|
64
|
+
if name.endswith(".json"):
|
|
65
|
+
return "application/json"
|
|
66
|
+
if name.endswith(".txt"):
|
|
67
|
+
return "text/plain"
|
|
68
|
+
return "application/octet-stream"
|