synth-ai 0.2.14__py3-none-any.whl → 0.2.17__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/README.md +1 -0
- examples/analyze_semantic_words.sh +2 -2
- examples/blog_posts/pokemon_vl/README.md +98 -0
- examples/blog_posts/pokemon_vl/configs/eval_qwen3_vl.toml +25 -0
- examples/blog_posts/pokemon_vl/configs/eval_rl_final.toml +24 -0
- examples/blog_posts/pokemon_vl/configs/filter_high_reward.toml +10 -0
- examples/blog_posts/pokemon_vl/configs/train_rl_from_sft.toml +42 -0
- examples/blog_posts/pokemon_vl/configs/train_sft_qwen4b_vl.toml +40 -0
- examples/blog_posts/warming_up_to_rl/README.md +158 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b.toml +25 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_groq_qwen32b.toml +25 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_openai_gpt_oss_120b.toml +29 -0
- examples/blog_posts/warming_up_to_rl/configs/filter_high_reward_dataset.toml +10 -0
- examples/blog_posts/warming_up_to_rl/configs/train_rl_from_sft.toml +41 -0
- examples/blog_posts/warming_up_to_rl/configs/train_sft_qwen4b.toml +40 -0
- examples/dev/qwen3_32b_qlora_4xh100.toml +5 -0
- examples/multi_step/SFT_README.md +147 -0
- examples/multi_step/configs/crafter_rl_outcome.toml +1 -1
- examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +73 -115
- examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +1 -1
- examples/multi_step/configs/crafter_rl_stepwise_simple.toml +1 -1
- examples/multi_step/configs/crafter_rl_stepwise_simple_NEW_FORMAT.toml +105 -0
- examples/multi_step/configs/crafter_sft_qwen30b_lora.toml +62 -0
- examples/multi_step/configs/verilog_rl_lora.toml +80 -123
- examples/multi_step/convert_traces_to_sft.py +84 -0
- examples/multi_step/run_sft_qwen30b.sh +45 -0
- examples/qwen_coder/configs/coder_lora_30b.toml +1 -2
- examples/qwen_coder/configs/coder_lora_4b.toml +5 -1
- examples/qwen_coder/configs/coder_lora_small.toml +1 -2
- examples/qwen_vl/BUGS_AND_FIXES.md +232 -0
- examples/qwen_vl/IMAGE_VALIDATION_COMPLETE.md +271 -0
- examples/qwen_vl/IMAGE_VALIDATION_SUMMARY.md +260 -0
- examples/qwen_vl/INFERENCE_SFT_TESTS.md +412 -0
- examples/qwen_vl/NEXT_STEPS_2B.md +325 -0
- examples/qwen_vl/QUICKSTART.md +327 -0
- examples/qwen_vl/QUICKSTART_RL_VISION.md +110 -0
- examples/qwen_vl/README.md +152 -0
- examples/qwen_vl/RL_VISION_COMPLETE.md +475 -0
- examples/qwen_vl/RL_VISION_TESTING.md +333 -0
- examples/qwen_vl/SDK_VISION_INTEGRATION.md +328 -0
- examples/qwen_vl/SETUP_COMPLETE.md +274 -0
- examples/qwen_vl/VISION_TESTS_COMPLETE.md +489 -0
- examples/qwen_vl/VLM_PIPELINE_COMPLETE.md +242 -0
- examples/qwen_vl/__init__.py +2 -0
- examples/qwen_vl/collect_data_via_cli.md +415 -0
- examples/qwen_vl/collect_vision_traces.py +368 -0
- examples/qwen_vl/configs/crafter_rl_vision_qwen3vl4b.toml +110 -0
- examples/qwen_vl/configs/crafter_vlm_sft_example.toml +59 -0
- examples/qwen_vl/configs/eval_gpt4o_mini_vision.toml +26 -0
- examples/qwen_vl/configs/eval_gpt4o_vision_proper.toml +29 -0
- examples/qwen_vl/configs/eval_gpt5nano_vision.toml +26 -0
- examples/qwen_vl/configs/eval_qwen3vl_vision.toml +26 -0
- examples/qwen_vl/configs/filter_qwen3vl_sft.toml +49 -0
- examples/qwen_vl/configs/filter_vision_sft.toml +52 -0
- examples/qwen_vl/configs/filter_vision_test.toml +8 -0
- examples/qwen_vl/configs/sft_qwen3_vl_2b_test.toml +54 -0
- examples/qwen_vl/crafter_gpt5nano_agent.py +308 -0
- examples/qwen_vl/crafter_qwen_vl_agent.py +300 -0
- examples/qwen_vl/run_vision_comparison.sh +61 -0
- examples/qwen_vl/run_vision_sft_pipeline.sh +175 -0
- examples/qwen_vl/test_image_validation.py +201 -0
- examples/qwen_vl/test_sft_vision_data.py +110 -0
- examples/rl/README.md +6 -6
- examples/rl/configs/eval_base_qwen.toml +17 -0
- examples/rl/configs/eval_rl_qwen.toml +13 -0
- examples/rl/configs/rl_from_base_qwen.toml +62 -0
- examples/rl/configs/rl_from_base_qwen17.toml +79 -0
- examples/rl/configs/rl_from_ft_qwen.toml +37 -0
- examples/rl/run_eval.py +436 -0
- examples/rl/run_rl_and_save.py +111 -0
- examples/rl/task_app/README.md +21 -0
- examples/rl/task_app/math_single_step.py +990 -0
- examples/rl/task_app/math_task_app.py +111 -0
- examples/run_crafter_demo.sh +2 -2
- examples/sft/README.md +6 -6
- examples/sft/configs/crafter_fft_qwen0p6b.toml +7 -2
- examples/sft/configs/crafter_lora_qwen0p6b.toml +7 -3
- examples/sft/evaluate.py +2 -4
- examples/sft/export_dataset.py +7 -4
- examples/swe/task_app/README.md +33 -3
- examples/swe/task_app/grpo_swe_mini.py +4 -1
- examples/swe/task_app/grpo_swe_mini_task_app.py +0 -12
- examples/swe/task_app/hosted/envs/crafter/react_agent.py +1 -1
- examples/swe/task_app/hosted/envs/mini_swe/environment.py +50 -23
- examples/swe/task_app/hosted/inference/openai_client.py +4 -4
- examples/swe/task_app/hosted/policy_routes.py +0 -2
- examples/swe/task_app/hosted/rollout.py +0 -8
- examples/swe/task_app/morph_backend.py +178 -0
- examples/task_apps/crafter/task_app/README.md +1 -1
- examples/task_apps/crafter/task_app/grpo_crafter.py +70 -10
- examples/task_apps/crafter/task_app/grpo_crafter_task_app.py +1 -1
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/policy.py +63 -27
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/react_agent.py +1 -2
- examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +48 -50
- examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +75 -36
- examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +31 -15
- examples/task_apps/enron/__init__.py +1 -0
- examples/task_apps/enron/task_app/grpo_enron_task_app.py +1 -1
- examples/task_apps/math/README.md +1 -2
- examples/task_apps/pokemon_red/README.md +3 -4
- examples/task_apps/pokemon_red/eval_image_only_gpt4o.toml +6 -5
- examples/task_apps/pokemon_red/eval_pokemon_red_policy.py +1 -2
- examples/task_apps/pokemon_red/task_app.py +36 -5
- examples/task_apps/sokoban/README.md +2 -3
- examples/task_apps/verilog/eval_groq_qwen32b.toml +12 -14
- examples/task_apps/verilog/task_app/grpo_verilog_task_app.py +1 -1
- examples/vlm/README.md +3 -3
- examples/vlm/configs/crafter_vlm_gpt4o.toml +5 -0
- examples/vlm/crafter_openai_vlm_agent.py +3 -5
- examples/vlm/filter_image_rows.py +1 -1
- examples/vlm/run_crafter_vlm_benchmark.py +2 -2
- examples/warming_up_to_rl/_utils.py +92 -0
- examples/warming_up_to_rl/analyze_trace_db.py +1 -1
- examples/warming_up_to_rl/configs/crafter_fft.toml +5 -0
- examples/warming_up_to_rl/configs/eval_fft_qwen4b.toml +2 -0
- examples/warming_up_to_rl/configs/eval_groq_qwen32b.toml +2 -0
- examples/warming_up_to_rl/configs/eval_modal_qwen4b.toml +2 -1
- examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +2 -1
- examples/warming_up_to_rl/configs/rl_from_ft.toml +2 -0
- examples/warming_up_to_rl/export_trace_sft.py +174 -60
- examples/warming_up_to_rl/readme.md +63 -132
- examples/warming_up_to_rl/run_fft_and_save.py +1 -1
- examples/warming_up_to_rl/run_local_rollout_traced.py +1 -1
- examples/warming_up_to_rl/run_rl_and_save.py +1 -1
- examples/warming_up_to_rl/task_app/README.md +42 -0
- examples/warming_up_to_rl/task_app/grpo_crafter.py +827 -0
- examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +135 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/README.md +173 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/__init__.py +5 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/branching.py +143 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/environment_routes.py +1226 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/__init__.py +1 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/__init__.py +6 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/app.py +1 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/environment.py +522 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +454 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +108 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/shared.py +305 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/tools.py +47 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +204 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/__init__.py +5 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +618 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/main.py +100 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +1084 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/registry.py +195 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +1861 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/__init__.py +5 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/volume.py +211 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/test_agents.py +161 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/test_service.py +137 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +62 -0
- examples/workflows/math_rl/configs/rl_from_base_qwen.toml +27 -0
- examples/workflows/math_rl/configs/rl_from_base_qwen17.toml +5 -0
- synth_ai/__init__.py +44 -30
- synth_ai/_utils/__init__.py +47 -0
- synth_ai/_utils/base_url.py +10 -0
- synth_ai/_utils/http.py +10 -0
- synth_ai/_utils/prompts.py +10 -0
- synth_ai/_utils/task_app_state.py +12 -0
- synth_ai/_utils/user_config.py +10 -0
- synth_ai/api/models/supported.py +144 -7
- synth_ai/api/train/__init__.py +13 -1
- synth_ai/api/train/builders.py +9 -3
- synth_ai/api/train/cli.py +155 -17
- synth_ai/api/train/config_finder.py +18 -11
- synth_ai/api/train/configs/__init__.py +8 -1
- synth_ai/api/train/configs/rl.py +32 -7
- synth_ai/api/train/configs/sft.py +6 -2
- synth_ai/api/train/configs/shared.py +59 -2
- synth_ai/api/train/env_resolver.py +13 -10
- synth_ai/auth/credentials.py +119 -0
- synth_ai/cli/__init__.py +61 -69
- synth_ai/cli/_modal_wrapper.py +7 -5
- synth_ai/cli/_typer_patch.py +0 -2
- synth_ai/cli/_validate_task_app.py +22 -4
- synth_ai/cli/commands/__init__.py +17 -0
- synth_ai/cli/commands/demo/__init__.py +6 -0
- synth_ai/cli/commands/demo/core.py +163 -0
- synth_ai/cli/commands/deploy/__init__.py +23 -0
- synth_ai/cli/commands/deploy/core.py +614 -0
- synth_ai/cli/commands/deploy/errors.py +72 -0
- synth_ai/cli/commands/deploy/validation.py +11 -0
- synth_ai/cli/commands/eval/__init__.py +19 -0
- synth_ai/cli/commands/eval/core.py +1109 -0
- synth_ai/cli/commands/eval/errors.py +81 -0
- synth_ai/cli/commands/eval/validation.py +133 -0
- synth_ai/cli/commands/filter/__init__.py +12 -0
- synth_ai/cli/commands/filter/core.py +388 -0
- synth_ai/cli/commands/filter/errors.py +55 -0
- synth_ai/cli/commands/filter/validation.py +77 -0
- synth_ai/cli/commands/help/__init__.py +177 -0
- synth_ai/cli/commands/help/core.py +73 -0
- synth_ai/cli/commands/status/__init__.py +64 -0
- synth_ai/cli/commands/status/client.py +192 -0
- synth_ai/cli/commands/status/config.py +92 -0
- synth_ai/cli/commands/status/errors.py +20 -0
- synth_ai/cli/commands/status/formatters.py +164 -0
- synth_ai/cli/commands/status/subcommands/__init__.py +9 -0
- synth_ai/cli/commands/status/subcommands/files.py +79 -0
- synth_ai/cli/commands/status/subcommands/jobs.py +334 -0
- synth_ai/cli/commands/status/subcommands/models.py +79 -0
- synth_ai/cli/commands/status/subcommands/runs.py +81 -0
- synth_ai/cli/commands/status/subcommands/summary.py +47 -0
- synth_ai/cli/commands/status/utils.py +114 -0
- synth_ai/cli/commands/train/__init__.py +53 -0
- synth_ai/cli/commands/train/core.py +21 -0
- synth_ai/cli/commands/train/errors.py +117 -0
- synth_ai/cli/commands/train/judge_schemas.py +199 -0
- synth_ai/cli/commands/train/judge_validation.py +304 -0
- synth_ai/cli/commands/train/validation.py +443 -0
- synth_ai/cli/demo.py +2 -162
- synth_ai/cli/deploy/__init__.py +28 -0
- synth_ai/cli/deploy/core.py +5 -0
- synth_ai/cli/deploy/errors.py +23 -0
- synth_ai/cli/deploy/validation.py +5 -0
- synth_ai/cli/eval/__init__.py +36 -0
- synth_ai/cli/eval/core.py +5 -0
- synth_ai/cli/eval/errors.py +31 -0
- synth_ai/cli/eval/validation.py +5 -0
- synth_ai/cli/filter/__init__.py +28 -0
- synth_ai/cli/filter/core.py +5 -0
- synth_ai/cli/filter/errors.py +23 -0
- synth_ai/cli/filter/validation.py +5 -0
- synth_ai/cli/legacy_root_backup.py +3 -1
- synth_ai/cli/lib/__init__.py +10 -0
- synth_ai/cli/lib/task_app_discovery.py +7 -0
- synth_ai/cli/lib/task_app_env.py +518 -0
- synth_ai/cli/modal_serve/__init__.py +12 -0
- synth_ai/cli/modal_serve/core.py +14 -0
- synth_ai/cli/modal_serve/errors.py +8 -0
- synth_ai/cli/modal_serve/validation.py +11 -0
- synth_ai/cli/recent.py +2 -1
- synth_ai/cli/serve/__init__.py +12 -0
- synth_ai/cli/serve/core.py +14 -0
- synth_ai/cli/serve/errors.py +8 -0
- synth_ai/cli/serve/validation.py +11 -0
- synth_ai/cli/setup.py +21 -0
- synth_ai/cli/status.py +7 -126
- synth_ai/cli/task_app_deploy.py +7 -0
- synth_ai/cli/task_app_list.py +25 -0
- synth_ai/cli/task_app_modal_serve.py +11 -0
- synth_ai/cli/task_app_serve.py +11 -0
- synth_ai/cli/task_apps.py +110 -1499
- synth_ai/cli/traces.py +1 -1
- synth_ai/cli/train/__init__.py +12 -0
- synth_ai/cli/train/core.py +21 -0
- synth_ai/cli/train/errors.py +8 -0
- synth_ai/cli/train/validation.py +24 -0
- synth_ai/cli/train.py +5 -0
- synth_ai/cli/turso.py +1 -1
- synth_ai/cli/watch.py +1 -1
- synth_ai/demos/__init__.py +10 -0
- synth_ai/demos/core/__init__.py +28 -1
- synth_ai/demos/crafter/__init__.py +1 -0
- synth_ai/demos/crafter/crafter_fft_4b.toml +55 -0
- synth_ai/demos/crafter/grpo_crafter_task_app.py +185 -0
- synth_ai/demos/crafter/rl_from_base_qwen4b.toml +74 -0
- synth_ai/demos/demo_registry.py +176 -0
- synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +1 -1
- synth_ai/demos/math/__init__.py +1 -0
- synth_ai/demos/math/_common.py +16 -0
- synth_ai/demos/math/app.py +38 -0
- synth_ai/demos/math/config.toml +76 -0
- synth_ai/demos/math/deploy_modal.py +54 -0
- synth_ai/demos/math/modal_task_app.py +702 -0
- synth_ai/demos/math/task_app_entry.py +51 -0
- synth_ai/environments/environment/core.py +7 -1
- synth_ai/environments/examples/bandit/engine.py +0 -1
- synth_ai/environments/examples/bandit/environment.py +0 -1
- synth_ai/environments/examples/red/engine.py +33 -12
- synth_ai/environments/examples/red/engine_helpers/reward_components.py +151 -179
- synth_ai/environments/examples/red/environment.py +26 -0
- synth_ai/environments/examples/red/trace_hooks_v3.py +168 -0
- synth_ai/environments/examples/wordle/environment.py +0 -1
- synth_ai/evals/base.py +16 -5
- synth_ai/evals/client.py +1 -1
- synth_ai/http.py +8 -22
- synth_ai/inference/client.py +1 -1
- synth_ai/judge_schemas.py +4 -5
- synth_ai/learning/client.py +1 -1
- synth_ai/learning/health.py +1 -1
- synth_ai/learning/jobs.py +1 -1
- synth_ai/learning/rl/client.py +4 -2
- synth_ai/learning/rl/env_keys.py +1 -1
- synth_ai/learning/rl/secrets.py +1 -1
- synth_ai/learning/sft/client.py +1 -1
- synth_ai/learning/sft/data.py +407 -4
- synth_ai/learning/validators.py +4 -1
- synth_ai/streaming/__init__.py +29 -0
- synth_ai/streaming/config.py +94 -0
- synth_ai/streaming/handlers.py +469 -0
- synth_ai/streaming/streamer.py +301 -0
- synth_ai/streaming/types.py +95 -0
- synth_ai/task/apps/__init__.py +4 -2
- synth_ai/task/config.py +6 -4
- synth_ai/task/rubrics/__init__.py +1 -2
- synth_ai/task/rubrics/loaders.py +14 -10
- synth_ai/task/rubrics.py +219 -0
- synth_ai/task/trace_correlation_helpers.py +24 -11
- synth_ai/task/tracing_utils.py +14 -3
- synth_ai/task/validators.py +0 -1
- synth_ai/tracing_v3/abstractions.py +3 -3
- synth_ai/tracing_v3/config.py +15 -13
- synth_ai/tracing_v3/constants.py +21 -0
- synth_ai/tracing_v3/db_config.py +3 -1
- synth_ai/tracing_v3/decorators.py +10 -7
- synth_ai/tracing_v3/llm_call_record_helpers.py +5 -5
- synth_ai/tracing_v3/migration_helper.py +1 -2
- synth_ai/tracing_v3/session_tracer.py +7 -7
- synth_ai/tracing_v3/storage/base.py +29 -29
- synth_ai/tracing_v3/storage/config.py +3 -3
- synth_ai/tracing_v3/turso/daemon.py +8 -9
- synth_ai/tracing_v3/turso/native_manager.py +80 -72
- synth_ai/tracing_v3/utils.py +2 -2
- synth_ai/utils/__init__.py +101 -0
- synth_ai/utils/base_url.py +94 -0
- synth_ai/utils/cli.py +131 -0
- synth_ai/utils/env.py +294 -0
- synth_ai/utils/http.py +172 -0
- synth_ai/utils/modal.py +308 -0
- synth_ai/utils/process.py +212 -0
- synth_ai/utils/prompts.py +39 -0
- synth_ai/utils/sqld.py +122 -0
- synth_ai/utils/task_app_discovery.py +882 -0
- synth_ai/utils/task_app_env.py +186 -0
- synth_ai/utils/task_app_state.py +318 -0
- synth_ai/utils/user_config.py +137 -0
- synth_ai/v0/config/__init__.py +1 -5
- synth_ai/v0/config/base_url.py +1 -7
- synth_ai/v0/tracing/config.py +1 -1
- synth_ai/v0/tracing/decorators.py +1 -1
- synth_ai/v0/tracing/upload.py +1 -1
- synth_ai/v0/tracing_v1/config.py +1 -1
- synth_ai/v0/tracing_v1/decorators.py +1 -1
- synth_ai/v0/tracing_v1/upload.py +1 -1
- {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/METADATA +91 -32
- {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/RECORD +341 -154
- synth_ai/cli/man.py +0 -106
- synth_ai/cli/tui.py +0 -57
- synth_ai/compound/cais.py +0 -0
- synth_ai/core/experiment.py +0 -13
- synth_ai/core/system.py +0 -15
- synth_ai/demo_registry.py +0 -295
- synth_ai/handshake.py +0 -109
- synth_ai/tui/__init__.py +0 -5
- synth_ai/tui/__main__.py +0 -13
- synth_ai/tui/cli/__init__.py +0 -1
- synth_ai/tui/cli/query_experiments.py +0 -164
- synth_ai/tui/cli/query_experiments_v3.py +0 -164
- synth_ai/tui/dashboard.py +0 -906
- {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/licenses/LICENSE +0 -0
- {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# Image Validation Implementation Complete ✅
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
Added comprehensive validation for invalid/bogus image content in vision SFT data to catch errors **before**:
|
|
6
|
+
1. Inference API calls (prevents wasted API costs on invalid requests)
|
|
7
|
+
2. Training job submission (prevents hours of wasted GPU time)
|
|
8
|
+
|
|
9
|
+
## What Was Done
|
|
10
|
+
|
|
11
|
+
### 1. SDK Tests Added (11 new tests in `synth-ai/tests/unit/learning/test_sft_data.py`)
|
|
12
|
+
|
|
13
|
+
**Invalid Image Content Tests:**
|
|
14
|
+
- `test_validate_vision_example_empty_url` - Empty image URLs
|
|
15
|
+
- `test_validate_vision_example_missing_url_field` - Missing URL field in image_url
|
|
16
|
+
- `test_validate_vision_example_null_url` - Null URL values
|
|
17
|
+
- `test_validate_vision_example_malformed_image_dict` - Malformed image dict structure
|
|
18
|
+
- `test_validate_vision_example_non_string_url` - Non-string URL values (integers, etc.)
|
|
19
|
+
- `test_validate_vision_example_whitespace_only_url` - Whitespace-only URLs
|
|
20
|
+
- `test_validate_vision_example_invalid_scheme` - Invalid URL schemes (ftp://, etc.)
|
|
21
|
+
- `test_validate_vision_example_multiple_invalid_urls` - Multiple invalid URLs
|
|
22
|
+
- `test_validate_vision_example_mixed_valid_invalid` - Mix of valid and invalid (strict: fails)
|
|
23
|
+
- `test_extract_image_urls_filters_invalid` - URL extraction filtering
|
|
24
|
+
- `test_validate_vision_example_invalid_base64_format` - Malformed base64
|
|
25
|
+
|
|
26
|
+
**Test Results:** ✅ 42/42 tests passing (6 existing + 25 reasoning + 11 invalid image)
|
|
27
|
+
|
|
28
|
+
### 2. SDK Implementation Enhanced (`synth-ai/synth_ai/learning/sft/data.py`)
|
|
29
|
+
|
|
30
|
+
#### `extract_image_urls()` - Now filters out:
|
|
31
|
+
- Empty strings (`""`)
|
|
32
|
+
- Whitespace-only strings (`" "`)
|
|
33
|
+
- Non-string values (`None`, integers, etc.)
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
def extract_image_urls(content: SFTMessageContent) -> list[str]:
|
|
37
|
+
"""Extract all image URLs from message content.
|
|
38
|
+
|
|
39
|
+
Filters out invalid entries:
|
|
40
|
+
- Non-string URLs
|
|
41
|
+
- Empty strings
|
|
42
|
+
- Whitespace-only strings
|
|
43
|
+
...
|
|
44
|
+
"""
|
|
45
|
+
# Now checks: isinstance(url, str) and url.strip()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
#### `validate_vision_example()` - Strict validation:
|
|
49
|
+
- Counts image_url type entries vs valid URLs
|
|
50
|
+
- **Fails if ANY image_url entry has invalid/missing URL**
|
|
51
|
+
- Detects mismatches: `Has 2 image_url entries but only 1 valid URLs`
|
|
52
|
+
- Warns about suspicious schemes (non-http/https/data:image)
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
# If we have image_url type entries but fewer valid URLs, some are invalid
|
|
56
|
+
if len(urls) < image_type_count:
|
|
57
|
+
return False, f"Message {i}: Has {image_type_count} image_url entries but only {len(urls)} valid URLs"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 3. Monorepo Integration (Automatic)
|
|
61
|
+
|
|
62
|
+
**SFT Training** (`monorepo/backend/app/routes/simple_training/training/sft/data.py`):
|
|
63
|
+
- Already uses `sdk_validate_vision_example()` at line 401-406
|
|
64
|
+
- Automatically gets stricter validation
|
|
65
|
+
- Logs warnings and skips invalid examples:
|
|
66
|
+
```python
|
|
67
|
+
is_valid, error = sdk_validate_vision_example(sdk_example, require_images=True)
|
|
68
|
+
if not is_valid:
|
|
69
|
+
logger.warning("Vision example %s failed validation: %s", idx, error)
|
|
70
|
+
continue # Skip invalid example
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Inference** (`monorepo/backend/app/routes/simple_training/modal_service/gpu_functions.py`):
|
|
74
|
+
- Uses `_validate_inference_request()` at line 3827-3856
|
|
75
|
+
- Currently validates structure but **NOT image content**
|
|
76
|
+
- **TODO: Add image validation to prevent API failures**
|
|
77
|
+
|
|
78
|
+
## Validation Catches
|
|
79
|
+
|
|
80
|
+
### ❌ Rejected Examples:
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"messages": [
|
|
84
|
+
{
|
|
85
|
+
"role": "user",
|
|
86
|
+
"content": [
|
|
87
|
+
{"type": "text", "text": "What's this?"},
|
|
88
|
+
{"type": "image_url", "image_url": {"url": ""}} // Empty!
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
**Error:** `"Message 0: Has 1 image_url entries but only 0 valid URLs (some are empty, null, or missing)"`
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"messages": [
|
|
99
|
+
{
|
|
100
|
+
"role": "user",
|
|
101
|
+
"content": [
|
|
102
|
+
{"type": "image_url", "image_url": {}} // Missing url field
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
**Error:** `"Message 0: Has 1 image_url entries but only 0 valid URLs"`
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"messages": [
|
|
113
|
+
{
|
|
114
|
+
"role": "user",
|
|
115
|
+
"content": [
|
|
116
|
+
{"type": "image_url", "image_url": {"url": "https://valid.jpg"}},
|
|
117
|
+
{"type": "image_url", "image_url": {"url": " "}} // Whitespace!
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
**Error:** `"Message 0: Has 2 image_url entries but only 1 valid URLs"`
|
|
124
|
+
|
|
125
|
+
### ✅ Accepted Examples:
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"messages": [
|
|
129
|
+
{
|
|
130
|
+
"role": "user",
|
|
131
|
+
"content": [
|
|
132
|
+
{"type": "text", "text": "Describe this"},
|
|
133
|
+
{"type": "image_url", "image_url": {"url": "https://example.com/image.jpg"}}
|
|
134
|
+
]
|
|
135
|
+
},
|
|
136
|
+
{"role": "assistant", "content": "A beautiful image"}
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"messages": [
|
|
144
|
+
{
|
|
145
|
+
"role": "user",
|
|
146
|
+
"content": [
|
|
147
|
+
{"type": "image_url", "image_url": {"url": "data:image/png;base64,iVBORw0KGgo..."}}
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Benefits
|
|
155
|
+
|
|
156
|
+
### For SFT Training:
|
|
157
|
+
1. **Early Detection:** Invalid examples caught during data preparation, not after hours of training
|
|
158
|
+
2. **Clear Errors:** Specific messages like "Has 2 image_url entries but only 1 valid URLs"
|
|
159
|
+
3. **Cost Savings:** Prevents wasted GPU time on datasets with invalid images
|
|
160
|
+
4. **Data Quality:** Ensures all training examples have valid image content
|
|
161
|
+
|
|
162
|
+
### For Inference:
|
|
163
|
+
1. **API Cost Savings:** Prevents sending invalid requests to OpenAI/Groq/etc.
|
|
164
|
+
2. **Faster Failures:** Fail-fast before network call, not after timeout
|
|
165
|
+
3. **Better Error Messages:** User knows exactly what's wrong with their image data
|
|
166
|
+
|
|
167
|
+
## Testing
|
|
168
|
+
|
|
169
|
+
### Run SDK tests:
|
|
170
|
+
```bash
|
|
171
|
+
cd /Users/joshpurtell/Documents/GitHub/synth-ai
|
|
172
|
+
uv run pytest tests/unit/learning/test_sft_data.py -v
|
|
173
|
+
|
|
174
|
+
# Just invalid image tests:
|
|
175
|
+
uv run pytest tests/unit/learning/test_sft_data.py -k "empty_url or missing_url or null_url or malformed or non_string or whitespace or invalid_scheme or multiple_invalid or mixed_valid or filters_invalid or invalid_base64" -v
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Test with actual data:
|
|
179
|
+
```python
|
|
180
|
+
from synth_ai.learning.sft.data import coerce_example, validate_vision_example
|
|
181
|
+
|
|
182
|
+
# This will fail validation:
|
|
183
|
+
example_data = {
|
|
184
|
+
"messages": [
|
|
185
|
+
{
|
|
186
|
+
"role": "user",
|
|
187
|
+
"content": [
|
|
188
|
+
{"type": "text", "text": "Check this"},
|
|
189
|
+
{"type": "image_url", "image_url": {"url": ""}}, # Empty!
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
{"role": "assistant", "content": "Response"},
|
|
193
|
+
]
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
example = coerce_example(example_data)
|
|
197
|
+
is_valid, error = validate_vision_example(example, require_images=True)
|
|
198
|
+
print(f"Valid: {is_valid}, Error: {error}")
|
|
199
|
+
# Output: Valid: False, Error: Message 0: Has 1 image_url entries but only 0 valid URLs...
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Next Steps
|
|
203
|
+
|
|
204
|
+
### 1. Add Inference Validation (High Priority)
|
|
205
|
+
Update `_validate_inference_request` to validate image content:
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
# In monorepo/backend/app/routes/simple_training/modal_service/gpu_functions.py
|
|
209
|
+
|
|
210
|
+
def _validate_inference_request(request: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
211
|
+
"""Validate inference request and return messages."""
|
|
212
|
+
# ... existing validation ...
|
|
213
|
+
|
|
214
|
+
# NEW: Validate image content if present
|
|
215
|
+
if SDK_SFT_AVAILABLE:
|
|
216
|
+
for i, msg in enumerate(messages):
|
|
217
|
+
content = msg.get("content")
|
|
218
|
+
if isinstance(content, list):
|
|
219
|
+
# Check for image_url entries
|
|
220
|
+
has_images = any(
|
|
221
|
+
isinstance(item, dict) and item.get("type") in {"image", "image_url"}
|
|
222
|
+
for item in content
|
|
223
|
+
)
|
|
224
|
+
if has_images:
|
|
225
|
+
urls = sdk_extract_image_urls(content)
|
|
226
|
+
image_count = sum(
|
|
227
|
+
1 for item in content
|
|
228
|
+
if isinstance(item, dict) and item.get("type") in {"image", "image_url"}
|
|
229
|
+
)
|
|
230
|
+
if len(urls) < image_count:
|
|
231
|
+
raise ValueError(
|
|
232
|
+
f"Message {i}: Has {image_count} image entries but only {len(urls)} valid URLs"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
return messages
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### 2. Add API-Level Validation
|
|
239
|
+
Add validation in backend API routes before forwarding to Modal.
|
|
240
|
+
|
|
241
|
+
### 3. Integration Tests
|
|
242
|
+
Add integration tests that verify rejected examples at the API level.
|
|
243
|
+
|
|
244
|
+
## Files Modified
|
|
245
|
+
|
|
246
|
+
### SDK:
|
|
247
|
+
- `synth-ai/synth_ai/learning/sft/data.py` - Enhanced validation logic
|
|
248
|
+
- `synth-ai/tests/unit/learning/test_sft_data.py` - Added 11 invalid image tests
|
|
249
|
+
|
|
250
|
+
### Monorepo:
|
|
251
|
+
- No changes needed - automatically uses enhanced SDK validation in SFT training
|
|
252
|
+
- **TODO:** Add validation to `monorepo/backend/app/routes/simple_training/modal_service/gpu_functions.py`
|
|
253
|
+
|
|
254
|
+
## Related Issues Prevented
|
|
255
|
+
|
|
256
|
+
### Without this validation:
|
|
257
|
+
1. **Training Job Failures:** Hours into training, discover dataset has empty image URLs
|
|
258
|
+
2. **API Errors:** Send requests with invalid base64, get 400 errors from OpenAI
|
|
259
|
+
3. **Silent Failures:** Model trained on text-only when images expected
|
|
260
|
+
4. **Cost Waste:** GPU time and API calls on invalid data
|
|
261
|
+
|
|
262
|
+
### With this validation:
|
|
263
|
+
1. **Immediate Feedback:** Know within seconds if data is invalid
|
|
264
|
+
2. **Clear Error Messages:** Exactly which message and what's wrong
|
|
265
|
+
3. **Confidence:** All training/inference data has been validated
|
|
266
|
+
4. **Cost Savings:** Never waste resources on bogus data
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
**Status:** ✅ SDK validation complete and tested. Monorepo SFT training automatically protected. Inference validation recommended as next step.
|
|
271
|
+
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# Image Validation Complete - Summary ✅
|
|
2
|
+
|
|
3
|
+
## Mission Accomplished
|
|
4
|
+
|
|
5
|
+
Added comprehensive validation for **invalid/bogus image content** to prevent errors before:
|
|
6
|
+
1. ❌ Wasted API calls to OpenAI/Groq/vLLM with invalid images
|
|
7
|
+
2. ❌ Wasted GPU hours training on corrupted datasets
|
|
8
|
+
3. ❌ Silent failures where models train on text-only when images expected
|
|
9
|
+
|
|
10
|
+
## What We Built
|
|
11
|
+
|
|
12
|
+
### 1. SDK Enhancement (`synth-ai`) ✅
|
|
13
|
+
|
|
14
|
+
**New Validation Logic:**
|
|
15
|
+
- `extract_image_urls()` - Filters out empty, null, whitespace-only, and non-string URLs
|
|
16
|
+
- `validate_vision_example()` - Strict validation that fails if ANY image entry is invalid
|
|
17
|
+
- Detects mismatches: "Has 2 image_url entries but only 1 valid URLs"
|
|
18
|
+
|
|
19
|
+
**Test Coverage:** 42/42 passing
|
|
20
|
+
- 6 existing SFT data tests
|
|
21
|
+
- 25 reasoning/thinking tests
|
|
22
|
+
- 11 NEW invalid image validation tests
|
|
23
|
+
|
|
24
|
+
### 2. Monorepo Integration ✅
|
|
25
|
+
|
|
26
|
+
**SFT Training Protection:**
|
|
27
|
+
- `backend/app/routes/simple_training/training/sft/data.py` (line 401-406)
|
|
28
|
+
- Already uses `sdk_validate_vision_example()`
|
|
29
|
+
- **Automatically protected** - no code changes needed!
|
|
30
|
+
|
|
31
|
+
**Inference Protection:**
|
|
32
|
+
- `backend/app/routes/simple_training/modal_service/gpu_functions.py` (line 3827-3915)
|
|
33
|
+
- Enhanced `_validate_inference_request()` with image validation
|
|
34
|
+
- **Now validates images before vLLM inference calls**
|
|
35
|
+
|
|
36
|
+
## Validation Examples
|
|
37
|
+
|
|
38
|
+
### ❌ **REJECTED** - Empty URL:
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"messages": [{
|
|
42
|
+
"role": "user",
|
|
43
|
+
"content": [
|
|
44
|
+
{"type": "text", "text": "What's this?"},
|
|
45
|
+
{"type": "image_url", "image_url": {"url": ""}} // ← CAUGHT!
|
|
46
|
+
]
|
|
47
|
+
}]
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
**Error:** `"Message 0: Has 1 image_url entries but only 0 valid URLs"`
|
|
51
|
+
|
|
52
|
+
### ❌ **REJECTED** - Missing URL field:
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"messages": [{
|
|
56
|
+
"role": "user",
|
|
57
|
+
"content": [
|
|
58
|
+
{"type": "image_url", "image_url": {}} // ← No url field!
|
|
59
|
+
]
|
|
60
|
+
}]
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
**Error:** `"Message 0: Has 1 image_url entries but only 0 valid URLs"`
|
|
64
|
+
|
|
65
|
+
### ❌ **REJECTED** - Mixed valid/invalid (strict):
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"messages": [{
|
|
69
|
+
"role": "user",
|
|
70
|
+
"content": [
|
|
71
|
+
{"type": "image_url", "image_url": {"url": "https://valid.jpg"}}, // Valid
|
|
72
|
+
{"type": "image_url", "image_url": {"url": " "}} // ← Whitespace!
|
|
73
|
+
]
|
|
74
|
+
}]
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
**Error:** `"Message 0: Has 2 image_url entries but only 1 valid URLs"`
|
|
78
|
+
|
|
79
|
+
### ✅ **ACCEPTED** - Valid image:
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"messages": [
|
|
83
|
+
{
|
|
84
|
+
"role": "user",
|
|
85
|
+
"content": [
|
|
86
|
+
{"type": "text", "text": "Describe this"},
|
|
87
|
+
{"type": "image_url", "image_url": {"url": "https://example.com/img.jpg"}}
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
{"role": "assistant", "content": "A beautiful image"}
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Test Coverage
|
|
96
|
+
|
|
97
|
+
### Invalid Image Tests (11 new):
|
|
98
|
+
```bash
|
|
99
|
+
✅ test_validate_vision_example_empty_url
|
|
100
|
+
✅ test_validate_vision_example_missing_url_field
|
|
101
|
+
✅ test_validate_vision_example_null_url
|
|
102
|
+
✅ test_validate_vision_example_malformed_image_dict
|
|
103
|
+
✅ test_validate_vision_example_non_string_url
|
|
104
|
+
✅ test_validate_vision_example_whitespace_only_url
|
|
105
|
+
✅ test_validate_vision_example_invalid_scheme
|
|
106
|
+
✅ test_validate_vision_example_multiple_invalid_urls
|
|
107
|
+
✅ test_validate_vision_example_mixed_valid_invalid
|
|
108
|
+
✅ test_extract_image_urls_filters_invalid
|
|
109
|
+
✅ test_validate_vision_example_invalid_base64_format
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Run Tests:
|
|
113
|
+
```bash
|
|
114
|
+
cd /Users/joshpurtell/Documents/GitHub/synth-ai
|
|
115
|
+
uv run pytest tests/unit/learning/test_sft_data.py -v
|
|
116
|
+
|
|
117
|
+
# Just invalid image tests:
|
|
118
|
+
uv run pytest tests/unit/learning/test_sft_data.py -k "invalid or bogus or empty_url or null_url or malformed or whitespace" -v
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Impact
|
|
122
|
+
|
|
123
|
+
### Before This Work ❌
|
|
124
|
+
- **Training:** Hours into GPU job before discovering dataset has empty image URLs
|
|
125
|
+
- **Inference:** Send request to OpenAI → get 400 error → debug → retry
|
|
126
|
+
- **Cost:** Waste $$ on API calls and GPU time for invalid data
|
|
127
|
+
- **Silent Failures:** Model trains on text-only, no one notices images missing
|
|
128
|
+
|
|
129
|
+
### After This Work ✅
|
|
130
|
+
- **Training:** Invalid examples caught during data prep, logged and skipped
|
|
131
|
+
- **Inference:** Request fails instantly with clear error before API call
|
|
132
|
+
- **Cost:** Zero waste - validation is instantaneous and local
|
|
133
|
+
- **Confidence:** All data validated, no silent failures possible
|
|
134
|
+
|
|
135
|
+
## Files Modified
|
|
136
|
+
|
|
137
|
+
### `synth-ai/` (SDK):
|
|
138
|
+
1. **`synth_ai/learning/sft/data.py`**
|
|
139
|
+
- Enhanced `extract_image_urls()` to filter invalid entries
|
|
140
|
+
- Enhanced `validate_vision_example()` with strict validation
|
|
141
|
+
- Added proper None checks for type safety
|
|
142
|
+
|
|
143
|
+
2. **`tests/unit/learning/test_sft_data.py`**
|
|
144
|
+
- Added 11 new invalid image validation tests
|
|
145
|
+
- All 42 tests passing ✅
|
|
146
|
+
|
|
147
|
+
3. **`examples/qwen_vl/IMAGE_VALIDATION_COMPLETE.md`**
|
|
148
|
+
- Detailed documentation with examples
|
|
149
|
+
|
|
150
|
+
### `monorepo/` (Backend):
|
|
151
|
+
1. **`backend/app/routes/simple_training/training/sft/data.py`**
|
|
152
|
+
- **No changes needed** - already uses SDK validation ✅
|
|
153
|
+
|
|
154
|
+
2. **`backend/app/routes/simple_training/modal_service/gpu_functions.py`**
|
|
155
|
+
- Enhanced `_validate_inference_request()` (line 3827-3915)
|
|
156
|
+
- Added image content validation for multimodal inference requests
|
|
157
|
+
- Self-contained (no SDK dependency for Modal deployment)
|
|
158
|
+
|
|
159
|
+
## Error Messages (Developer-Friendly)
|
|
160
|
+
|
|
161
|
+
All validation errors are **specific and actionable**:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
# Empty URL
|
|
165
|
+
"Message 0: Image URL cannot be empty or whitespace-only"
|
|
166
|
+
|
|
167
|
+
# Missing URL field
|
|
168
|
+
"Message 0: Image entry missing URL field. Expected image_url.url or image field."
|
|
169
|
+
|
|
170
|
+
# Non-string URL
|
|
171
|
+
"Message 0: Image URL must be a string, got int"
|
|
172
|
+
|
|
173
|
+
# Mismatch count
|
|
174
|
+
"Message 0: Has 2 image_url entries but only 1 valid URLs. Some URLs are invalid, empty, or missing."
|
|
175
|
+
|
|
176
|
+
# No images when required
|
|
177
|
+
"No image content found in any message"
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Validation Behavior
|
|
181
|
+
|
|
182
|
+
### `extract_image_urls()` - Filters out:
|
|
183
|
+
- ❌ Empty strings: `""`
|
|
184
|
+
- ❌ Whitespace-only: `" "`
|
|
185
|
+
- ❌ Non-strings: `None`, `123`, `[]`
|
|
186
|
+
- ❌ Missing `url` field
|
|
187
|
+
- ✅ Returns only valid URL strings
|
|
188
|
+
|
|
189
|
+
### `validate_vision_example()` - Strict:
|
|
190
|
+
- Counts `image_url` type entries vs valid URLs extracted
|
|
191
|
+
- **Fails if count mismatch** (some entries have invalid URLs)
|
|
192
|
+
- Warns about suspicious schemes (non-http/https/data:image)
|
|
193
|
+
- Validates each URL: must be non-empty string
|
|
194
|
+
|
|
195
|
+
### Inference Validation - Fail-Fast:
|
|
196
|
+
- Validates before vLLM API call
|
|
197
|
+
- Clear error messages
|
|
198
|
+
- Prevents wasted network/GPU time
|
|
199
|
+
|
|
200
|
+
## Future Enhancements (Optional)
|
|
201
|
+
|
|
202
|
+
1. **Base64 Decoding Validation:**
|
|
203
|
+
- Currently: Check URL string format only
|
|
204
|
+
- Future: Validate base64 can be decoded (add flag to avoid perf hit)
|
|
205
|
+
|
|
206
|
+
2. **Image Size Validation:**
|
|
207
|
+
- Currently: Any valid URL accepted
|
|
208
|
+
- Future: Check decoded image size limits (e.g., < 20MB)
|
|
209
|
+
|
|
210
|
+
3. **Format Validation:**
|
|
211
|
+
- Currently: URL scheme check only
|
|
212
|
+
- Future: Validate image format (PNG, JPEG, etc.) if base64
|
|
213
|
+
|
|
214
|
+
4. **Integration Tests:**
|
|
215
|
+
- Add E2E tests that submit invalid data to API
|
|
216
|
+
- Verify proper error responses
|
|
217
|
+
|
|
218
|
+
## Usage
|
|
219
|
+
|
|
220
|
+
### For SFT Training:
|
|
221
|
+
```python
|
|
222
|
+
from synth_ai.learning.sft.data import coerce_example, validate_vision_example
|
|
223
|
+
|
|
224
|
+
example = coerce_example(raw_data)
|
|
225
|
+
is_valid, error = validate_vision_example(example, require_images=True)
|
|
226
|
+
|
|
227
|
+
if not is_valid:
|
|
228
|
+
print(f"Skipping invalid example: {error}")
|
|
229
|
+
# Log and skip, don't train on this data
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### For Inference:
|
|
233
|
+
```python
|
|
234
|
+
# In monorepo backend, validation happens automatically:
|
|
235
|
+
# 1. API endpoint receives request
|
|
236
|
+
# 2. _validate_inference_request() called
|
|
237
|
+
# 3. If images invalid → ValueError raised → 400 error returned
|
|
238
|
+
# 4. If images valid → forwarded to vLLM
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Related Work
|
|
242
|
+
|
|
243
|
+
This builds on previous enhancements:
|
|
244
|
+
- **Reasoning Support:** Added `reasoning` and `raw_content` fields with validation
|
|
245
|
+
- **Vision Support:** Added multimodal message handling for Crafter VLM pipeline
|
|
246
|
+
- **SDK Integration:** Monorepo backend uses SDK for consistent data handling
|
|
247
|
+
|
|
248
|
+
## Status
|
|
249
|
+
|
|
250
|
+
✅ **Complete and Production-Ready**
|
|
251
|
+
|
|
252
|
+
- SDK enhanced with strict validation
|
|
253
|
+
- Comprehensive test coverage (42/42 passing)
|
|
254
|
+
- Monorepo SFT training automatically protected
|
|
255
|
+
- Monorepo inference validation added
|
|
256
|
+
- No lint errors
|
|
257
|
+
- Documentation complete
|
|
258
|
+
|
|
259
|
+
**Ready to catch bogus images before they cost you $$!** 💰
|
|
260
|
+
|